Repository: charmbracelet/lipgloss Branch: main Commit: cd93a9f5d2e3 Files: 300 Total size: 553.6 KB Directory structure: gitextract_jquxj2jk/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── dependabot.yml │ └── workflows/ │ ├── build.yml │ ├── coverage.yml │ ├── dependabot-sync.yml │ ├── lint-sync.yml │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── Taskfile.yaml ├── UPGRADE_GUIDE_V2.md ├── align.go ├── align_test.go ├── ansi_unix.go ├── ansi_windows.go ├── blending.go ├── blending_test.go ├── borders.go ├── borders_test.go ├── canvas.go ├── canvas_test.go ├── color.go ├── color_test.go ├── compat/ │ ├── color.go │ └── doc.go ├── examples/ │ ├── blending/ │ │ ├── border-blend-rotation/ │ │ │ └── bubbletea/ │ │ │ └── main.go │ │ ├── linear-1d/ │ │ │ ├── bubbletea/ │ │ │ │ └── main.go │ │ │ └── standalone/ │ │ │ └── main.go │ │ └── linear-2d/ │ │ ├── bubbletea/ │ │ │ └── main.go │ │ └── standalone/ │ │ └── main.go │ ├── brightness/ │ │ └── main.go │ ├── canvas/ │ │ └── main.go │ ├── color/ │ │ ├── bubbletea/ │ │ │ └── main.go │ │ └── standalone/ │ │ └── main.go │ ├── compat/ │ │ ├── bubbletea/ │ │ │ └── main.go │ │ └── standalone/ │ │ └── main.go │ ├── go.mod │ ├── go.sum │ ├── layout/ │ │ └── main.go │ ├── list/ │ │ ├── duckduckgoose/ │ │ │ └── main.go │ │ ├── glow/ │ │ │ └── main.go │ │ ├── grocery/ │ │ │ └── main.go │ │ ├── roman/ │ │ │ └── main.go │ │ ├── simple/ │ │ │ └── main.go │ │ └── sublist/ │ │ └── main.go │ ├── ssh/ │ │ └── main.go │ ├── table/ │ │ ├── ansi/ │ │ │ └── main.go │ │ ├── chess/ │ │ │ └── main.go │ │ ├── demo.tape │ │ ├── languages/ │ │ │ └── main.go │ │ ├── mindy/ │ │ │ └── main.go │ │ └── pokemon/ │ │ └── main.go │ └── tree/ │ ├── background/ │ │ └── main.go │ ├── files/ │ │ └── main.go │ ├── makeup/ │ │ └── main.go │ ├── rounded/ │ │ └── main.go │ ├── selection/ │ │ └── main.go │ ├── simple/ │ │ └── main.go │ ├── styles/ │ │ └── main.go │ └── toggle/ │ └── main.go ├── get.go ├── go.mod ├── go.sum ├── join.go ├── join_test.go ├── layer.go ├── lipgloss.go ├── list/ │ ├── enumerator.go │ ├── list.go │ ├── list_test.go │ └── testdata/ │ ├── TestComplexSublist.golden │ ├── TestEnumerators/ │ │ ├── alphabet.golden │ │ ├── arabic.golden │ │ ├── asterisk.golden │ │ ├── bullet.golden │ │ ├── dash.golden │ │ └── roman.golden │ ├── TestEnumeratorsAlign.golden │ ├── TestEnumeratorsTransform/ │ │ ├── alphabet_lower.golden │ │ ├── arabic).golden │ │ ├── bullet_is_dash.golden │ │ └── roman_within_().golden │ ├── TestList.golden │ ├── TestListIntegers.golden │ ├── TestListItems.golden │ ├── TestMultiline.golden │ ├── TestSubListItems2.golden │ ├── TestSublist.golden │ └── TestSublistItems.golden ├── position.go ├── query.go ├── ranges.go ├── ranges_test.go ├── runes.go ├── runes_test.go ├── set.go ├── size.go ├── size_test.go ├── style.go ├── style_test.go ├── table/ │ ├── resizing.go │ ├── rows.go │ ├── table.go │ ├── table_test.go │ ├── testdata/ │ │ ├── TestBorderColumnsWithExtraRows.golden │ │ ├── TestBorderStyles/ │ │ │ ├── ASCIIBorder.golden │ │ │ ├── BlockBorder.golden │ │ │ ├── HiddenBorder.golden │ │ │ ├── MarkdownBorder.golden │ │ │ ├── NormalBorder.golden │ │ │ ├── RoundedBorder.golden │ │ │ └── ThickBorder.golden │ │ ├── TestBorderedCells.golden │ │ ├── TestCarriageReturn.golden │ │ ├── TestContentWrapping/ │ │ │ ├── LongHeaderContentLongAndShortRows.golden │ │ │ ├── LongRowContent.golden │ │ │ ├── LongRowContentNoWrap.golden │ │ │ ├── LongRowContentNoWrapCustomMargins.golden │ │ │ ├── LongRowContentNoWrapNoMargins.golden │ │ │ ├── LongTextDifferentLanguages.golden │ │ │ └── MissingRowContent.golden │ │ ├── TestContentWrapping_ColumnWidth/ │ │ │ ├── LongHeaderContentLongAndShortRows.golden │ │ │ ├── LongRowContent.golden │ │ │ ├── LongTextDifferentLanguages.golden │ │ │ └── MissingRowContent.golden │ │ ├── TestContentWrapping_WithHeight/ │ │ │ └── LongHeaderContentLongAndShortRows/ │ │ │ ├── HeightOf05.golden │ │ │ ├── HeightOf15.golden │ │ │ ├── HeightOf25.golden │ │ │ └── HeightOf35.golden │ │ ├── TestContentWrapping_WithMargins/ │ │ │ ├── LongHeaderContentLongAndShortRows.golden │ │ │ ├── LongRowContent.golden │ │ │ ├── LongTextDifferentLanguages.golden │ │ │ └── MissingRowContent.golden │ │ ├── TestContentWrapping_WithPadding/ │ │ │ ├── LongHeaderContentLongAndShortRows.golden │ │ │ ├── LongRowContent.golden │ │ │ ├── LongTextDifferentLanguages.golden │ │ │ └── MissingRowContent.golden │ │ ├── TestExtraPaddingHeading.golden │ │ ├── TestExtraPaddingHeadingLong.golden │ │ ├── TestFilter.golden │ │ ├── TestFilterInverse.golden │ │ ├── TestInnerBordersOnly.golden │ │ ├── TestMoreCellsThanHeaders.golden │ │ ├── TestMoreCellsThanHeadersExtra.golden │ │ ├── TestNoFinalEmptyRowWhenOverflow.golden │ │ ├── TestStyleFunc/ │ │ │ ├── MarginAndPaddingSet.golden │ │ │ └── RightAlignedTextWithMargins.golden │ │ ├── TestTable.golden │ │ ├── TestTableANSI.golden │ │ ├── TestTableBorder.golden │ │ ├── TestTableEmpty.golden │ │ ├── TestTableExample.golden │ │ ├── TestTableHeightExact.golden │ │ ├── TestTableHeightExtra.golden │ │ ├── TestTableHeightShrink/ │ │ │ ├── NoBorderRow/ │ │ │ │ ├── HeightOf01.golden │ │ │ │ ├── HeightOf02.golden │ │ │ │ ├── HeightOf03.golden │ │ │ │ ├── HeightOf04.golden │ │ │ │ ├── HeightOf05.golden │ │ │ │ ├── HeightOf06.golden │ │ │ │ ├── HeightOf07.golden │ │ │ │ ├── HeightOf08.golden │ │ │ │ └── HeightOf09.golden │ │ │ ├── NoBorderRowPadding/ │ │ │ │ ├── HeightOf01.golden │ │ │ │ ├── HeightOf02.golden │ │ │ │ ├── HeightOf03.golden │ │ │ │ ├── HeightOf04.golden │ │ │ │ ├── HeightOf05.golden │ │ │ │ ├── HeightOf06.golden │ │ │ │ ├── HeightOf07.golden │ │ │ │ ├── HeightOf08.golden │ │ │ │ ├── HeightOf09.golden │ │ │ │ ├── HeightOf10.golden │ │ │ │ ├── HeightOf11.golden │ │ │ │ ├── HeightOf12.golden │ │ │ │ ├── HeightOf13.golden │ │ │ │ ├── HeightOf14.golden │ │ │ │ ├── HeightOf15.golden │ │ │ │ ├── HeightOf16.golden │ │ │ │ ├── HeightOf17.golden │ │ │ │ ├── HeightOf18.golden │ │ │ │ ├── HeightOf19.golden │ │ │ │ ├── HeightOf20.golden │ │ │ │ └── HeightOf21.golden │ │ │ ├── WithBorderRow/ │ │ │ │ ├── HeightOf01.golden │ │ │ │ ├── HeightOf02.golden │ │ │ │ ├── HeightOf03.golden │ │ │ │ ├── HeightOf04.golden │ │ │ │ ├── HeightOf05.golden │ │ │ │ ├── HeightOf06.golden │ │ │ │ ├── HeightOf07.golden │ │ │ │ ├── HeightOf08.golden │ │ │ │ ├── HeightOf09.golden │ │ │ │ ├── HeightOf10.golden │ │ │ │ ├── HeightOf11.golden │ │ │ │ ├── HeightOf12.golden │ │ │ │ └── HeightOf13.golden │ │ │ └── WithBorderRowPadding/ │ │ │ ├── HeightOf01.golden │ │ │ ├── HeightOf02.golden │ │ │ ├── HeightOf03.golden │ │ │ ├── HeightOf04.golden │ │ │ ├── HeightOf05.golden │ │ │ ├── HeightOf06.golden │ │ │ ├── HeightOf07.golden │ │ │ ├── HeightOf08.golden │ │ │ ├── HeightOf09.golden │ │ │ ├── HeightOf10.golden │ │ │ ├── HeightOf11.golden │ │ │ ├── HeightOf12.golden │ │ │ ├── HeightOf13.golden │ │ │ ├── HeightOf14.golden │ │ │ ├── HeightOf15.golden │ │ │ ├── HeightOf16.golden │ │ │ ├── HeightOf17.golden │ │ │ ├── HeightOf18.golden │ │ │ ├── HeightOf19.golden │ │ │ ├── HeightOf20.golden │ │ │ ├── HeightOf21.golden │ │ │ ├── HeightOf22.golden │ │ │ ├── HeightOf23.golden │ │ │ ├── HeightOf24.golden │ │ │ └── HeightOf25.golden │ │ ├── TestTableHeightWithYOffset.golden │ │ ├── TestTableHeights.golden │ │ ├── TestTableMarginAndRightAlignment.golden │ │ ├── TestTableMultiLineRowSeparator.golden │ │ ├── TestTableNoColumnSeparators.golden │ │ ├── TestTableNoColumnSeparatorsWithHeaders.golden │ │ ├── TestTableNoHeaders.golden │ │ ├── TestTableNoStyleFunc.golden │ │ ├── TestTableOverFlowNoWrap.golden │ │ ├── TestTableRowSeparators/ │ │ │ ├── no_overflow.golden │ │ │ └── with_overflow.golden │ │ ├── TestTableRowSeparators.golden │ │ ├── TestTableSetRows.golden │ │ ├── TestTableShrinkWithYOffset/ │ │ │ ├── NoHeaders.golden │ │ │ ├── WithBorderRow.golden │ │ │ └── WithHeaders.golden │ │ ├── TestTableUnsetBorders.golden │ │ ├── TestTableUnsetHeaderSeparator.golden │ │ ├── TestTableUnsetHeaderSeparatorWithBorder.golden │ │ ├── TestTableWidthExpand.golden │ │ ├── TestTableWidthShrink/ │ │ │ ├── DefaultBorders.golden │ │ │ ├── NoBorders.golden │ │ │ └── OutlineBordersOnly.golden │ │ ├── TestTableWidthSmartCrop.golden │ │ ├── TestTableWidthSmartCropExtensive.golden │ │ ├── TestTableWidthSmartCropTiny.golden │ │ ├── TestTableWidths.golden │ │ ├── TestTableWithBackground.golden │ │ ├── TestTableYOffset.golden │ │ ├── TestWrapPreStyledContent.golden │ │ └── TestWrapStyleFuncContent.golden │ └── util.go ├── terminal.go ├── tree/ │ ├── children.go │ ├── enumerator.go │ ├── example_test.go │ ├── renderer.go │ ├── testdata/ │ │ ├── TestAddItemWithAndWithoutRoot/ │ │ │ ├── with_root.golden │ │ │ └── without_root.golden │ │ ├── TestEmbedListWithinTree.golden │ │ ├── TestFilter.golden │ │ ├── TestMultilinePrefix.golden │ │ ├── TestMultilinePrefixInception.golden │ │ ├── TestMultilinePrefixSubtree.golden │ │ ├── TestRootStyle.golden │ │ ├── TestTree/ │ │ │ ├── after.golden │ │ │ └── before.golden │ │ ├── TestTreeAddTwoSubTreesWithoutName.golden │ │ ├── TestTreeAllHidden.golden │ │ ├── TestTreeCustom.golden │ │ ├── TestTreeHidden.golden │ │ ├── TestTreeLastNodeIsSubTree.golden │ │ ├── TestTreeMixedEnumeratorSize.golden │ │ ├── TestTreeMultilineNode.golden │ │ ├── TestTreeNil.golden │ │ ├── TestTreeRoot.golden │ │ ├── TestTreeStartsWithSubtree.golden │ │ ├── TestTreeStyleAt.golden │ │ ├── TestTreeStyleNilFuncs.golden │ │ ├── TestTreeSubTreeWithCustomEnumerator.golden │ │ ├── TestTreeTable.golden │ │ └── TestTypes.golden │ ├── tree.go │ └── tree_test.go ├── unset.go ├── whitespace.go ├── whitespace_test.go ├── wrap.go └── writer.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org/ root = true [*] charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true indent_style = space indent_size = 2 [*.go] indent_style = tab indent_size = 8 [*.golden] insert_final_newline = false trim_trailing_whitespace = false ================================================ FILE: .gitattributes ================================================ *.golden linguist-generated=true -text ================================================ FILE: .github/CODEOWNERS ================================================ * @meowgorithm @aymanbagabas ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **Setup** Please complete the following information along with version numbers, if applicable. - OS [e.g. Ubuntu, macOS] - Shell [e.g. zsh, fish] - Terminal Emulator [e.g. kitty, iterm] - Terminal Multiplexer [e.g. tmux] - Locale [e.g. en_US.UTF-8, zh_CN.UTF-8, etc.] **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Source Code** Please include source code if needed to reproduce the behavior. **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** Add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Discord url: https://charm.sh/discord about: Chat on our Discord. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" day: "monday" time: "05:00" timezone: "America/New_York" labels: - "dependencies" commit-message: prefix: "chore" include: "scope" groups: all: patterns: - "*" ignore: - dependency-name: github.com/charmbracelet/bubbletea/v2 versions: - v2.0.0-beta1 - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" day: "monday" time: "05:00" timezone: "America/New_York" labels: - "dependencies" commit-message: prefix: "chore" include: "scope" groups: all: patterns: - "*" - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly" day: "monday" time: "05:00" timezone: "America/New_York" labels: - "dependencies" commit-message: prefix: "chore" include: "scope" groups: all: patterns: - "*" - package-ecosystem: "gomod" directory: "/example" schedule: interval: "weekly" day: "monday" time: "05:00" timezone: "America/New_York" labels: - "dependencies" commit-message: prefix: "chore" include: "scope" groups: all: patterns: - "*" ================================================ FILE: .github/workflows/build.yml ================================================ name: build on: push: branches: - "master" pull_request: jobs: build: uses: charmbracelet/meta/.github/workflows/build.yml@main secrets: gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }} ================================================ FILE: .github/workflows/coverage.yml ================================================ name: coverage on: [push, pull_request] jobs: coverage: strategy: matrix: go-version: [^1] os: [ubuntu-latest] runs-on: ${{ matrix.os }} env: GO111MODULE: "on" steps: - name: Install Go uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v6 - run: | git config --global url."https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/charmbracelet".insteadOf "https://github.com/charmbracelet" git config --global url."https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/charmcli".insteadOf "https://github.com/charmcli" - name: Coverage env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | go test -race -covermode atomic -coverprofile=profile.cov ./... go install github.com/mattn/goveralls@latest goveralls -coverprofile=profile.cov -service=github ================================================ FILE: .github/workflows/dependabot-sync.yml ================================================ name: dependabot-sync on: schedule: - cron: "0 0 * * 0" # every Sunday at midnight workflow_dispatch: # allows manual triggering permissions: contents: write pull-requests: write jobs: dependabot-sync: uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main with: repo_name: ${{ github.event.repository.name }} secrets: gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} ================================================ FILE: .github/workflows/lint-sync.yml ================================================ name: lint-sync on: schedule: # every Sunday at midnight - cron: "0 0 * * 0" workflow_dispatch: # allows manual triggering permissions: contents: write pull-requests: write jobs: lint: uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main ================================================ FILE: .github/workflows/lint.yml ================================================ name: lint on: push: pull_request: jobs: lint: uses: charmbracelet/meta/.github/workflows/lint.yml@main with: golangci_path: .golangci.yml golangci_version: v2.9 timeout: 10m ================================================ FILE: .github/workflows/release.yml ================================================ name: goreleaser on: push: tags: - v*.*.* concurrency: group: goreleaser cancel-in-progress: true jobs: goreleaser: uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main secrets: docker_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_token: ${{ secrets.DOCKERHUB_TOKEN }} gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }} goreleaser_key: ${{ secrets.GORELEASER_KEY }} twitter_consumer_key: ${{ secrets.TWITTER_CONSUMER_KEY }} twitter_consumer_secret: ${{ secrets.TWITTER_CONSUMER_SECRET }} twitter_access_token: ${{ secrets.TWITTER_ACCESS_TOKEN }} twitter_access_token_secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} mastodon_client_id: ${{ secrets.MASTODON_CLIENT_ID }} mastodon_client_secret: ${{ secrets.MASTODON_CLIENT_SECRET }} mastodon_access_token: ${{ secrets.MASTODON_ACCESS_TOKEN }} discord_webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }} discord_webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }} # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json ================================================ FILE: .gitignore ================================================ ssh_example_ed25519* /tmp **/.crush/** ================================================ FILE: .golangci.yml ================================================ version: "2" run: tests: false linters: enable: - bodyclose - exhaustive - goconst - godot - gomoddirectives - goprintffuncname - gosec - misspell - nakedret - nestif - nilerr - noctx - nolintlint - prealloc - revive - rowserrcheck - sqlclosecheck - tparallel - unconvert - unparam - whitespace - wrapcheck exclusions: rules: - text: '(slog|log)\.\w+' linters: - noctx generated: lax presets: - common-false-positives settings: exhaustive: default-signifies-exhaustive: true issues: max-issues-per-linter: 0 max-same-issues: 0 formatters: enable: - gofumpt - goimports exclusions: generated: lax ================================================ FILE: .goreleaser.yml ================================================ includes: - from_url: url: charmbracelet/meta/main/goreleaser-lib.yaml # yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021-2026 Charmbracelet, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Lip Gloss


Latest Release GoDoc Build Status

Style definitions for nice terminal layouts. Built with TUIs in mind.

![Lip Gloss example](https://github.com/user-attachments/assets/92560e60-d70e-4ce0-b39e-a60bb933356b) Lip Gloss takes an expressive, declarative approach to terminal rendering. Users familiar with CSS will feel at home with Lip Gloss. ```go import "charm.land/lipgloss/v2" var style = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#FAFAFA")). Background(lipgloss.Color("#7D56F4")). PaddingTop(2). PaddingLeft(4). Width(22) lipgloss.Println(style.Render("Hello, kitty")) ``` ## Installation ```bash go get charm.land/lipgloss/v2 ``` > [!TIP] > > Upgrading from v1? Check out the [upgrade guide](./UPGRADE_GUIDE_V2.md), or > point your LLM at it and let it go to town. ## Colors Lip Gloss supports the following color profiles: ### ANSI 16 colors (4-bit) ```go lipgloss.Color("5") // magenta lipgloss.Color("9") // red lipgloss.Color("12") // light blue ``` ### ANSI 256 Colors (8-bit) ```go lipgloss.Color("86") // aqua lipgloss.Color("201") // hot pink lipgloss.Color("202") // orange ``` ### True Color (16,777,216 colors; 24-bit) ```go lipgloss.Color("#0000FF") // good ol' 100% blue lipgloss.Color("#04B575") // a green lipgloss.Color("#3C3C3C") // a dark gray ``` ...as well as a 1-bit ASCII profile, which is black and white only. There are also named constants for the 16 standard ANSI colors: ```go lipgloss.Black lipgloss.Red lipgloss.Green lipgloss.Yellow lipgloss.Blue lipgloss.Magenta lipgloss.Cyan lipgloss.White lipgloss.BrightBlack lipgloss.BrightRed lipgloss.BrightGreen lipgloss.BrightYellow lipgloss.BrightBlue lipgloss.BrightMagenta lipgloss.BrightCyan lipgloss.BrightWhite ``` ### Automatically Downsampling Colors Some users don't have Truecolor terminals. Other times, output might not support color at all (for example, in logs). Lip Gloss was designed to handle this gracefully by automatically downsampling colors to the best available profile. If you're using Lip Gloss with Bubble Tea, there’s nothing to do. If you're using Lip Gloss standalone, just use `lipgloss.Println` or `lipgloss.Sprint` (and their variants). For more, see [advanced color usage](#advanced-color-usage). ### Color Utilities Lip Gloss ships with a handful of handy tools for working with colors: ```go c := lipgloss.Color("#EB4268") // Sriracha sauce color dark := lipgloss.Darken(c, 0.5) // dark Sriracha sauce light := lipgloss.Lighten(c, 0.35) // light Sriracha sauce green := lipgloss.Complementary(c) // greenish Sriracha sauce withAlpha := lipgloss.Alpha(c, 0.2) // watered down Sriracha sauce ``` ### Advanced Color Tooling Lip Gloss also supports color blending, automatically choosing light or dark variants of colors at runtime, and a lot more. For details, see [Advanced Color Usage](#advanced-color-usage) and [the docs][docs]. ## Inline Formatting Lip Gloss supports the usual ANSI text formatting options: ```go var style = lipgloss.NewStyle(). Bold(true). Italic(true). Faint(true). Blink(true). Strikethrough(true). Underline(true). Reverse(true) ``` ### Underline Styles Beyond simple on/off, underlines support multiple styles and custom colors: ```go s := lipgloss.NewStyle(). UnderlineStyle(lipgloss.UnderlineCurly). UnderlineColor(lipgloss.Color("#FF0000")) ``` Available styles: `UnderlineNone`, `UnderlineSingle`, `UnderlineDouble`, `UnderlineCurly`, `UnderlineDotted`, `UnderlineDashed`. ### Hyperlinks Styles can render clickable hyperlinks in supporting terminals: ```go s := lipgloss.NewStyle(). Foreground(lipgloss.Color("#7B2FBE")). Hyperlink("https://charm.land") lipgloss.Println(s.Render("Visit Charm")) ``` In unsupported terminals this will degrade gracefully and hyperlinks will simply not render. ## Block-Level Formatting Lip Gloss also supports rules for block-level formatting: ```go // Padding var style = lipgloss.NewStyle(). PaddingTop(2). PaddingRight(4). PaddingBottom(2). PaddingLeft(4) // Margins var style = lipgloss.NewStyle(). MarginTop(2). MarginRight(4). MarginBottom(2). MarginLeft(4) ``` There is also shorthand syntax for margins and padding, which follows the same format as CSS: ```go // 2 cells on all sides lipgloss.NewStyle().Padding(2) // 2 cells on the top and bottom, 4 cells on the left and right lipgloss.NewStyle().Margin(2, 4) // 1 cell on the top, 4 cells on the sides, 2 cells on the bottom lipgloss.NewStyle().Padding(1, 4, 2) // Clockwise, starting from the top: 2 cells on the top, 4 on the right, 3 on // the bottom, and 1 on the left lipgloss.NewStyle().Margin(2, 4, 3, 1) ``` You can also customize the characters used for padding and margin fill: ```go s := lipgloss.NewStyle(). Padding(1, 2). PaddingChar('·'). Margin(1, 2). MarginChar('░') ``` ## Aligning Text You can align paragraphs of text to the left, right, or center. ```go var style = lipgloss.NewStyle(). Width(24). Align(lipgloss.Left). // align it left Align(lipgloss.Right). // no wait, align it right Align(lipgloss.Center) // just kidding, align it in the center ``` ## Width and Height Setting a minimum width and height is simple and straightforward. ```go var style = lipgloss.NewStyle(). SetString("What’s for lunch?"). Width(24). Height(32). Foreground(lipgloss.Color("63")) ``` ## Borders Adding borders is easy: ```go // Add a purple, rectangular border var style = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("63")) // Set a rounded, yellow-on-purple border to the top and left var anotherStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("228")). BorderBackground(lipgloss.Color("63")). BorderTop(true). BorderLeft(true) // Make your own border var myCuteBorder = lipgloss.Border{ Top: "._.:*:", Bottom: "._.:*:", Left: "|*", Right: "|*", TopLeft: "*", TopRight: "*", BottomLeft: "*", BottomRight: "*", } ``` There are also shorthand functions for defining borders, which follow a similar pattern to the margin and padding shorthand functions. ```go // Add a thick border to the top and bottom lipgloss.NewStyle(). Border(lipgloss.ThickBorder(), true, false) // Add a double border to the top and left sides. Rules are set clockwise // from top. lipgloss.NewStyle(). Border(lipgloss.DoubleBorder(), true, false, false, true) ``` You can also pass multiple colors to a border for a gradient effect: ```go s := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForegroundBlend(lipgloss.Color("#FF0000"), lipgloss.Color("#0000FF")) ``` For more on borders see [the docs](https://pkg.go.dev/charm.land/lipgloss/v2#Border). ## Copying Styles Just use assignment: ```go style := lipgloss.NewStyle().Foreground(lipgloss.Color("219")) copiedStyle := style // this is a true copy wildStyle := style.Blink(true) // this is also true copy, with blink added ``` Since `Style` is a pure value type, assigning a style to another effectively creates a new copy of the style without mutating the original. ## Inheritance Styles can inherit rules from other styles. When inheriting, only unset rules on the receiver are inherited. ```go var styleA = lipgloss.NewStyle(). Foreground(lipgloss.Color("229")). Background(lipgloss.Color("63")) // Only the background color will be inherited here, because the foreground // color will have been already set: var styleB = lipgloss.NewStyle(). Foreground(lipgloss.Color("201")). Inherit(styleA) ``` ## Unsetting Rules All rules can be unset: ```go var style = lipgloss.NewStyle(). Bold(true). // make it bold UnsetBold(). // jk don't make it bold Background(lipgloss.Color("227")). // yellow background UnsetBackground() // never mind ``` When a rule is unset, it won’t be inherited or copied. ## Enforcing Rules Sometimes, such as when developing a component, you want to make sure style definitions respect their intended purpose in the UI. This is where `Inline` and `MaxWidth`, and `MaxHeight` come in: ```go // Force rendering onto a single line, ignoring margins, padding, and borders. someStyle.Inline(true).Render("yadda yadda") // Also limit rendering to five cells someStyle.Inline(true).MaxWidth(5).Render("yadda yadda") // Limit rendering to a 5x5 cell block someStyle.MaxWidth(5).MaxHeight(5).Render("yadda yadda") ``` ## Tabs The tab character (`\t`) is rendered differently in different terminals (often as 8 spaces, sometimes 4). Because of this inconsistency, Lip Gloss converts tabs to 4 spaces at render time. This behavior can be changed on a per-style basis, however: ```go style := lipgloss.NewStyle() // tabs will render as 4 spaces, the default style = style.TabWidth(2) // render tabs as 2 spaces style = style.TabWidth(0) // remove tabs entirely style = style.TabWidth(lipgloss.NoTabConversion) // leave tabs intact ``` ## Wrapping The `Wrap` function wraps text while preserving ANSI styles and hyperlinks across line boundaries: ```go wrapped := lipgloss.Wrap(styledText, 40, " ") ``` ## Rendering Generally, you just call the `Render(string...)` method on a `lipgloss.Style`: ```go style := lipgloss.NewStyle().Bold(true).SetString("Hello,") lipgloss.Println(style.Render("kitty.")) // Hello, kitty. lipgloss.Println(style.Render("puppy.")) // Hello, puppy. ``` But you could also use the Stringer interface: ```go var style = lipgloss.NewStyle().SetString("你好,猫咪。").Bold(true) lipgloss.Println(style) // 你好,猫咪。 ``` ## Utilities In addition to pure styling, Lip Gloss also ships with some utilities to help assemble your layouts. ### Compositing

xx

Lip Gloss includes a powerful, cell-based compositor for rendering layered content: ```go // Create some layers. a := lipgloss.NewLayer(pickles).X(4).Y(2).Z(1) b := lipgloss.NewLayer(bitterMelon).X(22).Y(1) c := lipgloss.NewLayer(sriracha).X(11).Y(7) // Composite 'em and render. output := compositor.Compose(a, b, c).Render() ``` For a more thorough example, see [the canvas example](./examples/canvas/main.go). For reference, including how to detect mouse clicks on layers, see [the docs][docs]. ### Joining Paragraphs Horizontally and vertically joining paragraphs is a cinch. ```go // Horizontally join three paragraphs along their bottom edges lipgloss.JoinHorizontal(lipgloss.Bottom, paragraphA, paragraphB, paragraphC) // Vertically join two paragraphs along their center axes lipgloss.JoinVertical(lipgloss.Center, paragraphA, paragraphB) // Horizontally join three paragraphs, with the shorter ones aligning 20% // from the top of the tallest lipgloss.JoinHorizontal(0.2, paragraphA, paragraphB, paragraphC) ``` ### Measuring Width and Height Sometimes you’ll want to know the width and height of text blocks when building your layouts. ```go // Render a block of text. var style = lipgloss.NewStyle(). Width(40). Padding(2) var block string = style.Render(someLongString) // Get the actual, physical dimensions of the text block. width := lipgloss.Width(block) height := lipgloss.Height(block) // Here's a shorthand function. w, h := lipgloss.Size(block) ``` ### Blending Colors You can blend colors in one or two dimensions for gradient effects: ```go // 1-dimentinoal gradient colors := lipgloss.Blend1D(10, lipgloss.Color("#FF0000"), lipgloss.Color("#0000FF")) // 2-dimensional gradient with rotation colors := lipgloss.Blend2D(80, 24, 45.0, color1, color2, color3) ``` ### Placing Text in Whitespace Sometimes you’ll simply want to place a block of text in whitespace. This is a lightweight alternative to compositing. ```go // Center a paragraph horizontally in a space 80 cells wide. The height of // the block returned will be as tall as the input paragraph. block := lipgloss.PlaceHorizontal(80, lipgloss.Center, fancyStyledParagraph) // Place a paragraph at the bottom of a space 30 cells tall. The width of // the text block returned will be as wide as the input paragraph. block := lipgloss.PlaceVertical(30, lipgloss.Bottom, fancyStyledParagraph) // Place a paragraph in the bottom right corner of a 30x80 cell space. block := lipgloss.Place(30, 80, lipgloss.Right, lipgloss.Bottom, fancyStyledParagraph) ``` You can also style the whitespace. For details, see [the docs][docs]. ## Rendering Tables Lip Gloss ships with a table rendering sub-package. ```go import "charm.land/lipgloss/v2/table" ``` Define some rows of data. ```go rows := [][]string{ {"Chinese", "您好", "你好"}, {"Japanese", "こんにちは", "やあ"}, {"Arabic", "أهلين", "أهلا"}, {"Russian", "Здравствуйте", "Привет"}, {"Spanish", "Hola", "¿Qué tal?"}, } ``` Use the table package to style and render the table. ```go var ( purple = lipgloss.Color("99") gray = lipgloss.Color("245") lightGray = lipgloss.Color("241") headerStyle = lipgloss.NewStyle().Foreground(purple).Bold(true).Align(lipgloss.Center) cellStyle = lipgloss.NewStyle().Padding(0, 1).Width(14) oddRowStyle = cellStyle.Foreground(gray) evenRowStyle = cellStyle.Foreground(lightGray) ) t := table.New(). Border(lipgloss.NormalBorder()). BorderStyle(lipgloss.NewStyle().Foreground(purple)). StyleFunc(func(row, col int) lipgloss.Style { switch { case row == table.HeaderRow: return headerStyle case row%2 == 0: return evenRowStyle default: return oddRowStyle } }). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...) // You can also add tables row-by-row t.Row("English", "You look absolutely fabulous.", "How's it going?") ``` Print the table. ```go lipgloss.Println(t) ``` ![Table Example](https://github.com/charmbracelet/lipgloss/assets/42545625/6e4b70c4-f494-45da-a467-bdd27df30d5d) ### Table Borders There are helpers to generate tables in markdown or ASCII style: #### Markdown Table ```go table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false) ``` ``` | LANGUAGE | FORMAL | INFORMAL | |----------|--------------|-----------| | Chinese | Nǐn hǎo | Nǐ hǎo | | French | Bonjour | Salut | | Russian | Zdravstvuyte | Privet | | Spanish | Hola | ¿Qué tal? | ``` #### ASCII Table ```go table.New().Border(lipgloss.ASCIIBorder()) ``` ``` +----------+--------------+-----------+ | LANGUAGE | FORMAL | INFORMAL | +----------+--------------+-----------+ | Chinese | Nǐn hǎo | Nǐ hǎo | | French | Bonjour | Salut | | Russian | Zdravstvuyte | Privet | | Spanish | Hola | ¿Qué tal? | +----------+--------------+-----------+ ``` For more on tables see [the docs][docs] and [examples](https://github.com/charmbracelet/lipgloss/tree/master/examples/table). ## Rendering Lists Lip Gloss ships with a list rendering sub-package. ```go import "charm.land/lipgloss/v2/list" ``` Define a new list. ```go l := list.New("A", "B", "C") ``` Print the list. ```go lipgloss.Println(l) // • A // • B // • C ``` Lists have the ability to nest. ```go l := list.New( "A", list.New("Artichoke"), "B", list.New("Baking Flour", "Bananas", "Barley", "Bean Sprouts"), "C", list.New("Cashew Apple", "Cashews", "Coconut Milk", "Curry Paste", "Currywurst"), "D", list.New("Dill", "Dragonfruit", "Dried Shrimp"), "E", list.New("Eggs"), "F", list.New("Fish Cake", "Furikake"), "J", list.New("Jicama"), "K", list.New("Kohlrabi"), "L", list.New("Leeks", "Lentils", "Licorice Root"), ) ``` Print the list. ```go lipgloss.Println(l) ```

image

Lists can be customized via their enumeration function as well as using `lipgloss.Style`s. ```go enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1) itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")).MarginRight(1) l := list.New( "Glossier", "Claire's Boutique", "Nyx", "Mac", "Milk", ). Enumerator(list.Roman). EnumeratorStyle(enumeratorStyle). ItemStyle(itemStyle) ``` Print the list.

List example

In addition to the predefined enumerators (`Arabic`, `Alphabet`, `Roman`, `Bullet`, `Tree`), you may also define your own custom enumerator: ```go l := list.New("Duck", "Duck", "Duck", "Duck", "Goose", "Duck", "Duck") func DuckDuckGooseEnumerator(l list.Items, i int) string { if l.At(i).Value() == "Goose" { return "Honk →" } return "" } l = l.Enumerator(DuckDuckGooseEnumerator) ``` Print the list:

image

If you need, you can also build lists incrementally: ```go l := list.New() for i := 0; i < repeat; i++ { l.Item("Lip Gloss") } ``` ## Rendering Trees Lip Gloss ships with a tree rendering sub-package. ```go import "charm.land/lipgloss/v2/tree" ``` Define a new tree. ```go t := tree.Root("."). Child("A", "B", "C") ``` Print the tree. ```go lipgloss.Println(t) // . // ├── A // ├── B // └── C ``` Trees have the ability to nest. ```go t := tree.Root("."). Child("macOS"). Child( tree.New(). Root("Linux"). Child("NixOS"). Child("Arch Linux (btw)"). Child("Void Linux"), ). Child( tree.New(). Root("BSD"). Child("FreeBSD"). Child("OpenBSD"), ) ``` Print the tree. ```go lipgloss.Println(t) ```

Tree Example (simple)

Trees can be customized via their enumeration function as well as using `lipgloss.Style`s. ```go enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1) rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35")) itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")) t := tree. Root("⁜ Makeup"). Child( "Glossier", "Fenty Beauty", tree.New().Child( "Gloss Bomb Universal Lip Luminizer", "Hot Cheeks Velour Blushlighter", ), "Nyx", "Mac", "Milk", ). Enumerator(tree.RoundedEnumerator). EnumeratorStyle(enumeratorStyle). RootStyle(rootStyle). ItemStyle(itemStyle) ``` Print the tree.

Tree Example (makeup)

The predefined enumerators for trees are `DefaultEnumerator` and `RoundedEnumerator`. If you need, you can also build trees incrementally: ```go t := tree.New() for i := 0; i < repeat; i++ { t.Child("Lip Gloss") } ``` ## Advanced Color Usage One of the most powerful features of Lip Gloss is the ability to render different colors at runtime depending on the user's terminal and environment, allowing you to present the best possible user experience. This section shows you how to do exactly that.
Migrating from v1? The `compat` package provides `AdaptiveColor`, `CompleteColor`, and `CompleteAdaptiveColor` for a quicker migration from v1. These work by looking at `stdin` and `stdout` on a global basis: ```go import "charm.land/lipgloss/v2/compat" color := compat.AdaptiveColor{ Light: lipgloss.Color("#f1f1f1"), Dark: lipgloss.Color("#cccccc"), } ``` Note that we don't recommend this for new code as it removes the purity from Lip Gloss, computationally speaking, as it removes transparency around when I/O happens, which could cause Lip Gloss to compete for resources (like stdin) with other tools.
### Adaptive Colors You can render different colors at runtime depending on whether the terminal has a light or dark background: ```go hasDarkBG := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) lightDark := lipgloss.LightDark(hasDarkBG) myColor := lightDark(lipgloss.Color("#D7FFAE"), lipgloss.Color("#D75FEE")) ``` #### With Bubble Tea In Bubble Tea, request the background color, listen for a `BackgroundColorMsg`, and respond accordingly: ```go func (m model) Init() tea.Cmd { // First, send a Cmd to request the terminal background color. return tea.RequestBackgroundColor } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.BackgroundColorMsg: // Great, we have the background color. Now we can set up our styles // against the color. m.styles = newStyles(msg.IsDark()) return m, nil } } func newStyles(bgIsDark bool) styles { // A little ternary function that will return the appropriate color // based on the background color. lightDark := lipgloss.LightDark(bgIsDark) return styles{ myHotStyle: lipgloss.NewStyle().Foreground(lightDark( lipgloss.Color("#f1f1f1"), lipgloss.Color("#333333"), )), } } ``` #### Standalone If you’re not using Bubble Tea you can perform the query manually: ```go // What's the background color? hasDarkBG := lipgloss.HasDarkBackground(os.Stdin, os.Stderr) // A helper function that will return the appropriate color based on the // background. lightDark := lipgloss.LightDark(hasDarkBG) // A couple colors with light and dark variants. thisColor := lightDark(lipgloss.Color("#C5ADF9"), lipgloss.Color("#864EFF")) thatColor := lightDark(lipgloss.Color("#37CD96"), lipgloss.Color("#22C78A")) a := lipgloss.NewStyle().Foreground(thisColor).Render("this") b := lipgloss.NewStyle().Foreground(thatColor).Render("that") // Render the appropriate colors at runtime: lipgloss.Fprintf(os.Stderr, "my fave colors are %s and %s", a, b) ``` ### Complete Colors In some cases where you may want to specify exact values for each color profile (ANSI 16, ANSI 156, and TrueColor). For these cases, use the `Complete` helper: ```go // You'll need the colorprofile package. import "github.com/charmbracelet/colorprofile" // Get the color profile. profile := colorprofile.Detect(os.Stdout, os.Environ()) // Create a function for rendering the appropriate color based on the profile. var completeColor := lipgloss.Complete(profile) // Now we'll choose the appropriate color at runtime. myColor := completeColor(ansiColor, ansi256Color, trueColor) ``` ### Color Downsampling One of the best things about Lip Gloss is that it can automatically downsample colors to the best available profile, stripping colors (and ANSI) entirely when output is not a TTY. If you’re using Lip Gloss with Bubble Tea there’s nothing to do here: downsampling is built into Bubble Tea v2. If you’re not using Bubble Tea, use the Lip Gloss writer functions, which are a drop-in replacement for the `fmt` package: ```go s := lipgloss.NewStyle() .Foreground(lipgloss.Color("#EB4268")) .Render("Hello!") // Downsample if needed and print to stdout. lipgloss.Println(s) // Render to a variable. downsampled := lipgloss.Sprint(s) // Print to stderr. lipgloss.Fprint(os.Stderr, s) ``` The full set: `Print`, `Println`, `Printf`, `Fprint`, `Fprintln`, `Fprintf`, `Sprint`, `Sprintln`, `Sprintf`. Need more control? Check out [Colorprofile](https://github.com/charmbracelet/colorprofile), which Lip Gloss uses under the hood. ## What about [Bubble Tea][tea]? Lip Gloss doesn’t replace Bubble Tea. Rather, it is an excellent Bubble Tea companion. It was designed to make assembling terminal user interface views as simple and fun as possible so that you can focus on building your application instead of concerning yourself with low-level layout details. In simple terms, you can use Lip Gloss to help build your Bubble Tea views. [tea]: https://github.com/charmbracelet/bubbletea ## Rendering Markdown For a more document-centric rendering solution with support for things like lists, tables, and syntax-highlighted code have a look at [Glamour][glamour], the stylesheet-based Markdown renderer. [glamour]: https://github.com/charmbracelet/glamour ## Contributing 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! - [Discord](https://charm.land/chat) - [Matrix](https://charm.land/matrix) ## 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/charm.land/lipgloss/v2?tab=doc ================================================ FILE: Taskfile.yaml ================================================ # https://taskfile.dev version: "3" tasks: lint: desc: Run base linters cmds: - golangci-lint run test: desc: Run tests cmds: - go test ./... {{.CLI_ARGS}} test:table: desc: Run table tests cmds: - go test ./table {{.CLI_ARGS}} test:tree: desc: Run tree tests cmds: - go test ./tree {{.CLI_ARGS}} ================================================ FILE: UPGRADE_GUIDE_V2.md ================================================ # Lip Gloss v2 Upgrade Guide This guide covers migrating from Lip Gloss v1 (`github.com/charmbracelet/lipgloss`) to Lip Gloss v2 (`charm.land/lipgloss/v2`). It is written for both humans and LLMs performing automated migrations. --- ## Table of Contents 1. [Quick Start](#quick-start) 2. [Module Path](#module-path) 3. [Color System](#color-system) 4. [Renderer Removal](#renderer-removal) 5. [Printing and Color Downsampling](#printing-and-color-downsampling) 6. [Background Detection and Adaptive Colors](#background-detection-and-adaptive-colors) 7. [Whitespace Options](#whitespace-options) 8. [Underline](#underline) 9. [Style API Changes](#style-api-changes) 10. [Tree Subpackage](#tree-subpackage) 11. [Removed APIs](#removed-apis) 12. [Quick Reference Table](#quick-reference-table) --- ## Quick Start For the fastest possible upgrade, do these two things: ### 1. Use the `compat` package for adaptive/complete colors ```go import "charm.land/lipgloss/v2/compat" // v1 color := lipgloss.AdaptiveColor{Light: "#f1f1f1", Dark: "#cccccc"} // v2 color := compat.AdaptiveColor{Light: lipgloss.Color("#f1f1f1"), Dark: lipgloss.Color("#cccccc")} ``` The `compat` package reads `stdin`/`stdout` globally, just like v1. To customize: ```go import ( "charm.land/lipgloss/v2/compat" "github.com/charmbracelet/colorprofile" ) func init() { compat.HasDarkBackground = lipgloss.HasDarkBackground(os.Stdin, os.Stderr) compat.Profile = colorprofile.Detect(os.Stderr, os.Environ()) } ``` ### 2. Use Lip Gloss writers for output ```go // v1 fmt.Println(s) // v2 lipgloss.Println(s) ``` This ensures colors are automatically downsampled. If you're using Bubble Tea v2, this step is unnecessary — Bubble Tea handles it for you. **That's the quick path.** Read on for the full migration details. --- ## Module Path The import path has changed. ```go // v1 import "github.com/charmbracelet/lipgloss" // v2 import "charm.land/lipgloss/v2" ``` **Install:** ```bash go get charm.land/lipgloss/v2 ``` All subpackages follow the same pattern: ```go // v1 import "github.com/charmbracelet/lipgloss/table" import "github.com/charmbracelet/lipgloss/tree" import "github.com/charmbracelet/lipgloss/list" // v2 import "charm.land/lipgloss/v2/table" import "charm.land/lipgloss/v2/tree" import "charm.land/lipgloss/v2/list" ``` **Search-and-replace pattern:** ``` github.com/charmbracelet/lipgloss → charm.land/lipgloss/v2 ``` --- ## Color System This is the most significant API change. ### `Color` is now a function, not a type ```go // v1 — Color is a string type var c lipgloss.Color = "21" var c lipgloss.Color = "#ff00ff" // v2 — Color is a function returning color.Color var c color.Color = lipgloss.Color("21") var c color.Color = lipgloss.Color("#ff00ff") ``` The return type is `image/color.Color` (from the standard library). ### `TerminalColor` interface is removed All methods that accepted `lipgloss.TerminalColor` now accept `image/color.Color`: ```go // v1 func (s Style) Foreground(c TerminalColor) Style func (s Style) Background(c TerminalColor) Style func (s Style) BorderForeground(c ...TerminalColor) Style // v2 func (s Style) Foreground(c color.Color) Style func (s Style) Background(c color.Color) Style func (s Style) BorderForeground(c ...color.Color) Style ``` **Migration:** Replace every `lipgloss.TerminalColor` with `color.Color` and add `import "image/color"`. ### `ANSIColor` is now an alias ```go // v1 — custom uint type type ANSIColor uint // v2 — alias for ansi.IndexedColor type ANSIColor = ansi.IndexedColor ``` v2 also exports named constants for the 16 basic ANSI colors: ```go lipgloss.Black, lipgloss.Red, lipgloss.Green, lipgloss.Yellow, lipgloss.Blue, lipgloss.Magenta, lipgloss.Cyan, lipgloss.White, lipgloss.BrightBlack, lipgloss.BrightRed, lipgloss.BrightGreen, lipgloss.BrightYellow, lipgloss.BrightBlue, lipgloss.BrightMagenta, lipgloss.BrightCyan, lipgloss.BrightWhite ``` ### `AdaptiveColor`, `CompleteColor`, `CompleteAdaptiveColor` These types have been moved out of the root package. Use the `compat` package for a drop-in replacement, or use the new `LightDark` and `Complete` helpers for explicit control: ```go // v1 color := lipgloss.AdaptiveColor{Light: "#0000ff", Dark: "#000099"} // v2 — using compat (quick path) color := compat.AdaptiveColor{ Light: lipgloss.Color("#0000ff"), Dark: lipgloss.Color("#000099"), } // v2 — using LightDark (recommended) hasDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) lightDark := lipgloss.LightDark(hasDark) color := lightDark(lipgloss.Color("#0000ff"), lipgloss.Color("#000099")) ``` ```go // v1 color := lipgloss.CompleteColor{TrueColor: "#ff00ff", ANSI256: "200", ANSI: "5"} // v2 — using compat color := compat.CompleteColor{ TrueColor: lipgloss.Color("#ff00ff"), ANSI256: lipgloss.Color("200"), ANSI: lipgloss.Color("5"), } // v2 — using Complete (recommended) profile := colorprofile.Detect(os.Stdout, os.Environ()) complete := lipgloss.Complete(profile) color := complete(lipgloss.Color("5"), lipgloss.Color("200"), lipgloss.Color("#ff00ff")) ``` Note that `compat.AdaptiveColor` and friends take `color.Color` values for their fields, not strings. --- ## Renderer Removal The `Renderer` type and all associated functions are removed. In v1, every `Style` carried a `*Renderer` pointer and the package maintained a global default renderer. ```go // v1 — these no longer exist lipgloss.DefaultRenderer() lipgloss.SetDefaultRenderer(r) lipgloss.NewRenderer(w, opts...) lipgloss.ColorProfile() lipgloss.SetColorProfile(p) renderer.NewStyle() ``` **In v2, `Style` is a plain value type.** There is no renderer. Color downsampling is handled at the output layer (see next section). **Migration:** - Replace `lipgloss.DefaultRenderer().NewStyle()` with `lipgloss.NewStyle()`. - Replace `renderer.NewStyle()` with `lipgloss.NewStyle()`. - Remove any `*Renderer` fields from your types. - Remove calls to `SetColorProfile` — use `colorprofile.Detect` at the output layer instead. --- ## Printing and Color Downsampling In v1, color downsampling happened inside `Style.Render()` via the renderer. In v2, `Render()` always emits full-fidelity ANSI. Downsampling happens when you print. ### Standalone Usage Use the Lip Gloss writer functions: ```go s := someStyle.Render("Hello!") // Print to stdout with automatic downsampling lipgloss.Println(s) // Print to stderr lipgloss.Fprintln(os.Stderr, s) // Render to a string (downsampled for stdout's profile) str := lipgloss.Sprint(s) ``` The default writer targets `stdout`. To customize: ```go lipgloss.Writer = colorprofile.NewWriter(os.Stderr, os.Environ()) ``` ### With Bubble Tea No changes needed. Bubble Tea v2 handles downsampling internally. --- ## Background Detection and Adaptive Colors ### Standalone v1 detected the background color automatically via the global renderer. v2 requires explicit queries: ```go // v1 hasDark := lipgloss.HasDarkBackground() // v2 — specify the input and output hasDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) ``` Then use `LightDark` to pick colors: ```go lightDark := lipgloss.LightDark(hasDark) fg := lightDark(lipgloss.Color("#333333"), lipgloss.Color("#f1f1f1")) s := lipgloss.NewStyle().Foreground(fg) ``` ### With Bubble Tea Request the background color in `Init` and listen for the response: ```go func (m model) Init() tea.Cmd { return tea.RequestBackgroundColor } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.BackgroundColorMsg: m.styles = newStyles(msg.IsDark()) } // ... } func newStyles(bgIsDark bool) styles { lightDark := lipgloss.LightDark(bgIsDark) return styles{ title: lipgloss.NewStyle().Foreground(lightDark( lipgloss.Color("#333333"), lipgloss.Color("#f1f1f1"), )), } } ``` --- ## Whitespace Options The separate foreground/background whitespace options have been replaced by a single style option: ```go // v1 lipgloss.Place(width, height, hPos, vPos, str, lipgloss.WithWhitespaceForeground(lipgloss.Color("#333")), lipgloss.WithWhitespaceBackground(lipgloss.Color("#000")), ) // v2 lipgloss.Place(width, height, hPos, vPos, str, lipgloss.WithWhitespaceStyle(lipgloss.NewStyle(). Foreground(lipgloss.Color("#333")). Background(lipgloss.Color("#000")), ), ) ``` --- ## Underline `Underline(bool)` still works for basic on/off. v2 adds fine-grained control: ```go // v1 s := lipgloss.NewStyle().Underline(true) // v2 — still works s := lipgloss.NewStyle().Underline(true) // v2 — new: specific styles s := lipgloss.NewStyle().UnderlineStyle(lipgloss.UnderlineCurly) // v2 — new: colored underlines s := lipgloss.NewStyle(). UnderlineStyle(lipgloss.UnderlineSingle). UnderlineColor(lipgloss.Color("#FF0000")) ``` Internally, `Underline(true)` is equivalent to `UnderlineStyle(UnderlineSingle)` and `Underline(false)` is equivalent to `UnderlineStyle(UnderlineNone)`. --- ## Style API Changes ### `NewStyle()` is no longer tied to a Renderer ```go // v1 s := lipgloss.NewStyle() // uses global renderer s := renderer.NewStyle() // uses specific renderer // v2 s := lipgloss.NewStyle() // pure value, no renderer ``` ### Color getters return `color.Color` ```go // v1 fg := s.GetForeground() // returns TerminalColor // v2 fg := s.GetForeground() // returns color.Color ``` ### New style methods | Method | Description | |---|---| | `UnderlineStyle(Underline)` | Set underline style (single, double, curly, etc.) | | `UnderlineColor(color.Color)` | Set underline color | | `PaddingChar(rune)` | Set the character used for padding fill | | `MarginChar(rune)` | Set the character used for margin fill | | `Hyperlink(link, params...)` | Set a clickable hyperlink | | `BorderForegroundBlend(...color.Color)` | Apply gradient colors to borders | | `BorderForegroundBlendOffset(int)` | Set the offset for border gradient | Each has a corresponding `Get*`, `Unset*`, and where applicable `Get*` accessor. --- ## Tree Subpackage The import path changes and there are new styling options: ```go // v1 import "github.com/charmbracelet/lipgloss/tree" // v2 import "charm.land/lipgloss/v2/tree" ``` New methods: - `IndenterStyle(lipgloss.Style)` — set a static style for tree indentation. - `IndenterStyleFunc(func(Children, int) lipgloss.Style)` — conditionally style indentation. - `Width(int)` — set tree width for padding. --- ## Removed APIs The following types and functions no longer exist in v2. This table shows each removed symbol and its replacement. | v1 Symbol | v2 Replacement | |---|---| | `type Renderer` | Removed entirely | | `DefaultRenderer()` | Not needed | | `SetDefaultRenderer(r)` | Not needed | | `NewRenderer(w, opts...)` | Not needed | | `ColorProfile()` | `colorprofile.Detect(w, env)` | | `SetColorProfile(p)` | Set `lipgloss.Writer.Profile` | | `HasDarkBackground()` (no args) | `lipgloss.HasDarkBackground(in, out)` | | `SetHasDarkBackground(b)` | Not needed — pass bool to `LightDark` | | `type TerminalColor` | `image/color.Color` | | `type Color string` | `func Color(string) color.Color` | | `type ANSIColor uint` | `type ANSIColor = ansi.IndexedColor` | | `type AdaptiveColor` | `compat.AdaptiveColor` or `LightDark` | | `type CompleteColor` | `compat.CompleteColor` or `Complete` | | `type CompleteAdaptiveColor` | `compat.CompleteAdaptiveColor` | | `WithWhitespaceForeground(c)` | `WithWhitespaceStyle(s)` | | `WithWhitespaceBackground(c)` | `WithWhitespaceStyle(s)` | | `renderer.NewStyle()` | `lipgloss.NewStyle()` | --- ## Quick Reference Table A side-by-side summary for common patterns: | Task | v1 | v2 | |---|---|---| | Import | `"github.com/charmbracelet/lipgloss"` | `"charm.land/lipgloss/v2"` | | Create style | `lipgloss.NewStyle()` | `lipgloss.NewStyle()` | | Hex color | `lipgloss.Color("#ff00ff")` | `lipgloss.Color("#ff00ff")` | | ANSI color | `lipgloss.Color("5")` | `lipgloss.Color("5")` or `lipgloss.Magenta` | | Adaptive color | `lipgloss.AdaptiveColor{Light: "#fff", Dark: "#000"}` | `compat.AdaptiveColor{Light: lipgloss.Color("#fff"), Dark: lipgloss.Color("#000")}` | | Set foreground | `s.Foreground(lipgloss.Color("5"))` | `s.Foreground(lipgloss.Color("5"))` | | Print with downsampling | `fmt.Println(s.Render("hi"))` | `lipgloss.Println(s.Render("hi"))` | | Detect dark bg | `lipgloss.HasDarkBackground()` | `lipgloss.HasDarkBackground(os.Stdin, os.Stdout)` | | Light/dark color | `lipgloss.AdaptiveColor{...}` | `lipgloss.LightDark(isDark)(light, dark)` | | Whitespace styling | `WithWhitespaceForeground(c)` | `WithWhitespaceStyle(lipgloss.NewStyle().Foreground(c))` | | Underline | `s.Underline(true)` | `s.Underline(true)` or `s.UnderlineStyle(lipgloss.UnderlineCurly)` | --- ## Feedback Questions, issues, or feedback: - [Discord](https://charm.land/discord) - [Matrix](https://charm.land/matrix) - [Email](mailto:vt100@charm.land) --- Part of [Charm](https://charm.land). The Charm logo Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة ================================================ FILE: align.go ================================================ package lipgloss import ( "strings" "github.com/charmbracelet/x/ansi" ) // Perform text alignment. If the string is multi-lined, we also make all lines // the same width by padding them with spaces. If a style is passed, use that // to style the spaces added. func alignTextHorizontal(str string, pos Position, width int, style *ansi.Style) string { lines, widestLine := getLines(str) var b strings.Builder for i, l := range lines { lineWidth := ansi.StringWidth(l) shortAmount := widestLine - lineWidth // difference from the widest line shortAmount += max(0, width-(shortAmount+lineWidth)) // difference from the total width, if set if shortAmount > 0 { switch pos { case Right: s := strings.Repeat(" ", shortAmount) if style != nil { s = style.Styled(s) } l = s + l case Center: // Note: remainder goes on the right. left := shortAmount / 2 //nolint:mnd right := left + shortAmount%2 //nolint:mnd leftSpaces := strings.Repeat(" ", left) rightSpaces := strings.Repeat(" ", right) if style != nil { leftSpaces = style.Styled(leftSpaces) rightSpaces = style.Styled(rightSpaces) } l = leftSpaces + l + rightSpaces default: // Left s := strings.Repeat(" ", shortAmount) if style != nil { s = style.Styled(s) } l += s } } b.WriteString(l) if i < len(lines)-1 { b.WriteRune('\n') } } return b.String() } func alignTextVertical(str string, pos Position, height int, _ *ansi.Style) string { strHeight := strings.Count(str, "\n") + 1 if height < strHeight { return str } switch pos { case Top: return str + strings.Repeat("\n", height-strHeight) case Center: topPadding, bottomPadding := (height-strHeight)/2, (height-strHeight)/2 //nolint:mnd if strHeight+topPadding+bottomPadding > height { topPadding-- } else if strHeight+topPadding+bottomPadding < height { bottomPadding++ } return strings.Repeat("\n", topPadding) + str + strings.Repeat("\n", bottomPadding) case Bottom: return strings.Repeat("\n", height-strHeight) + str } return str } ================================================ FILE: align_test.go ================================================ package lipgloss import "testing" func TestAlignTextVertical(t *testing.T) { tests := []struct { str string pos Position height int want string }{ {str: "Foo", pos: Top, height: 2, want: "Foo\n"}, {str: "Foo", pos: Center, height: 5, want: "\n\nFoo\n\n"}, {str: "Foo", pos: Bottom, height: 5, want: "\n\n\n\nFoo"}, {str: "Foo\nBar", pos: Bottom, height: 5, want: "\n\n\nFoo\nBar"}, {str: "Foo\nBar", pos: Center, height: 5, want: "\nFoo\nBar\n\n"}, {str: "Foo\nBar", pos: Top, height: 5, want: "Foo\nBar\n\n\n"}, {str: "Foo\nBar\nBaz", pos: Bottom, height: 5, want: "\n\nFoo\nBar\nBaz"}, {str: "Foo\nBar\nBaz", pos: Center, height: 5, want: "\nFoo\nBar\nBaz\n"}, {str: "Foo\nBar\nBaz", pos: Bottom, height: 3, want: "Foo\nBar\nBaz"}, {str: "Foo\nBar\nBaz", pos: Center, height: 3, want: "Foo\nBar\nBaz"}, {str: "Foo\nBar\nBaz", pos: Top, height: 3, want: "Foo\nBar\nBaz"}, {str: "Foo\n\n\n\nBar", pos: Bottom, height: 5, want: "Foo\n\n\n\nBar"}, {str: "Foo\n\n\n\nBar", pos: Center, height: 5, want: "Foo\n\n\n\nBar"}, {str: "Foo\n\n\n\nBar", pos: Top, height: 5, want: "Foo\n\n\n\nBar"}, {str: "Foo\nBar\nBaz", pos: Center, height: 9, want: "\n\n\nFoo\nBar\nBaz\n\n\n"}, {str: "Foo\nBar\nBaz", pos: Center, height: 10, want: "\n\n\nFoo\nBar\nBaz\n\n\n\n"}, } for _, test := range tests { got := alignTextVertical(test.str, test.pos, test.height, nil) if got != test.want { t.Errorf("alignTextVertical(%q, %v, %d) = %q, want %q", test.str, test.pos, test.height, got, test.want) } } } ================================================ FILE: ansi_unix.go ================================================ //go:build !windows package lipgloss import "os" // EnableLegacyWindowsANSI is only needed on Windows. func EnableLegacyWindowsANSI(*os.File) {} ================================================ FILE: ansi_windows.go ================================================ //go:build windows package lipgloss import ( "os" "golang.org/x/sys/windows" ) // EnableLegacyWindowsANSI enables support for ANSI color sequences in the // Windows default console (cmd.exe and the PowerShell application). Note that // this only works with Windows 10 and greater. Also note that Windows Terminal // supports colors by default. func EnableLegacyWindowsANSI(f *os.File) { var mode uint32 handle := windows.Handle(f.Fd()) err := windows.GetConsoleMode(handle, &mode) if err != nil { return } // See https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences if mode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING { vtpmode := mode | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING if err := windows.SetConsoleMode(handle, vtpmode); err != nil { return } } } ================================================ FILE: blending.go ================================================ package lipgloss import ( "image/color" "math" "slices" "github.com/lucasb-eyer/go-colorful" ) // Blend1D blends a series of colors together in one linear dimension using multiple // stops, into the provided number of steps. Uses the "CIE L*, a*, b*" (CIELAB) color-space. // // Note that if any of the provided colors are completely transparent, we will // assume that the alpha value was lost in conversion from RGB -> RGBA, and we // will set the alpha to opaque, as it's not possible to blend something completely // transparent. func Blend1D(steps int, stops ...color.Color) []color.Color { if steps < 0 { steps = 0 } if steps <= len(stops) { return stops[:steps] } // Ensure they didn't provide any nil colors. stops = slices.DeleteFunc(stops, func(c color.Color) bool { return c == nil }) if len(stops) == 0 { return nil // We can't safely fallback. } // If they only provided one valid color (or some nil colors), we will just return // an array of that color, for the amount of steps they requested. if len(stops) == 1 { singleColor := stops[0] result := make([]color.Color, steps) for i := range result { result[i] = singleColor } return result } blended := make([]color.Color, steps) // Convert stops to colorful.Color once cstops := make([]colorful.Color, len(stops)) for i, k := range stops { cstops[i], _ = colorful.MakeColor(ensureNotTransparent(k)) } numSegments := len(cstops) - 1 defaultSize := steps / numSegments remainingSteps := steps % numSegments resultIndex := 0 for i := range numSegments { from := cstops[i] to := cstops[i+1] // Calculate segment size. segmentSize := defaultSize if i < remainingSteps { segmentSize++ } divisor := float64(segmentSize - 1) // Generate colors for this segment. for j := 0; j < segmentSize; j++ { var blendingFactor float64 if segmentSize > 1 { blendingFactor = float64(j) / divisor } blended[resultIndex] = from.BlendLab(to, blendingFactor).Clamped() resultIndex++ } } return blended } // Blend2D blends a series of colors together in two linear dimensions using // multiple stops, into the provided width/height. Uses the "CIE L*, a*, b*" (CIELAB) // color-space. The angle parameter controls the rotation of the gradient (0-360°), // where 0° is left-to-right, 45° is bottom-left to top-right (diagonal). The function // returns colors in a 1D row-major order ([row1, row2, row3, ...]). // // Example of how to iterate over the result: // // gradient := colors.Blend2D(width, height, 180, color1, color2, color3, ...) // gradientContent := strings.Builder{} // for y := range height { // for x := range width { // index := y*width + x // gradientContent.WriteString( // lipgloss.NewStyle(). // Background(gradient[index]). // Render(" "), // ) // } // if y < height-1 { // End of row. // gradientContent.WriteString("\n") // } // } // // Note that if any of the provided colors are completely transparent, we will // assume that the alpha value was lost in conversion from RGB -> RGBA, and we // will set the alpha to opaque, as it's not possible to blend something completely // transparent. func Blend2D(width, height int, angle float64, stops ...color.Color) []color.Color { if width < 1 { width = 1 } if height < 1 { height = 1 } // Normalize angle to 0-360. angle = math.Mod(angle, 360) if angle < 0 { angle += 360 } // Ensure they didn't provide any nil colors. stops = slices.DeleteFunc(stops, func(c color.Color) bool { return c == nil }) if len(stops) == 0 { return nil // We can't safely fallback. } // If they only provided one valid color (or some nil colors), we will just return // an array of that color, for the amount of pixels they requested. if len(stops) == 1 { singleColor := stops[0] result := make([]color.Color, width*height) for i := range result { result[i] = singleColor } return result } // For 2D blending, we'll create a gradient along the diagonal and then sample // from it based on the angle. We'll use the maximum dimension to ensure we have // enough resolution for the gradient. diagonalGradient := Blend1D(max(width, height), stops...) result := make([]color.Color, width*height) // Calculate center point for rotation. centerX := float64(width-1) / 2.0 centerY := float64(height-1) / 2.0 angleRad := angle * math.Pi / 180.0 // -> radians. // Pre-calculate sin and cos. cosAngle := math.Cos(angleRad) sinAngle := math.Sin(angleRad) // Calculate diagonal length for proper gradient mapping. diagonalLength := math.Sqrt(float64(width*width + height*height)) // Pre-calculate gradient length for index calculation. gradientLen := float64(len(diagonalGradient) - 1) for y := range height { // Calculate the distance from center along the gradient direction. dy := float64(y) - centerY for x := 0; x < width; x++ { // Calculate the distance from center along the gradient direction. dx := float64(x) - centerX rotX := dx*cosAngle - dy*sinAngle // Rotate the point by the angle. // Map the rotated position to the gradient. Normalize to 0-1 range based on // the diagonal length. gradientPos := clamp((rotX+diagonalLength/2.0)/diagonalLength, 0, 1) // Calculate the index in the gradient. gradientIndex := int(gradientPos * gradientLen) if gradientIndex >= len(diagonalGradient) { gradientIndex = len(diagonalGradient) - 1 } result[y*width+x] = diagonalGradient[gradientIndex] // -> row-major order. } } return result } ================================================ FILE: blending_test.go ================================================ package lipgloss import ( "image/color" "testing" ) func TestBlend1D(t *testing.T) { tests := []struct { name string steps int stops []color.Color expected []color.Color }{ { name: "2-colors-10-steps", steps: 10, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, expected: []color.Color{ &color.RGBA{R: 255, G: 0, B: 0, A: 255}, &color.RGBA{R: 246, G: 0, B: 45, A: 255}, &color.RGBA{R: 235, G: 0, B: 73, A: 255}, &color.RGBA{R: 223, G: 0, B: 99, A: 255}, &color.RGBA{R: 210, G: 0, B: 124, A: 255}, &color.RGBA{R: 193, G: 0, B: 149, A: 255}, &color.RGBA{R: 173, G: 0, B: 175, A: 255}, &color.RGBA{R: 147, G: 0, B: 201, A: 255}, &color.RGBA{R: 109, G: 0, B: 228, A: 255}, &color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, }, { name: "3-colors-4-steps", steps: 4, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 255, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, expected: []color.Color{ &color.RGBA{R: 255, G: 0, B: 0, A: 255}, &color.RGBA{R: 0, G: 255, B: 0, A: 255}, &color.RGBA{R: 0, G: 255, B: 0, A: 255}, &color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, }, { name: "black-to-white-5-steps", steps: 5, stops: []color.Color{ color.RGBA{R: 0, G: 0, B: 0, A: 255}, color.RGBA{R: 255, G: 255, B: 255, A: 255}, }, expected: []color.Color{ &color.RGBA{R: 0, G: 0, B: 0, A: 255}, &color.RGBA{R: 59, G: 59, B: 59, A: 255}, &color.RGBA{R: 119, G: 119, B: 119, A: 255}, &color.RGBA{R: 185, G: 185, B: 185, A: 255}, &color.RGBA{R: 255, G: 255, B: 255, A: 255}, }, }, { name: "4-colors-6-steps", steps: 6, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 255, G: 255, B: 0, A: 255}, color.RGBA{R: 0, G: 255, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, expected: []color.Color{ &color.RGBA{R: 255, G: 0, B: 0, A: 255}, &color.RGBA{R: 255, G: 255, B: 0, A: 255}, &color.RGBA{R: 255, G: 255, B: 0, A: 255}, &color.RGBA{R: 0, G: 255, B: 0, A: 255}, &color.RGBA{R: 0, G: 255, B: 0, A: 255}, &color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, }, { name: "2-steps-5-stops", steps: 2, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 255, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, color.RGBA{R: 255, G: 255, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 0, A: 255}, }, expected: []color.Color{ &color.RGBA{R: 255, G: 0, B: 0, A: 255}, &color.RGBA{R: 0, G: 255, B: 0, A: 255}, }, }, { name: "3-steps-1-stop", steps: 3, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, }, expected: []color.Color{ &color.RGBA{R: 255, G: 0, B: 0, A: 255}, &color.RGBA{R: 255, G: 0, B: 0, A: 255}, &color.RGBA{R: 255, G: 0, B: 0, A: 255}, }, }, { name: "1-step-2-stops", steps: 1, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, expected: []color.Color{ &color.RGBA{R: 255, G: 0, B: 0, A: 255}, }, }, { name: "0-steps-0-stops", steps: 0, stops: []color.Color{}, expected: []color.Color{}, }, { name: "0-steps-1-stop", steps: 0, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, }, expected: []color.Color{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() got := Blend1D(tt.steps, tt.stops...) if len(got) != len(tt.expected) { t.Errorf("Blend() = %v length, want %v length", len(got), len(tt.expected)) } for i := range tt.expected { expectColorMatches(t, got[i], tt.expected[i]) } }) } } func TestBlend2D(t *testing.T) { tests := []struct { name string width, height int angle float64 stops []color.Color expectedLength int }{ { name: "2x2-red-to-blue-0deg", width: 2, height: 2, angle: 0, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, expectedLength: 4, }, { name: "3x2-red-to-blue-90deg", width: 3, height: 2, angle: 90, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, expectedLength: 6, }, { name: "2x3-red-to-blue-180deg", width: 2, height: 3, angle: 180, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, expectedLength: 6, }, { name: "2x2-red-to-blue-270deg", width: 2, height: 2, angle: 270, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, expectedLength: 4, }, { name: "1x1-single-color", width: 1, height: 1, angle: 0, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, }, expectedLength: 1, }, { name: "3-colors-2x2-0deg", width: 2, height: 2, angle: 0, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 255, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, expectedLength: 4, }, { name: "invalid-dimensions-fallback", width: 0, height: -1, angle: 0, stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, }, expectedLength: 1, }, { name: "angle-normalization-450", width: 2, height: 2, angle: 450, // Should normalize to 90 stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, expectedLength: 4, }, { name: "negative-angle-normalization", width: 2, height: 2, angle: -90, // Should normalize to 270 stops: []color.Color{ color.RGBA{R: 255, G: 0, B: 0, A: 255}, color.RGBA{R: 0, G: 0, B: 255, A: 255}, }, expectedLength: 4, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() got := Blend2D(tt.width, tt.height, tt.angle, tt.stops...) if len(got) != tt.expectedLength { t.Errorf("Blend2D() = %v length, want %v length", len(got), tt.expectedLength) } // Verify row-major order by checking that the width matches. if tt.width > 0 && tt.height > 0 { expectedTotal := max(tt.width, 1) * max(tt.height, 1) if len(got) != expectedTotal { t.Errorf("Blend2D() total pixels = %v, want %v", len(got), expectedTotal) } } // Verify that we have valid colors (not nil). for i, color := range got { if color == nil { t.Errorf("Blend2D() color at index %d is nil", i) } } // For single color tests, verify all colors are the same. if len(tt.stops) == 1 && len(got) > 0 { firstColor := got[0] for _, color := range got { expectColorMatches(t, color, firstColor) } } }) } } func TestBlend2DEdgeCases(t *testing.T) { t.Run("nil-stops", func(t *testing.T) { t.Parallel() got := Blend2D(2, 2, 0, nil, nil) if got != nil { t.Errorf("Blend2D() with nil stops = %v, want nil", got) } }) t.Run("empty-stops", func(t *testing.T) { t.Parallel() got := Blend2D(2, 2, 0) if got != nil { t.Errorf("Blend2D() with empty stops = %v, want nil", got) } }) t.Run("nil-color-in-stops", func(t *testing.T) { t.Parallel() got := Blend2D(2, 2, 0, color.RGBA{R: 255, G: 0, B: 0, A: 255}, nil, color.RGBA{R: 0, G: 0, B: 255, A: 255}) if len(got) != 4 { t.Errorf("Blend2D() with nil color in stops = %v length, want 4", len(got)) } // Should still work with the non-nil colors and produce valid colors for i, color := range got { if color == nil { t.Errorf("Blend2D() color at index %d is nil", i) } } }) } func BenchmarkBlend1D(b *testing.B) { stops := []color.Color{ hex("#FF0000"), // Red hex("#00FF00"), // Green hex("#0000FF"), // Blue hex("#FFFF00"), // Yellow hex("#FF00FF"), // Magenta } for b.Loop() { Blend1D(100, stops...) } } func BenchmarkBlend2D(b *testing.B) { stops := []color.Color{ hex("#FF0000"), // Red hex("#00FF00"), // Green hex("#0000FF"), // Blue hex("#FFFF00"), // Yellow hex("#FF00FF"), // Magenta } for b.Loop() { Blend2D(100, 50, 45, stops...) } } ================================================ FILE: borders.go ================================================ package lipgloss import ( "image/color" "slices" "strings" "unicode/utf8" "github.com/charmbracelet/x/ansi" "github.com/clipperhouse/displaywidth" "github.com/rivo/uniseg" ) // Border contains a series of values which comprise the various parts of a // border. type Border struct { Top string Bottom string Left string Right string TopLeft string TopRight string BottomLeft string BottomRight string MiddleLeft string MiddleRight string Middle string MiddleTop string MiddleBottom string } // GetTopSize returns the width of the top border. If borders contain runes of // varying widths, the widest rune is returned. If no border exists on the top // edge, 0 is returned. func (b Border) GetTopSize() int { return getBorderEdgeWidth(b.TopLeft, b.Top, b.TopRight) } // GetRightSize returns the width of the right border. If borders contain // runes of varying widths, the widest rune is returned. If no border exists on // the right edge, 0 is returned. func (b Border) GetRightSize() int { return getBorderEdgeWidth(b.TopRight, b.Right, b.BottomRight) } // GetBottomSize returns the width of the bottom border. If borders contain // runes of varying widths, the widest rune is returned. If no border exists on // the bottom edge, 0 is returned. func (b Border) GetBottomSize() int { return getBorderEdgeWidth(b.BottomLeft, b.Bottom, b.BottomRight) } // GetLeftSize returns the width of the left border. If borders contain runes // of varying widths, the widest rune is returned. If no border exists on the // left edge, 0 is returned. func (b Border) GetLeftSize() int { return getBorderEdgeWidth(b.TopLeft, b.Left, b.BottomLeft) } func getBorderEdgeWidth(borderParts ...string) (maxWidth int) { for _, piece := range borderParts { maxWidth = max(maxWidth, maxRuneWidth(piece)) } return maxWidth } var ( noBorder = Border{} normalBorder = Border{ Top: "─", Bottom: "─", Left: "│", Right: "│", TopLeft: "┌", TopRight: "┐", BottomLeft: "└", BottomRight: "┘", MiddleLeft: "├", MiddleRight: "┤", Middle: "┼", MiddleTop: "┬", MiddleBottom: "┴", } roundedBorder = Border{ Top: "─", Bottom: "─", Left: "│", Right: "│", TopLeft: "╭", TopRight: "╮", BottomLeft: "╰", BottomRight: "╯", MiddleLeft: "├", MiddleRight: "┤", Middle: "┼", MiddleTop: "┬", MiddleBottom: "┴", } blockBorder = Border{ Top: "█", Bottom: "█", Left: "█", Right: "█", TopLeft: "█", TopRight: "█", BottomLeft: "█", BottomRight: "█", MiddleLeft: "█", MiddleRight: "█", Middle: "█", MiddleTop: "█", MiddleBottom: "█", } outerHalfBlockBorder = Border{ Top: "▀", Bottom: "▄", Left: "▌", Right: "▐", TopLeft: "▛", TopRight: "▜", BottomLeft: "▙", BottomRight: "▟", } innerHalfBlockBorder = Border{ Top: "▄", Bottom: "▀", Left: "▐", Right: "▌", TopLeft: "▗", TopRight: "▖", BottomLeft: "▝", BottomRight: "▘", } thickBorder = Border{ Top: "━", Bottom: "━", Left: "┃", Right: "┃", TopLeft: "┏", TopRight: "┓", BottomLeft: "┗", BottomRight: "┛", MiddleLeft: "┣", MiddleRight: "┫", Middle: "╋", MiddleTop: "┳", MiddleBottom: "┻", } doubleBorder = Border{ Top: "═", Bottom: "═", Left: "║", Right: "║", TopLeft: "╔", TopRight: "╗", BottomLeft: "╚", BottomRight: "╝", MiddleLeft: "╠", MiddleRight: "╣", Middle: "╬", MiddleTop: "╦", MiddleBottom: "╩", } hiddenBorder = Border{ Top: " ", Bottom: " ", Left: " ", Right: " ", TopLeft: " ", TopRight: " ", BottomLeft: " ", BottomRight: " ", MiddleLeft: " ", MiddleRight: " ", Middle: " ", MiddleTop: " ", MiddleBottom: " ", } markdownBorder = Border{ Top: "-", Bottom: "-", Left: "|", Right: "|", TopLeft: "|", TopRight: "|", BottomLeft: "|", BottomRight: "|", MiddleLeft: "|", MiddleRight: "|", Middle: "|", MiddleTop: "|", MiddleBottom: "|", } asciiBorder = Border{ Top: "-", Bottom: "-", Left: "|", Right: "|", TopLeft: "+", TopRight: "+", BottomLeft: "+", BottomRight: "+", MiddleLeft: "+", MiddleRight: "+", Middle: "+", MiddleTop: "+", MiddleBottom: "+", } ) // NormalBorder returns a standard-type border with a normal weight and 90 // degree corners. func NormalBorder() Border { return normalBorder } // RoundedBorder returns a border with rounded corners. func RoundedBorder() Border { return roundedBorder } // BlockBorder returns a border that takes the whole block. func BlockBorder() Border { return blockBorder } // OuterHalfBlockBorder returns a half-block border that sits outside the frame. func OuterHalfBlockBorder() Border { return outerHalfBlockBorder } // InnerHalfBlockBorder returns a half-block border that sits inside the frame. func InnerHalfBlockBorder() Border { return innerHalfBlockBorder } // ThickBorder returns a border that's thicker than the one returned by // NormalBorder. func ThickBorder() Border { return thickBorder } // DoubleBorder returns a border comprised of two thin strokes. func DoubleBorder() 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 // color to a hidden border. func HiddenBorder() 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 // ensure that the output is valid markdown. // // table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false) func MarkdownBorder() Border { return markdownBorder } // ASCIIBorder returns a table border with ASCII characters. func ASCIIBorder() Border { return asciiBorder } type borderBlend struct { topGradient []color.Color rightGradient []color.Color bottomGradient []color.Color leftGradient []color.Color } func (s Style) borderBlend(width, height int, colors ...color.Color) *borderBlend { gradient := Blend1D( (height+width+2)*2, colors..., ) // Rotate array forward or reverse based on the offset if provided. if r := -s.getAsInt(borderForegroundBlendOffsetKey); r != 0 { n := len(gradient) r %= n if r < 0 { r += n } slices.Reverse(gradient[:r]) slices.Reverse(gradient[r:]) slices.Reverse(gradient) } offset := 0 getFromOffset := func(size int) (s []color.Color) { s = gradient[offset : offset+size] offset += size return s } blend := &borderBlend{ topGradient: getFromOffset(width + 2), rightGradient: getFromOffset(height), bottomGradient: getFromOffset(width + 2), leftGradient: getFromOffset(height), } // bottom and left gradients are reversed because they are drawn in reverse order. slices.Reverse(blend.bottomGradient) slices.Reverse(blend.leftGradient) return blend } func (s Style) applyBorder(str string) string { var ( border = s.getBorderStyle() hasTop = s.getAsBool(borderTopKey, false) hasRight = s.getAsBool(borderRightKey, false) hasBottom = s.getAsBool(borderBottomKey, false) hasLeft = s.getAsBool(borderLeftKey, false) ) // If a border is set and no sides have been specifically turned on or off // render borders on all sides. if s.isBorderStyleSetWithoutSides() { hasTop = true hasRight = true hasBottom = true hasLeft = true } // If no border is set or all borders are been disabled, abort. if border == noBorder || (!hasTop && !hasRight && !hasBottom && !hasLeft) { return str } lines, width := getLines(str) if hasLeft { if border.Left == "" { border.Left = " " } width += maxRuneWidth(border.Left) } if hasRight { if border.Right == "" { border.Right = " " } width += maxRuneWidth(border.Right) } // If corners should be rendered but are set with the empty string, fill them // with a single space. if hasTop && hasLeft && border.TopLeft == "" { border.TopLeft = " " } if hasTop && hasRight && border.TopRight == "" { border.TopRight = " " } if hasBottom && hasLeft && border.BottomLeft == "" { border.BottomLeft = " " } if hasBottom && hasRight && border.BottomRight == "" { border.BottomRight = " " } // Figure out which corners we should actually be using based on which // sides are set to show. if hasTop { switch { case !hasLeft && !hasRight: border.TopLeft = "" border.TopRight = "" case !hasLeft: border.TopLeft = "" case !hasRight: border.TopRight = "" } } if hasBottom { switch { case !hasLeft && !hasRight: border.BottomLeft = "" border.BottomRight = "" case !hasLeft: border.BottomLeft = "" case !hasRight: border.BottomRight = "" } } // For now, limit corners to one rune. border.TopLeft = getFirstRuneAsString(border.TopLeft) border.TopRight = getFirstRuneAsString(border.TopRight) border.BottomRight = getFirstRuneAsString(border.BottomRight) border.BottomLeft = getFirstRuneAsString(border.BottomLeft) var topFG, rightFG, bottomFG, leftFG color.Color var ( blendFG = s.getAsColors(borderForegroundBlendKey) topBG = s.getAsColor(borderTopBackgroundKey) rightBG = s.getAsColor(borderRightBackgroundKey) bottomBG = s.getAsColor(borderBottomBackgroundKey) leftBG = s.getAsColor(borderLeftBackgroundKey) ) var blend *borderBlend if len(blendFG) > 0 { blend = s.borderBlend(width, len(lines), blendFG...) } else { topFG = s.getAsColor(borderTopForegroundKey) rightFG = s.getAsColor(borderRightForegroundKey) bottomFG = s.getAsColor(borderBottomForegroundKey) leftFG = s.getAsColor(borderLeftForegroundKey) } var out strings.Builder // Render top if hasTop { top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width) if blend != nil { out.WriteString(s.styleBorderBlend(top, blend.topGradient, topBG)) } else { out.WriteString(s.styleBorder(top, topFG, topBG)) } out.WriteRune('\n') } leftRunes := []rune(border.Left) leftIndex := 0 rightRunes := []rune(border.Right) rightIndex := 0 // Render sides var r string for i, l := range lines { if hasLeft { r = string(leftRunes[leftIndex]) leftIndex++ if leftIndex >= len(leftRunes) { leftIndex = 0 } if blend != nil { out.WriteString(s.styleBorder(r, blend.leftGradient[i], leftBG)) } else { out.WriteString(s.styleBorder(r, leftFG, leftBG)) } } out.WriteString(l) if hasRight { r = string(rightRunes[rightIndex]) rightIndex++ if rightIndex >= len(rightRunes) { rightIndex = 0 } if blend != nil { out.WriteString(s.styleBorder(r, blend.rightGradient[i], rightBG)) } else { out.WriteString(s.styleBorder(r, rightFG, rightBG)) } } if i < len(lines)-1 { out.WriteRune('\n') } } // Render bottom if hasBottom { bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width) out.WriteRune('\n') if blend != nil { out.WriteString(s.styleBorderBlend(bottom, blend.bottomGradient, bottomBG)) } else { out.WriteString(s.styleBorder(bottom, bottomFG, bottomBG)) } } return out.String() } // Render the horizontal (top or bottom) portion of a border. func renderHorizontalEdge(left, middle, right string, width int) string { if middle == "" { middle = " " } leftWidth := ansi.StringWidth(left) rightWidth := ansi.StringWidth(right) runes := []rune(middle) j := 0 out := strings.Builder{} out.WriteString(left) for i := 0; i < width-leftWidth-rightWidth; { r := runes[j] out.WriteRune(r) i += ansi.StringWidth(string(r)) j++ if j >= len(runes) { j = 0 } } out.WriteString(right) return out.String() } // styleBorder applies foreground and background styling to a border. func (s Style) styleBorder(border string, fg, bg color.Color) string { if fg == noColor && bg == noColor { return border } var style ansi.Style if fg != noColor { style = style.ForegroundColor(fg) } if bg != noColor { style = style.BackgroundColor(bg) } return style.Styled(border) } // styleBorderBlend applies foreground and background styling to a border, using blending. func (s Style) styleBorderBlend(border string, fg []color.Color, bg color.Color) string { var out strings.Builder var style ansi.Style var i int gr := uniseg.NewGraphemes(border) for gr.Next() { style = style[:0] if fg[i] != noColor { style = style.ForegroundColor(fg[i]) } if bg != noColor { style = style.BackgroundColor(bg) } _, _ = out.WriteString(style.String()) _, _ = out.Write(gr.Bytes()) i++ } _, _ = out.WriteString(ansi.ResetStyle) return out.String() } func maxRuneWidth(str string) int { switch len(str) { case 0: return 0 case 1: return displaywidth.String(str) } var width int g := displaywidth.StringGraphemes(str) for g.Next() { width = max(width, g.Width()) } return width } func getFirstRuneAsString(str string) string { if str == "" { return str } _, size := utf8.DecodeRuneInString(str) return str[:size] } ================================================ FILE: borders_test.go ================================================ package lipgloss import ( "testing" "github.com/rivo/uniseg" ) func BenchmarkBorderRendering(b *testing.B) { dimensions := []struct { name string width int height int }{ {"10x5", 10, 5}, {"20x10", 20, 10}, {"40x20", 40, 15}, {"80x40", 80, 20}, {"120x60", 120, 25}, {"160x80", 160, 30}, } for _, dim := range dimensions { b.Run(dim.name, func(b *testing.B) { style := NewStyle(). Border(RoundedBorder(), true). Foreground(Color("#ffffff")). Background(Color("#000000")). Width(dim.width). Height(dim.height) b.ResetTimer() for b.Loop() { _ = style.Render("") } }) } } func BenchmarkBorderBlend(b *testing.B) { dimensions := []struct { name string width int height int }{ {"10x5", 10, 5}, {"20x10", 20, 10}, {"40x20", 40, 15}, {"80x40", 80, 20}, {"120x60", 120, 25}, {"160x80", 160, 30}, } for _, dim := range dimensions { b.Run(dim.name, func(b *testing.B) { style := NewStyle(). Border(RoundedBorder(), true). BorderForegroundBlend( Color("#00FA68"), Color("#9900FF"), Color("#ED5353"), ). Width(dim.width). Height(dim.height) b.ResetTimer() for b.Loop() { _ = style.Render("") } }) } } func BenchmarkBorderRenderingNoColors(b *testing.B) { dimensions := []struct { name string width int height int }{ {"10x5", 10, 5}, {"20x10", 20, 10}, {"40x20", 40, 15}, {"80x40", 80, 20}, {"120x60", 120, 25}, {"160x80", 160, 30}, } for _, dim := range dimensions { b.Run(dim.name, func(b *testing.B) { style := NewStyle(). Border(RoundedBorder(), true). Width(dim.width). Height(dim.height) b.ResetTimer() for b.Loop() { _ = style.Render("") } }) } } // Old implementation using rune slice conversion func getFirstRuneAsStringOld(str string) string { if str == "" { return str } r := []rune(str) return string(r[0]) } func TestGetFirstRuneAsString(t *testing.T) { tests := []struct { name string input string want string }{ {"Empty", "", ""}, {"SingleASCII", "A", "A"}, {"SingleUnicode", "世", "世"}, {"ASCIIString", "Hello", "H"}, {"UnicodeString", "你好世界", "你"}, {"MixedASCIIFirst", "Hello世界", "H"}, {"MixedUnicodeFirst", "世界Hello", "世"}, {"Emoji", "😀Happy", "😀"}, {"MultiByteFirst", "ñoño", "ñ"}, {"LongString", "The quick brown fox jumps over the lazy dog", "T"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := getFirstRuneAsString(tt.input) if got != tt.want { t.Errorf("getFirstRuneAsString(%q) = %q, want %q", tt.input, got, tt.want) } // Verify new implementation matches old implementation old := getFirstRuneAsStringOld(tt.input) if got != old { t.Errorf("getFirstRuneAsString(%q) = %q, but old implementation returns %q", tt.input, got, old) } }) } } func BenchmarkGetFirstRuneAsString(b *testing.B) { testCases := []struct { name string str string }{ {"ASCII", "Hello, World!"}, {"Unicode", "你好世界"}, {"Single", "A"}, {"Empty", ""}, } b.Run("Old", func(b *testing.B) { for _, tc := range testCases { b.Run(tc.name, func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { _ = getFirstRuneAsStringOld(tc.str) } }) } }) b.Run("New", func(b *testing.B) { for _, tc := range testCases { b.Run(tc.name, func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { _ = getFirstRuneAsString(tc.str) } }) } }) } func BenchmarkMaxRuneWidth(b *testing.B) { testCases := []struct { name string str string }{ {"Blank", " "}, {"ASCII", "+"}, {"Markdown", "|"}, {"Normal", "├"}, {"Rounded", "╭"}, {"Block", "█"}, {"Emoji", "😀"}, } for _, tc := range testCases { b.Run(tc.name, func(b *testing.B) { b.Run("Before", func(b *testing.B) { b.ReportAllocs() for b.Loop() { _ = maxRuneWidthOld(tc.str) } }) b.Run("After", func(b *testing.B) { b.ReportAllocs() for b.Loop() { _ = maxRuneWidth(tc.str) } }) }) } } func maxRuneWidthOld(str string) int { var width int state := -1 for len(str) > 0 { var w int _, str, w, state = uniseg.FirstGraphemeClusterInString(str, state) if w > width { width = w } } return width } ================================================ FILE: canvas.go ================================================ package lipgloss import ( uv "github.com/charmbracelet/ultraviolet" "github.com/charmbracelet/x/ansi" ) // Canvas is a cell-buffer that can be used to compose and draw [uv.Drawable]s // like [Layer]s. // // Composed drawables are drawn onto the canvas in the order they were // composed, meaning later drawables will appear "on top" of earlier ones. // // A canvas can read, modify, and render its cell contents. // // It implements [uv.Screen] and [uv.Drawable]. type Canvas struct { scr uv.ScreenBuffer } var _ uv.Screen = (*Canvas)(nil) // NewCanvas creates a new [Canvas] with the given size. func NewCanvas(width, height int) *Canvas { c := new(Canvas) c.scr = uv.NewScreenBuffer(width, height) c.scr.Method = ansi.GraphemeWidth return c } // Resize resizes the canvas to the given width and height. func (c *Canvas) Resize(width, height int) { c.scr.Resize(width, height) } // Clear clears the canvas. func (c *Canvas) Clear() { c.scr.Clear() } // Bounds implements [uv.Screen]. func (c *Canvas) Bounds() uv.Rectangle { return c.scr.Bounds() } // Width returns the width of the canvas. func (c *Canvas) Width() int { return c.scr.Width() } // Height returns the height of the canvas. func (c *Canvas) Height() int { return c.scr.Height() } // CellAt implements [uv.Screen]. func (c *Canvas) CellAt(x int, y int) *uv.Cell { return c.scr.CellAt(x, y) } // SetCell implements [uv.Screen]. func (c *Canvas) SetCell(x int, y int, cell *uv.Cell) { c.scr.SetCell(x, y, cell) } // WidthMethod implements [uv.Screen]. func (c *Canvas) WidthMethod() uv.WidthMethod { return c.scr.WidthMethod() } // Compose composes a [Layer] or any [uv.Drawable] onto the [Canvas]. func (c *Canvas) Compose(drawer uv.Drawable) *Canvas { drawer.Draw(c, c.Bounds()) return c } // Draw draws the [Canvas] onto the given [uv.Screen] within the specified // area. // // It implements [uv.Drawable]. func (c *Canvas) Draw(scr uv.Screen, area uv.Rectangle) { c.scr.Draw(scr, area) } // Render renders the canvas into a styled string. func (c *Canvas) Render() string { return c.scr.Render() } ================================================ FILE: canvas_test.go ================================================ package lipgloss import ( "strings" "testing" ) func TestCanvasRender(t *testing.T) { c := NewCanvas(5, 3) // Fill the canvas with dots for y := 0; y < c.Height(); y++ { for x := 0; x < c.Width(); x++ { cell := c.CellAt(x, y) cell.Content = "." } } // Draw a rectangle for y := 1; y < 2; y++ { for x := 1; x < 4; x++ { cell := c.CellAt(x, y) cell.Content = "#" } } expected := strings.Join([]string{ ".....", ".###.", ".....", }, "\n") if rendered := c.Render(); rendered != expected { t.Errorf("expected:\n%q\ngot:\n%q", expected, rendered) } } func TestCanvasRenderWithTrailingSpaces(t *testing.T) { c := NewCanvas(5, 2) // Fill the canvas with spaces and some trailing spaces for y := 0; y < c.Height(); y++ { for x := 0; x < c.Width(); x++ { cell := c.CellAt(x, y) if x < 3 { cell.Content = "A" } else { cell.Content = " " } } } expected := strings.Join([]string{ "AAA", "AAA", }, "\n") if rendered := c.Render(); rendered != expected { t.Errorf("expected:\n%q\ngot:\n%q", expected, rendered) } } ================================================ FILE: color.go ================================================ package lipgloss import ( "cmp" "errors" "image/color" "strconv" "strings" "github.com/charmbracelet/colorprofile" "github.com/charmbracelet/x/ansi" "github.com/lucasb-eyer/go-colorful" ) func clamp[T cmp.Ordered](v, low, high T) T { if high < low { high, low = low, high } return min(high, max(low, v)) } // 4-bit color constants. const ( Black ansi.BasicColor = iota Red Green Yellow Blue Magenta Cyan White BrightBlack BrightRed BrightGreen BrightYellow BrightBlue BrightMagenta BrightCyan BrightWhite ) var noColor = NoColor{} // NoColor is used to specify the absence of color styling. When this is active // foreground colors will be rendered with the terminal's default text color, // and background colors will not be drawn at all. // // Example usage: // // var style = someStyle.Background(lipgloss.NoColor{}) type NoColor struct{} // RGBA returns the RGBA value of this color. Because we have to return // something, despite this color being the absence of color, we're returning // black with 100% opacity. // // Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. func (n NoColor) RGBA() (r, g, b, a uint32) { return 0x0, 0x0, 0x0, 0xFFFF //nolint:mnd } // Color specifies a color by hex or ANSI256 value. For example: // // ansiColor := lipgloss.Color("1") // The same as lipgloss.Red // ansi256Color := lipgloss.Color("21") // hexColor := lipgloss.Color("#0000ff") func Color(s string) color.Color { if strings.HasPrefix(s, "#") { c, err := parseHex(s) if err != nil { return noColor } return c } i, err := strconv.Atoi(s) if err != nil { return noColor } if i < 0 { // Only positive numbers i = -i } if i < 16 { return ansi.BasicColor(i) //nolint:gosec } else if i < 256 { return ANSIColor(i) //nolint:gosec } r, g, b := uint8((i>>16)&0xff), uint8(i>>8&0xff), uint8(i&0xff) //nolint:gosec return color.RGBA{R: r, G: g, B: b, A: 0xff} } var errInvalidFormat = errors.New("invalid hex format") // pre-allocated. // parseHex parses a hex color string and returns a color.RGBA. The string can be // in the format #RRGGBB or #RGB. This is a more performant implementation of // [colorful.Hex]. func parseHex(s string) (c color.RGBA, err error) { c.A = 0xff if len(s) == 0 || s[0] != '#' { return c, errInvalidFormat } hexToByte := func(b byte) byte { switch { case b >= '0' && b <= '9': return b - '0' case b >= 'a' && b <= 'f': return b - 'a' + 10 case b >= 'A' && b <= 'F': return b - 'A' + 10 } err = errInvalidFormat return 0 } switch len(s) { case 7: c.R = hexToByte(s[1])<<4 + hexToByte(s[2]) c.G = hexToByte(s[3])<<4 + hexToByte(s[4]) c.B = hexToByte(s[5])<<4 + hexToByte(s[6]) case 4: c.R = hexToByte(s[1]) * 17 c.G = hexToByte(s[2]) * 17 c.B = hexToByte(s[3]) * 17 default: err = errInvalidFormat } return c, err } // RGBColor is a color specified by red, green, and blue values. type RGBColor struct { R uint8 G uint8 B uint8 } // RGBA returns the RGBA value of this color. This satisfies the Go Color // interface. func (c RGBColor) RGBA() (r, g, b, a uint32) { const shift = 8 r |= uint32(c.R) << shift g |= uint32(c.G) << shift b |= uint32(c.B) << shift a = 0xFFFF return } // ANSIColor is a color specified by an ANSI256 color value. // // Example usage: // // colorA := lipgloss.ANSIColor(8) // colorB := lipgloss.ANSIColor(134) type ANSIColor = ansi.IndexedColor // LightDarkFunc is a function that returns a color based on whether the // terminal has a light or dark background. You can create one of these with // [LightDark]. // // Example: // // lightDark := lipgloss.LightDark(hasDarkBackground) // red, blue := lipgloss.Color("#ff0000"), lipgloss.Color("#0000ff") // myHotColor := lightDark(red, blue) // // For more info see [LightDark]. type LightDarkFunc func(light, dark color.Color) color.Color // LightDark is a simple helper type that can be used to choose the appropriate // color based on whether the terminal has a light or dark background. // // lightDark := lipgloss.LightDark(hasDarkBackground) // red, blue := lipgloss.Color("#ff0000"), lipgloss.Color("#0000ff") // myHotColor := lightDark(red, blue) // // In practice, there are slightly different workflows between Bubble Tea and // Lip Gloss standalone. // // In Bubble Tea, listen for tea.BackgroundColorMsg, which automatically // flows through Update on start. This message will be received whenever the // background color changes: // // case tea.BackgroundColorMsg: // m.hasDarkBackground = msg.IsDark() // // Later, when you're rendering use: // // lightDark := lipgloss.LightDark(m.hasDarkBackground) // red, blue := lipgloss.Color("#ff0000"), lipgloss.Color("#0000ff") // myHotColor := lightDark(red, blue) // // In standalone Lip Gloss, the workflow is simpler: // // hasDarkBG := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) // lightDark := lipgloss.LightDark(hasDarkBG) // red, blue := lipgloss.Color("#ff0000"), lipgloss.Color("#0000ff") // myHotColor := lightDark(red, blue) func LightDark(isDark bool) LightDarkFunc { return func(light, dark color.Color) color.Color { if isDark { return dark } return light } } // isDarkColor returns whether the given color is dark (based on the luminance // portion of the color as interpreted as HSL). // // Example usage: // // color := lipgloss.Color("#0000ff") // if lipgloss.isDarkColor(color) { // fmt.Println("It's dark! I love darkness!") // } else { // fmt.Println("It's light! Cover your eyes!") // } func isDarkColor(c color.Color) bool { col, ok := colorful.MakeColor(c) if !ok { return true } _, _, l := col.Hsl() return l < 0.5 //nolint:mnd } // CompleteFunc is a function that returns the appropriate color based on the // given color profile. // // Example usage: // // p := colorprofile.Detect(os.Stderr, os.Environ()) // complete := lipgloss.Complete(p) // color := complete( // lipgloss.Color(1), // ANSI // lipgloss.Color(124), // ANSI256 // lipgloss.Color("#ff34ac"), // TrueColor // ) // fmt.Println("Ooh, pretty color: ", color) // // For more info see [Complete]. type CompleteFunc func(ansi, ansi256, truecolor color.Color) color.Color // Complete returns a function that will return the appropriate color based on // the given color profile. // // Example usage: // // p := colorprofile.Detect(os.Stderr, os.Environ()) // complete := lipgloss.Complete(p) // color := complete( // lipgloss.Color(1), // ANSI // lipgloss.Color(124), // ANSI256 // lipgloss.Color("#ff34ac"), // TrueColor // ) // fmt.Println("Ooh, pretty color: ", color) func Complete(p colorprofile.Profile) CompleteFunc { return func(ansi, ansi256, truecolor color.Color) color.Color { switch p { //nolint:exhaustive case colorprofile.ANSI: return ansi case colorprofile.ANSI256: return ansi256 case colorprofile.TrueColor: return truecolor } return noColor } } // ensureNotTransparent ensures that the alpha value of a color is not 0, and if // it is, we will set it to 1. This is useful for when we are converting from // RGB -> RGBA, and the alpha value is lost in the conversion for gradient purposes. func ensureNotTransparent(c color.Color) color.Color { _, _, _, a := c.RGBA() if a == 0 { return Alpha(c, 1) } return c } // Alpha adjusts the alpha value of a color using a 0-1 (clamped) float scale // 0 = transparent, 1 = opaque. func Alpha(c color.Color, alpha float64) color.Color { if c == nil { return nil } r, g, b, _ := c.RGBA() return color.RGBA{ R: uint8(min(255, float64(r>>8))), G: uint8(min(255, float64(g>>8))), B: uint8(min(255, float64(b>>8))), A: uint8(clamp(alpha, 0, 1) * 255), } } // Complementary returns the complementary color (180° away on color wheel) of // the given color. This is useful for creating a contrasting color. func Complementary(c color.Color) color.Color { if c == nil { return nil } // Offset hue by 180°. cf, _ := colorful.MakeColor(ensureNotTransparent(c)) h, s, v := cf.Hsv() h += 180 if h >= 360 { h -= 360 } else if h < 0 { h += 360 } return colorful.Hsv(h, s, v).Clamped() } // Darken takes a color and makes it darker by a specific percentage (0-1, clamped). func Darken(c color.Color, percent float64) color.Color { if c == nil { return nil } mult := 1.0 - clamp(percent, 0, 1) r, g, b, a := c.RGBA() return color.RGBA{ R: uint8(float64(r>>8) * mult), G: uint8(float64(g>>8) * mult), B: uint8(float64(b>>8) * mult), A: uint8(min(255, float64(a>>8))), } } // Lighten makes a color lighter by a specific percentage (0-1, clamped). func Lighten(c color.Color, percent float64) color.Color { if c == nil { return nil } add := 255 * (clamp(percent, 0, 1)) r, g, b, a := c.RGBA() return color.RGBA{ R: uint8(min(255, float64(r>>8)+add)), G: uint8(min(255, float64(g>>8)+add)), B: uint8(min(255, float64(b>>8)+add)), A: uint8(min(255, float64(a>>8))), } } ================================================ FILE: color_test.go ================================================ package lipgloss import ( "fmt" "image/color" "testing" ) // hex converts a color to a hex string or panics if invalid. func hex(hex string) color.Color { cf, err := parseHex(hex) if err != nil { panic(err) } return cf } func expectColorMatches(t *testing.T, got, want color.Color) { t.Helper() if (got == nil) != (want == nil) { t.Errorf("expectColorMatches() = %s, want %s", rgbaString(t, got), rgbaString(t, want)) } if got == nil { return } gr, gg, gb, ga := got.RGBA() wr, wg, wb, wa := want.RGBA() gru, ggu, gbu, gau := uint8(gr>>8), uint8(gg>>8), uint8(gb>>8), uint8(ga>>8) wru, wgu, wbu, wau := uint8(wr>>8), uint8(wg>>8), uint8(wb>>8), uint8(wa>>8) if gru != wru || ggu != wgu || gbu != wbu || gau != wau { t.Errorf("expectColorMatches() = %s, want %s", rgbaString(t, got), rgbaString(t, want)) } } func rgbaString(t *testing.T, c color.Color) string { t.Helper() if c == nil { return "nil" } r, g, b, a := c.RGBA() return fmt.Sprintf("rgba(%d,%d,%d,%d)", uint8(r>>8), uint8(g>>8), uint8(b>>8), uint8(a>>8)) } func TestHexToColor(t *testing.T) { t.Parallel() tt := []struct { input string expected uint }{ { "#FF0000", 0xFF0000, }, { "#00F", 0x0000FF, }, { "#6B50FF", 0x6B50FF, }, { "invalid color", 0x0, }, } for i, tc := range tt { r, g, b, _ := Color(tc.input).RGBA() o := uint(r>>8)<<16 + uint(g>>8)<<8 + uint(b>>8) if o != tc.expected { t.Errorf("expected %X, got %X (test #%d)", tc.expected, o, i+1) } } } func TestRGBA(t *testing.T) { tt := []struct { input string expected uint }{ // lipgloss.Color { "#FF0000", 0xFF0000, }, { "9", 0xFF0000, }, { "21", 0x0000FF, }, { "16711680", // #FF0000 0xFF0000, }, } for i, tc := range tt { r, g, b, _ := Color(tc.input).RGBA() o := uint(r/256)<<16 + uint(g/256)<<8 + uint(b/256) if o != tc.expected { t.Errorf("expected %X, got %X (test #%d)", tc.expected, o, i+1) } } } func TestParseHex(t *testing.T) { tests := []struct { name string input string expected color.Color expectError bool }{ {name: "valid-6-red", input: "#FF0000", expected: hex("#FF0000")}, {name: "valid-6-green", input: "#00FF00", expected: hex("#00FF00")}, {name: "valid-6-blue", input: "#0000FF", expected: hex("#0000FF")}, {name: "valid-6-white", input: "#FFFFFF", expected: hex("#FFFFFF")}, {name: "valid-6-black", input: "#000000", expected: hex("#000000")}, {name: "valid-6-gray", input: "#808080", expected: hex("#808080")}, {name: "valid-3-red", input: "#F00", expected: hex("#FF0000")}, {name: "valid-3-green", input: "#0F0", expected: hex("#00FF00")}, {name: "valid-3-blue", input: "#00F", expected: hex("#0000FF")}, {name: "valid-3-white", input: "#FFF", expected: hex("#FFFFFF")}, {name: "valid-3-black", input: "#000", expected: hex("#000000")}, {name: "valid-6-lowercase", input: "#ff0000", expected: hex("#FF0000")}, {name: "valid-6-mixed-case", input: "#Ff0000", expected: hex("#FF0000")}, {name: "valid-3-lowercase", input: "#f00", expected: hex("#FF0000")}, {name: "missing-hash-prefix", input: "FF0000", expectError: true}, {name: "empty-string", input: "", expectError: true}, {name: "only-hash", input: "#", expectError: true}, {name: "too-short-3", input: "#F0", expectError: true}, {name: "too-long-6", input: "#FF00000", expectError: true}, {name: "invalid-char", input: "#FG0000", expectError: true}, {name: "invalid-char-3", input: "#FG0", expectError: true}, {name: "invalid-char-lowercase", input: "#fg0000", expectError: true}, {name: "invalid-char-mixed", input: "#Fg0000", expectError: true}, {name: "wrong-len-5", input: "#FF000", expectError: true}, {name: "wrong-len-8", input: "#FF000000", expectError: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() result, err := parseHex(tt.input) if tt.expectError { if err == nil { t.Errorf("FromHex() expected error but got none for input %q", tt.input) } return } if err != nil { t.Errorf("FromHex() unexpected error for input %q: %v", tt.input, err) return } expectColorMatches(t, result, tt.expected) }) } } func TestAlpha(t *testing.T) { tests := []struct { name string color color.Color alpha float64 expected color.Color }{ { name: "alpha-full-opacity", color: color.RGBA{R: 255, G: 0, B: 0, A: 255}, alpha: 1.0, expected: color.RGBA{R: 255, G: 0, B: 0, A: 255}, }, { name: "alpha-half-opacity", color: color.RGBA{R: 0, G: 255, B: 0, A: 255}, alpha: 0.5, expected: color.RGBA{R: 0, G: 255, B: 0, A: 127}, }, { name: "alpha-quarter-opacity", color: color.RGBA{R: 0, G: 0, B: 255, A: 255}, alpha: 0.25, expected: color.RGBA{R: 0, G: 0, B: 255, A: 63}, }, { name: "alpha-zero-opacity", color: color.RGBA{R: 255, G: 255, B: 255, A: 255}, alpha: 0.0, expected: color.RGBA{R: 255, G: 255, B: 255, A: 0}, }, { name: "alpha-clamp-above-max", color: color.RGBA{R: 255, G: 0, B: 255, A: 255}, alpha: 1.5, expected: color.RGBA{R: 255, G: 0, B: 255, A: 255}, }, { name: "alpha-clamp-below-min", color: color.RGBA{R: 255, G: 255, B: 0, A: 255}, alpha: -0.5, expected: color.RGBA{R: 255, G: 255, B: 0, A: 0}, }, { name: "alpha-complex-color", color: color.RGBA{R: 18, G: 52, B: 86, A: 255}, alpha: 0.75, expected: color.RGBA{R: 18, G: 52, B: 86, A: 191}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() expectColorMatches(t, Alpha(tt.color, tt.alpha), tt.expected) }) } } func TestComplementary(t *testing.T) { tests := []struct { name string color color.Color expected color.Color }{ {name: "red", color: hex("#FF0000"), expected: hex("#00FFFF")}, {name: "green", color: hex("#00FF00"), expected: hex("#FF00FF")}, {name: "blue", color: hex("#0000FF"), expected: hex("#FFFF00")}, {name: "yellow", color: hex("#FFFF00"), expected: hex("#0000FF")}, {name: "cyan", color: hex("#00FFFF"), expected: hex("#FF0000")}, {name: "magenta", color: hex("#FF00FF"), expected: hex("#00FF00")}, // Black has no hue to complement {name: "black", color: hex("#000000"), expected: hex("#000000")}, // White has no hue to complement {name: "white", color: hex("#FFFFFF"), expected: hex("#FFFFFF")}, // Gray has no hue to complement {name: "gray", color: hex("#808080"), expected: hex("#808080")}, {name: "orange", color: hex("#FF8000"), expected: hex("#007FFF")}, {name: "purple", color: hex("#8000FF"), expected: hex("#7FFF00")}, {name: "nil-color", color: nil, expected: nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() expectColorMatches(t, Complementary(tt.color), tt.expected) }) } } func TestDarken(t *testing.T) { tests := []struct { name string color color.Color percent float64 expected color.Color }{ {name: "darken-white-50-percent", color: hex("#FFFFFF"), percent: 0.5, expected: hex("#7F7F7F")}, {name: "darken-red-25-percent", color: hex("#FF0000"), percent: 0.25, expected: hex("#BF0000")}, {name: "darken-blue-75-percent", color: hex("#0000FF"), percent: 0.75, expected: hex("#00003F")}, {name: "darken-black-10-percent", color: hex("#000000"), percent: 0.1, expected: hex("#000000")}, {name: "darken-with-clamp-min", color: hex("#FFFFFF"), percent: 0, expected: hex("#FFFFFF")}, {name: "darken-with-clamp-max", color: hex("#FFFFFF"), percent: 1, expected: hex("#000000")}, {name: "darken-nil-color", color: nil, percent: 0.5, expected: nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() expectColorMatches(t, Darken(tt.color, tt.percent), tt.expected) }) } } func TestLighten(t *testing.T) { tests := []struct { name string color color.Color percent float64 expected color.Color }{ {name: "lighten-black-50-percent", color: hex("#000000"), percent: 0.5, expected: hex("#7F7F7F")}, {name: "lighten-red-25-percent", color: hex("#800000"), percent: 0.25, expected: hex("#BF3F3F")}, {name: "lighten-blue-75-percent", color: hex("#000080"), percent: 0.75, expected: hex("#BFBFFF")}, {name: "lighten-white-10-percent", color: hex("#FFFFFF"), percent: 0.1, expected: hex("#FFFFFF")}, {name: "lighten-with-clamp-min", color: hex("#000000"), percent: 0, expected: hex("#000000")}, {name: "lighten-with-clamp-max", color: hex("#000000"), percent: 1, expected: hex("#FFFFFF")}, {name: "lighten-nil-color", color: nil, percent: 0.5, expected: nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() expectColorMatches(t, Lighten(tt.color, tt.percent), tt.expected) }) } } ================================================ FILE: compat/color.go ================================================ package compat import ( "image/color" "os" "charm.land/lipgloss/v2" "github.com/charmbracelet/colorprofile" ) var ( // HasDarkBackground is true if the terminal has a dark background. HasDarkBackground = lipgloss.HasDarkBackground(os.Stdin, os.Stdout) // Profile is the color profile of the terminal. Profile = colorprofile.Detect(os.Stdout, os.Environ()) ) // AdaptiveColor provides color options for light and dark backgrounds. The // appropriate color will be returned at runtime based on the darkness of the // terminal background color. // // Example usage: // // color := lipgloss.AdaptiveColor{Light: "#0000ff", Dark: "#000099"} type AdaptiveColor struct { Light color.Color Dark color.Color } // RGBA returns the RGBA value of this color. This satisfies the Go Color // interface. func (c AdaptiveColor) RGBA() (uint32, uint32, uint32, uint32) { if HasDarkBackground { return c.Dark.RGBA() } return c.Light.RGBA() } // CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color // profiles. Automatic color degradation will not be performed. type CompleteColor struct { TrueColor color.Color ANSI256 color.Color ANSI color.Color } // RGBA returns the RGBA value of this color. This satisfies the Go Color // interface. func (c CompleteColor) RGBA() (uint32, uint32, uint32, uint32) { switch Profile { //nolint:exhaustive case colorprofile.TrueColor: return c.TrueColor.RGBA() case colorprofile.ANSI256: return c.ANSI256.RGBA() case colorprofile.ANSI: return c.ANSI.RGBA() } return lipgloss.NoColor{}.RGBA() } // CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color // profiles, with separate options for light and dark backgrounds. Automatic // color degradation will not be performed. type CompleteAdaptiveColor struct { Light CompleteColor Dark CompleteColor } // RGBA returns the RGBA value of this color. This satisfies the Go Color // interface. func (c CompleteAdaptiveColor) RGBA() (uint32, uint32, uint32, uint32) { if HasDarkBackground { return c.Dark.RGBA() } return c.Light.RGBA() } ================================================ FILE: compat/doc.go ================================================ // Package compat is a compatibility layer for Lip Gloss that provides a way to // deal with the hassle of setting up a writer. It's impure because it uses // global variables, is not thread-safe, and only works with the default // standard I/O streams. // // In case you want [os.Stderr] to be used as the default writer, you can set // both [Writer] and [HasDarkBackground] to use [os.Stderr] with // the following code: // // import ( // "os" // // "github.com/charmbracelet/colorprofile" // "charm.land/lipgloss/v2/impure" // ) // // func init() { // impure.Writer = colorprofile.NewWriter(os.Stderr, os.Environ()) // impure.HasDarkBackground, _ = lipgloss.HasDarkBackground(os.Stdin, os.Stderr) // } package compat ================================================ FILE: examples/blending/border-blend-rotation/bubbletea/main.go ================================================ package main import ( "fmt" "os" "time" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" ) const ( borderRotationFPS = 15 borderRotationSteps = 5 ) type borderRotationTickMsg struct { Value int } func borderRotationTick(current int) tea.Cmd { return tea.Tick(time.Second/time.Duration(borderRotationFPS), func(_ time.Time) tea.Msg { return borderRotationTickMsg{Value: current + borderRotationSteps} }) } type model struct { borderRotation int } func (m model) Init() tea.Cmd { return borderRotationTick(0) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: switch msg.String() { case "q", "esc", "ctrl+c": return m, tea.Quit } case borderRotationTickMsg: m.borderRotation = msg.Value return m, borderRotationTick(msg.Value) } return m, nil } func (m model) View() tea.View { v := tea.NewView(lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForegroundBlend( lipgloss.Color("#00FA68"), lipgloss.Color("#9900FF"), lipgloss.Color("#ED5353"), lipgloss.Color("#9900FF"), lipgloss.Color("#00FA68"), ). BorderForegroundBlendOffset(m.borderRotation). Width(60). Height(15). Render("Hello, world!")) v.AltScreen = true return v } func main() { _, err := tea.NewProgram(model{}).Run() if err != nil { fmt.Fprintf(os.Stderr, "Uh oh: %v", err) os.Exit(1) } } ================================================ FILE: examples/blending/linear-1d/bubbletea/main.go ================================================ // This example demonstrates how to use the colors.Blend1D function to create // beautiful color gradients in a Bubble Tea application. package main import ( "fmt" "image/color" "os" "strings" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" ) var gradients = []gradientData{ { name: "Sunset", stops: []color.Color{ lipgloss.Color("#FF6B6B"), // Coral lipgloss.Color("#FFB74D"), // Orange lipgloss.Color("#FFDFBA"), // Peach }, }, { name: "Ocean", stops: []color.Color{ lipgloss.Color("#0077B6"), // Deep Blue lipgloss.Color("#48CAE4"), // Sky Blue lipgloss.Color("#ADE8F4"), // Light Blue }, }, { name: "Forest", stops: []color.Color{ lipgloss.Color("#228B22"), // Forest Green lipgloss.Color("#90EE90"), // Light Green lipgloss.Color("#FFFFE0"), // Cream }, }, { name: "Purple Dream", stops: []color.Color{ lipgloss.Color("#9370DB"), // Medium Purple lipgloss.Color("#DDA0DD"), // Plum lipgloss.Color("#FFB6C1"), // Light Pink }, }, { name: "Fire", stops: []color.Color{ lipgloss.Color("#FF0000"), // Red lipgloss.Color("#FFA500"), // Orange lipgloss.Color("#FFFF00"), // Yellow }, }, } type gradientData struct { name string stops []color.Color } // Style definitions. type styles struct { // UI styles. title lipgloss.Style gradientName lipgloss.Style info lipgloss.Style } func newStyles(dark bool) (s *styles) { s = &styles{} lightDark := lipgloss.LightDark(dark) s.title = lipgloss.NewStyle(). Bold(true). Foreground(lightDark(lipgloss.Color("#2D3748"), lipgloss.Color("#E2E8F0"))). MarginBottom(1). Align(lipgloss.Center) s.gradientName = lipgloss.NewStyle(). Bold(true). Foreground(lightDark(lipgloss.Color("#4A5568"), lipgloss.Color("#CBD5E0"))). PaddingRight(1) s.info = lipgloss.NewStyle(). Foreground(lightDark(lipgloss.Color("#718096"), lipgloss.Color("#A0AEC0"))). Italic(true) return s } type model struct { width int height int styles *styles } func (m model) Init() tea.Cmd { return tea.RequestBackgroundColor } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height return m, nil case tea.BackgroundColorMsg: m.styles = newStyles(msg.IsDark()) return m, nil case tea.KeyPressMsg: switch msg.String() { case "q", "esc", "ctrl+c": return m, tea.Quit } } return m, nil } func (m model) View() tea.View { var maxTitleWidth int for _, gradient := range gradients { maxTitleWidth = max(maxTitleWidth, lipgloss.Width(m.styles.gradientName.Render(gradient.name))) } var content strings.Builder content.WriteString(m.styles.title.Render("Color Gradient Examples with Blend1D")) content.WriteString("\n\n") var title string for _, gradient := range gradients { title = m.styles.gradientName.Width(maxTitleWidth).Render(gradient.name) content.WriteString(title) blendedColors := lipgloss.Blend1D(m.width-maxTitleWidth, gradient.stops...) for _, c := range blendedColors { content.WriteString(lipgloss.NewStyle().Background(c).Foreground(c).Render("█")) } content.WriteString("\n") } content.WriteString("\n") content.WriteString(m.styles.info.Render("Press Q to exit")) cursor := &tea.Cursor{} cursor.X = 0 cursor.Y = 0 v := tea.NewView(content.String()) v.Cursor = cursor v.AltScreen = true return v } func main() { _, err := tea.NewProgram(model{styles: newStyles(true)}).Run() if err != nil { fmt.Fprintf(os.Stderr, "Uh oh: %v", err) os.Exit(1) } } ================================================ FILE: examples/blending/linear-1d/standalone/main.go ================================================ // This example demonstrates how to use the colors.Blend1D function to create // beautiful color gradients in a standalone Lip Gloss application. package main import ( "image/color" "os" "strings" "charm.land/lipgloss/v2" ) var gradients = [][]color.Color{ { lipgloss.Color("#FF6B6B"), // Coral lipgloss.Color("#FFB74D"), // Orange lipgloss.Color("#FFDFBA"), // Peach }, { lipgloss.Color("#0077B6"), // Deep Blue lipgloss.Color("#48CAE4"), // Sky Blue lipgloss.Color("#ADE8F4"), // Light Blue }, { lipgloss.Color("#228B22"), // Forest Green lipgloss.Color("#90EE90"), // Light Green lipgloss.Color("#FFFFE0"), // Cream }, { lipgloss.Color("#9370DB"), // Medium Purple lipgloss.Color("#DDA0DD"), // Plum lipgloss.Color("#FFB6C1"), // Light Pink }, { lipgloss.Color("#9900FF"), // Purple lipgloss.Color("#00FA68"), // Lime lipgloss.Color("#ED5353"), // Red }, } func main() { hasDarkBG := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) lightDark := lipgloss.LightDark(hasDarkBG) // Create styles. titleStyle := lipgloss.NewStyle(). Bold(true). Foreground(lightDark(lipgloss.Color("#2D3748"), lipgloss.Color("#E2E8F0"))). MarginBottom(1). Align(lipgloss.Center) gradientStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lightDark(lipgloss.Color("#718096"), lipgloss.Color("#A0AEC0"))) var content strings.Builder content.WriteString(titleStyle.Render("Color Gradient Examples with Blend1D")) content.WriteString("\n") for _, gradient := range gradients { blendedColors := lipgloss.Blend1D(40, gradient...) var gradientBar strings.Builder for _, c := range blendedColors { blockStyle := lipgloss.NewStyle().Foreground(c) gradientBar.WriteString(blockStyle.Render("█")) } content.WriteString(gradientStyle.Render(gradientBar.String())) content.WriteString("\n") } lipgloss.Println(content.String()) } ================================================ FILE: examples/blending/linear-2d/bubbletea/main.go ================================================ // This example demonstrates how to use the colors.Blend2D function to create // beautiful 2D color gradients in a Bubble Tea application. package main import ( "cmp" "fmt" "image/color" "math" "os" "strings" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/charmbracelet/x/exp/charmtone" ) var gradients = [][]color.Color{ { lipgloss.Color("#FF6B6B"), // Coral lipgloss.Color("#FFB74D"), // Orange lipgloss.Color("#FFDFBA"), // Peach }, { lipgloss.Color("#0077B6"), // Deep Blue lipgloss.Color("#48CAE4"), // Sky Blue lipgloss.Color("#ADE8F4"), // Light Blue }, { lipgloss.Color("#228B22"), // Forest Green lipgloss.Color("#90EE90"), // Light Green lipgloss.Color("#FFFFE0"), // Cream }, { lipgloss.Color("#9370DB"), // Medium Purple lipgloss.Color("#DDA0DD"), // Plum lipgloss.Color("#FFB6C1"), // Light Pink }, { lipgloss.Color("#9900FF"), // Purple lipgloss.Color("#00FA68"), // Lime lipgloss.Color("#ED5353"), // Red }, } func main() { m := model{ boxWidth: 20, boxHeight: 10, angle: 45, selectedGradient: 0, infoStyle: lipgloss.NewStyle(). Foreground(lipgloss.Color("#888888")). MarginTop(1), controlsStyle: lipgloss.NewStyle(). Foreground(lipgloss.Color("#666666")). MarginTop(1), gradientBoxStyle: lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForegroundBlend( charmtone.Cherry, charmtone.Charple, charmtone.Guac, charmtone.Charple, charmtone.Sriracha, ), } p := tea.NewProgram(m) if _, err := p.Run(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) os.Exit(1) } } type model struct { // UI state. windowWidth int windowHeight int boxWidth int boxHeight int angle float64 selectedGradient int // UI styles. infoStyle lipgloss.Style controlsStyle lipgloss.Style gradientBoxStyle lipgloss.Style gradients []color.Color } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.windowWidth = msg.Width m.windowHeight = msg.Height m.updateGradient() case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c", "esc": return m, tea.Quit case "a": m.angle = math.Mod(m.angle+15, 360) m.updateGradient() case "d": m.angle = math.Mod(m.angle-15+360, 360) m.updateGradient() case "left": m.boxWidth -= 2 m.updateGradient() case "right": m.boxWidth += 2 m.updateGradient() case "up": m.boxHeight-- m.updateGradient() case "down": m.boxHeight++ m.updateGradient() case "1", "2", "3", "4", "5", "6", "7", "8", "9": m.selectedGradient = max(0, min(int(msg.String()[0]-'1'), len(gradients)-1)) m.updateGradient() } case tea.MouseClickMsg: switch msg.Mouse().Button { case tea.MouseLeft: m.boxWidth = msg.Mouse().X m.boxHeight = msg.Mouse().Y m.updateGradient() } } return m, nil } func (m *model) updateGradient() { m.boxWidth = clamp(m.boxWidth, 5, m.windowWidth-m.gradientBoxStyle.GetHorizontalFrameSize()) m.boxHeight = clamp(m.boxHeight, 3, m.windowHeight-m.gradientBoxStyle.GetVerticalFrameSize()-m.infoStyle.GetVerticalFrameSize()-m.controlsStyle.GetVerticalFrameSize()-2) // Since gradients that might be large can take up more memory, only generate gradients when // the box size (potentially) changes. If you have much smaller gradients, this is less of // an issue. m.gradients = lipgloss.Blend2D(m.boxWidth, m.boxHeight, m.angle, gradients[m.selectedGradient]...) } func (m model) View() tea.View { var v tea.View v.AltScreen = true v.MouseMode = tea.MouseModeCellMotion if len(m.gradients) == 0 || m.windowWidth == 0 || m.windowHeight == 0 { return v // Wait until we generate the initial gradient/get window size. } // Build the gradient content. gradientContent := strings.Builder{} for y := range m.boxHeight { // Uses 1D row-major order. for x := range m.boxWidth { index := y*m.boxWidth + x gradientContent.WriteString( lipgloss.NewStyle(). Background(m.gradients[index]). Render(" "), ) } if y < m.boxHeight-1 { // End of row. gradientContent.WriteString("\n") } } gradient := m.gradientBoxStyle.Render(gradientContent.String()) info := m.infoStyle.Width(m.windowWidth).Render(fmt.Sprintf( "Size: %dx%d | Angle: %.1f° | Colors: %d", m.boxWidth, m.boxHeight, m.angle, len(gradients[m.selectedGradient]), )) controls := m.controlsStyle.Width(m.windowWidth).Render(fmt.Sprintf( "Controls: a/d (angle) | ←→ (width) | ↑↓ (height) | 1-%d (color scheme) | mouse click", len(gradients), )) content := lipgloss.NewStyle(). Width(m.windowWidth). Height(m.windowHeight). Render(lipgloss.JoinVertical( lipgloss.Top, lipgloss.NewStyle(). Width(m.windowWidth). Height(m.windowHeight-lipgloss.Height(info)-lipgloss.Height(controls)). Render(gradient), info, controls, )) v.SetContent(content) return v } func clamp[T cmp.Ordered](v, low, high T) T { return min(high, max(low, v)) } ================================================ FILE: examples/blending/linear-2d/standalone/main.go ================================================ // This example demonstrates how to use the colors.Blend2D function to create // beautiful 2D color gradients in a standalone Lip Gloss application. package main import ( "fmt" "image/color" "os" "strings" "charm.land/lipgloss/v2" ) func main() { hasDarkBG := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) lightDark := lipgloss.LightDark(hasDarkBG) gradients := []struct { name string stops []color.Color angle float64 }{ { name: "Sunset Diagonal", stops: []color.Color{ lipgloss.Color("#FF6B6B"), // Coral lipgloss.Color("#FFB74D"), // Orange lipgloss.Color("#FFDFBA"), // Peach }, angle: 45, }, { name: "Ocean Wave", stops: []color.Color{ lipgloss.Color("#0077B6"), // Deep Blue lipgloss.Color("#48CAE4"), // Sky Blue lipgloss.Color("#ADE8F4"), // Light Blue }, angle: 90, }, { name: "Forest Mist", stops: []color.Color{ lipgloss.Color("#228B22"), // Forest Green lipgloss.Color("#90EE90"), // Light Green lipgloss.Color("#FFFFE0"), // Cream }, angle: 135, }, { name: "Purple Dream", stops: []color.Color{ lipgloss.Color("#9370DB"), // Medium Purple lipgloss.Color("#DDA0DD"), // Plum lipgloss.Color("#FFB6C1"), // Light Pink }, angle: 180, }, { name: "Fire Gradient", stops: []color.Color{ lipgloss.Color("#FF0000"), // Red lipgloss.Color("#FFA500"), // Orange lipgloss.Color("#FFFF00"), // Yellow }, angle: 225, }, } // Create styles. titleStyle := lipgloss.NewStyle(). Bold(true). Foreground(lightDark(lipgloss.Color("#2D3748"), lipgloss.Color("#E2E8F0"))). MarginBottom(1). Align(lipgloss.Center) gradientStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lightDark(lipgloss.Color("#718096"), lipgloss.Color("#A0AEC0"))). MarginBottom(1) gradientNameStyle := lipgloss.NewStyle(). Bold(true). Foreground(lightDark(lipgloss.Color("#4A5568"), lipgloss.Color("#CBD5E0"))). MarginBottom(1) var content strings.Builder content.WriteString(titleStyle.Render("2D Color Gradient Examples with Blend2D")) content.WriteString("\n\n") for _, gradient := range gradients { // Generate the gradient using Blend2D. width, height := 30, 12 blendedColors := lipgloss.Blend2D(width, height, gradient.angle, gradient.stops...) // Create the gradient box using individual character styling. var gradientBox strings.Builder for y := range height { // Uses 1D row-major order. for x := range width { index := y*width + x gradientBox.WriteString( lipgloss.NewStyle(). Foreground(blendedColors[index]). Render("█"), ) } if y < height-1 { // End of row. gradientBox.WriteString("\n") } } content.WriteString(gradientNameStyle.Render(fmt.Sprintf("%s (Angle: %.0f°)", gradient.name, gradient.angle))) content.WriteString("\n") content.WriteString(gradientStyle.Render(gradientBox.String())) content.WriteString("\n") } lipgloss.Println(content.String()) } ================================================ FILE: examples/brightness/main.go ================================================ // This example demonstrates how to use the colors.Lighten and colors.Darken functions // to create progressive brightness variations in a standalone Lip Gloss application. package main import ( "image/color" "os" "strings" "charm.land/lipgloss/v2" ) func main() { hasDarkBG := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) lightDark := lipgloss.LightDark(hasDarkBG) // Base colors to demonstrate lightening and darkening. baseColors := map[string]color.Color{ "Red": lipgloss.Color("#FF0000"), "Blue": lipgloss.Color("#0066FF"), "Green": lipgloss.Color("#00FF00"), "Gray": lipgloss.Color("#808080"), } // Percentage to lighten/darken by. percentage := 0.05 // 5% // Number of steps to generate. steps := 20 colorNameStyle := lipgloss.NewStyle(). Bold(true). Foreground(lightDark(lipgloss.Color("#2D3748"), lipgloss.Color("#E2E8F0"))) var content strings.Builder for name, baseColor := range baseColors { content.WriteString(colorNameStyle.Render(name)) content.WriteString("\n") // Create lightened variations. var lightenedBox strings.Builder lightenedBox.WriteString("Lightened: ") for i := range steps { lightenedBox.WriteString( lipgloss.NewStyle(). Foreground(lipgloss.Lighten(baseColor, percentage*(float64(i)+1))). Render("██"), ) } content.WriteString(lightenedBox.String()) content.WriteString("\n") // Create darkened variations. var darkenedBox strings.Builder darkenedBox.WriteString("Darkened: ") for i := range steps { darkenedBox.WriteString( lipgloss.NewStyle(). Foreground(lipgloss.Darken(baseColor, percentage*(float64(i)+1))). Render("██"), ) } content.WriteString(darkenedBox.String()) content.WriteString("\n\n") } lipgloss.Println(content.String()) } ================================================ FILE: examples/canvas/main.go ================================================ package main import ( "image/color" "os" "strings" "charm.land/lipgloss/v2" "github.com/charmbracelet/x/exp/charmtone" ) // newField fills a rectangular area with a given character in a given color. func newField(rows, cols int, color color.Color) string { fieldSetyle := lipgloss.NewStyle().Foreground(color) fieldBuilder := strings.Builder{} for i := range rows { for range cols { fieldBuilder.WriteString("/") } if i < rows-1 { fieldBuilder.WriteString("\n") } } return fieldSetyle.Render(fieldBuilder.String()) } // newCard creates a little card with rounded borders and a text label. func newCard(darkMode bool, text string) string { lightDark := lipgloss.LightDark(darkMode) return lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForegroundBlend( charmtone.Cherry, charmtone.Charple, charmtone.Guac, charmtone.Charple, charmtone.Sriracha, ). Foreground(lightDark(charmtone.Iron, charmtone.Butter)). Height(9). Width(16). PaddingTop(3). Align(lipgloss.Center). Render(text) } func main() { darkMode := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) lightDark := lipgloss.LightDark(darkMode) // A few text blocks. lighterField := newField(17, 43, lightDark(charmtone.Smoke, charmtone.Pepper)) darkerField := newField(17, 43, lightDark(charmtone.Squid, charmtone.Charcoal)) // A few layers. Layers are created from strings (or blocks of text). pickles := lipgloss.NewLayer(newCard(darkMode, "Pickles")) melon := lipgloss.NewLayer(newCard(darkMode, "Bitter Melon")) sriracha := lipgloss.NewLayer(newCard(darkMode, "Sriracha")) // Let's create our layers. layers := []*lipgloss.Layer{ // Layers can have X, Y, and Z offsets. By default, X, Y, and // Z are all 0. lipgloss.NewLayer(lighterField).X(5).Y(2), // Layers can be nested. lipgloss.NewLayer(darkerField).AddLayers( pickles.X(4).Y(2).Z(1), // the Z index places this layer above the others melon.X(22).Y(1), sriracha.X(11).Y(7), ), } // A compositor takes multiple layers and composites them together into // a single output. comp := lipgloss.NewCompositor(layers...) lipgloss.Println(comp.Render()) } ================================================ FILE: examples/color/bubbletea/main.go ================================================ package main import ( "fmt" "os" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" ) // Style definitions. type styles struct { frame, paragraph, text, keyword, activeButton, inactiveButton lipgloss.Style } // Styles are initialized based on the background color of the terminal. func newStyles(backgroundIsDark bool) (s *styles) { s = new(styles) // Create a new helper function for choosing either a light or dark color // based on the detected background color. lightDark := lipgloss.LightDark(backgroundIsDark) // Define some styles. adaptive.Color() can be used to choose the // appropriate light or dark color based on the detected background color. s.frame = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lightDark( lipgloss.Color("#C5ADF9"), lipgloss.Color("#864EFF"))). Padding(1, 3). Margin(1, 3) s.paragraph = lipgloss.NewStyle(). Width(40). MarginBottom(1). Align(lipgloss.Center) s.text = lipgloss.NewStyle(). Foreground(lightDark( lipgloss.Color("#696969"), lipgloss.Color("#bdbdbd"))) s.keyword = lipgloss.NewStyle(). Foreground(lightDark( lipgloss.Color("#37CD96"), lipgloss.Color("#22C78A"))). Bold(true) s.activeButton = lipgloss.NewStyle(). Padding(0, 3). Background(lipgloss.Color("#FF6AD2")). Foreground(lipgloss.Color("#FFFCC2")) s.inactiveButton = s.activeButton. Background(lightDark( lipgloss.Color("#988F95"), lipgloss.Color("#978692"))). Foreground(lightDark( lipgloss.Color("#FDFCE3"), lipgloss.Color("#FBFAE7"))) return s } type model struct { styles *styles yes bool chosen bool aborted bool } func (m model) Init() tea.Cmd { // Query for the background color on start. m.yes = true return tea.RequestBackgroundColor } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { // Bubble Tea automatically detects the background color on start. We // listen for the response here, then initialize our styles accordingly. case tea.BackgroundColorMsg: m.styles = newStyles(msg.IsDark()) return m, nil case tea.KeyPressMsg: switch msg.String() { case "q", "esc", "ctrl+c": m.aborted = true return m, tea.Quit case "enter": m.chosen = true return m, tea.Quit case "left", "right", "h", "l": m.yes = !m.yes case "y": m.yes = true m.chosen = true return m, tea.Quit case "n": m.yes = false m.chosen = true return m, tea.Quit } } return m, nil } func (m model) View() tea.View { var v tea.View if m.styles == nil { // We haven't received tea.BackgroundColorMsg yet. Don't worry, it'll // be here in a flash. return v } if m.chosen || m.aborted { // We're about to exit, so wipe the UI. return v } var ( s = m.styles y = "Yes" n = "No" ) if m.yes { y = s.activeButton.Render(y) n = s.inactiveButton.Render(n) } else { y = s.inactiveButton.Render(y) n = s.activeButton.Render(n) } content := s.frame.Render( lipgloss.JoinVertical(lipgloss.Center, s.paragraph.Render( s.text.Render("Are you sure you want to eat that ")+ s.keyword.Render("moderatly ripe")+ s.text.Render(" banana?"), ), y+" "+n, ), ) v.SetContent(content) return v } func main() { m, err := tea.NewProgram(model{}).Run() if err != nil { fmt.Fprintf(os.Stderr, "Uh oh: %v", err) os.Exit(1) } if m := m.(model); m.chosen { if m.yes { fmt.Println("Are you sure? It's not ripe yet.") } else { fmt.Println("Well, alright. It was probably good, though.") } } } ================================================ FILE: examples/color/standalone/main.go ================================================ // This example illustrates how to detect the terminal's background color and // choose either light or dark colors accordingly when using Lip Gloss in a. // standalone fashion, i.e. independent of Bubble Tea. // // For an example of how to do this in a Bubble Tea program, see the // 'bubbletea' example. package main import ( "os" "charm.land/lipgloss/v2" ) func main() { // Query for the background color. We only need to do this once, and only // when using Lip Gloss standalone. // // In Bubble Tea listen for tea.BackgroundColorMsg in your Update. hasDarkBG := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) // Create a new helper function for choosing either a light or dark color // based on the detected background color. lightDark := lipgloss.LightDark(hasDarkBG) // Define some styles. adaptive.Color() can be used to choose the // appropriate light or dark color based on the detected background color. frameStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lightDark(lipgloss.Color("#C5ADF9"), lipgloss.Color("#864EFF"))). Padding(1, 3). Margin(1, 3) paragraphStyle := lipgloss.NewStyle(). Width(40). MarginBottom(1). Align(lipgloss.Center) textStyle := lipgloss.NewStyle(). Foreground(lightDark(lipgloss.Color("#696969"), lipgloss.Color("#bdbdbd"))) keywordStyle := lipgloss.NewStyle(). Foreground(lightDark(lipgloss.Color("#37CD96"), lipgloss.Color("#22C78A"))). Bold(true) activeButton := lipgloss.NewStyle(). Padding(0, 3). Background(lipgloss.Color("#FF6AD2")). Foreground(lipgloss.Color("#FFFCC2")) inactiveButton := activeButton. Background(lightDark(lipgloss.Color("#988F95"), lipgloss.Color("#978692"))). Foreground(lightDark(lipgloss.Color("#FDFCE3"), lipgloss.Color("#FBFAE7"))) // Build layout. text := paragraphStyle.Render( textStyle.Render("Are you sure you want to eat that ") + keywordStyle.Render("moderatly ripe") + textStyle.Render(" banana?"), ) buttons := activeButton.Render("Yes") + " " + inactiveButton.Render("No") block := frameStyle.Render( lipgloss.JoinVertical(lipgloss.Center, text, buttons), ) // Print the block to stdout. It's important to use Lip Gloss's print // functions to ensure that colors are downsampled correctly. If output // isn't a TTY (i.e. we're logging to a file) colors will be stripped // entirely. // // Note that in Bubble Tea downsampling happens automatically. lipgloss.Println(block) } ================================================ FILE: examples/compat/bubbletea/main.go ================================================ package main import ( "fmt" "os" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/compat" ) var ( frameColor = compat.AdaptiveColor{Light: lipgloss.Color("#C5ADF9"), Dark: lipgloss.Color("#864EFF")} textColor = compat.AdaptiveColor{Light: lipgloss.Color("#696969"), Dark: lipgloss.Color("#bdbdbd")} keywordColor = compat.AdaptiveColor{Light: lipgloss.Color("#37CD96"), Dark: lipgloss.Color("#22C78A")} inactiveBgColor = compat.AdaptiveColor{Light: lipgloss.Color("#988F95"), Dark: lipgloss.Color("#978692")} inactiveFgColor = compat.AdaptiveColor{Light: lipgloss.Color("#FDFCE3"), Dark: lipgloss.Color("#FBFAE7")} ) // Style definitions. type styles struct { frame, paragraph, text, keyword, activeButton, inactiveButton lipgloss.Style } // Styles are initialized based on the background color of the terminal. func newStyles() (s styles) { // Define some styles. adaptive.Color() can be used to choose the // appropriate light or dark color based on the detected background color. s.frame = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(frameColor). Padding(1, 3). Margin(1, 3) s.paragraph = lipgloss.NewStyle(). Width(40). MarginBottom(1). Align(lipgloss.Center) s.text = lipgloss.NewStyle(). Foreground(textColor) s.keyword = lipgloss.NewStyle(). Foreground(keywordColor). Bold(true) s.activeButton = lipgloss.NewStyle(). Padding(0, 3). Background(lipgloss.Color("#FF6AD2")). Foreground(lipgloss.Color("#FFFCC2")) s.inactiveButton = s.activeButton. Background(inactiveBgColor). Foreground(inactiveFgColor) return s } type model struct { styles styles yes bool chosen bool aborted bool } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: switch msg.String() { case "q", "esc", "ctrl+c": m.aborted = true return m, tea.Quit case "enter": m.chosen = true return m, tea.Quit case "left", "right", "h", "l": m.yes = !m.yes case "y": m.yes = true m.chosen = true return m, tea.Quit case "n": m.yes = false m.chosen = true return m, tea.Quit } } return m, nil } func (m model) View() tea.View { var v tea.View if m.chosen || m.aborted { // We're about to exit, so wipe the UI. return v } var ( s = m.styles y = "Yes" n = "No" ) if m.yes { y = s.activeButton.Render(y) n = s.inactiveButton.Render(n) } else { y = s.inactiveButton.Render(y) n = s.activeButton.Render(n) } content := s.frame.Render( lipgloss.JoinVertical(lipgloss.Center, s.paragraph.Render( s.text.Render("Are you sure you want to eat that ")+ s.keyword.Render("moderatly ripe")+ s.text.Render(" banana?"), ), y+" "+n, ), ) v.SetContent(content) return v } func main() { initialModel := model{ yes: true, styles: newStyles(), } m, err := tea.NewProgram(initialModel).Run() if err != nil { fmt.Fprintf(os.Stderr, "Uh oh: %v", err) os.Exit(1) } if m := m.(model); m.chosen { if m.yes { fmt.Println("Are you sure? It's not ripe yet.") } else { fmt.Println("Well, alright. It was probably good, though.") } } } ================================================ FILE: examples/compat/standalone/main.go ================================================ // This example illustrates how to detect the terminal's background color and // choose either light or dark colors accordingly when using Lip Gloss in a. // standalone fashion, i.e. independent of Bubble Tea. // // For an example of how to do this in a Bubble Tea program, see the // 'bubbletea' example. package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/compat" ) var ( frameColor = compat.AdaptiveColor{Light: lipgloss.Color("#C5ADF9"), Dark: lipgloss.Color("#864EFF")} textColor = compat.AdaptiveColor{Light: lipgloss.Color("#696969"), Dark: lipgloss.Color("#bdbdbd")} keywordColor = compat.AdaptiveColor{Light: lipgloss.Color("#37CD96"), Dark: lipgloss.Color("#22C78A")} inactiveBgColor = compat.AdaptiveColor{Light: lipgloss.Color("#988F95"), Dark: lipgloss.Color("#978692")} inactiveFgColor = compat.AdaptiveColor{Light: lipgloss.Color("#FDFCE3"), Dark: lipgloss.Color("#FBFAE7")} ) func main() { // Define some styles. adaptive.Color() can be used to choose the // appropriate light or dark color based on the detected background color. frameStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(frameColor). Padding(1, 3). Margin(1, 3) paragraphStyle := lipgloss.NewStyle(). Width(40). MarginBottom(1). Align(lipgloss.Center) textStyle := lipgloss.NewStyle(). Foreground(textColor) keywordStyle := lipgloss.NewStyle(). Foreground(keywordColor). Bold(true) activeButton := lipgloss.NewStyle(). Padding(0, 3). Background(lipgloss.Color("#FF6AD2")). Foreground(lipgloss.Color("#FFFCC2")) inactiveButton := activeButton. Background(inactiveBgColor). Foreground(inactiveFgColor) // Build layout. text := paragraphStyle.Render( textStyle.Render("Are you sure you want to eat that ") + keywordStyle.Render("moderatly ripe") + textStyle.Render(" banana?"), ) buttons := activeButton.Render("Yes") + " " + inactiveButton.Render("No") block := frameStyle.Render( lipgloss.JoinVertical(lipgloss.Center, text, buttons), ) // Print the block to stdout. It's important to use Lip Gloss's print // functions to ensure that colors are downsampled correctly. If output // isn't a TTY (i.e. we're logging to a file) colors will be stripped // entirely. // // Note that in Bubble Tea downsampling happens automatically. lipgloss.Println(block) } ================================================ FILE: examples/go.mod ================================================ module examples go 1.24.3 toolchain go1.24.4 replace charm.land/lipgloss/v2 => ../ require ( charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251201184111-551c60ee5a5c charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7 github.com/charmbracelet/colorprofile v0.4.2 github.com/charmbracelet/ssh v0.0.0-20241211182756-4fe22b0f1b7c github.com/charmbracelet/wish/v2 v2.0.0-20251106193208-3cd15da8229f github.com/charmbracelet/x/exp/charmtone v0.0.0-20250627134340-c144409e381c github.com/charmbracelet/x/term v0.2.2 github.com/rivo/uniseg v0.4.7 ) require ( github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/charmbracelet/keygen v0.5.1 // indirect github.com/charmbracelet/log/v2 v2.0.0-20251106192421-eb64aaa963a0 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/conpty v0.1.0 // indirect github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.5.0 // indirect github.com/creack/pty v1.1.21 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.41.0 // indirect ) // replace with log v2 replace github.com/charmbracelet/log => github.com/charmbracelet/log v0.4.1-0.20241010222913-47ce960d4847 ================================================ FILE: examples/go.sum ================================================ charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251201184111-551c60ee5a5c h1:Jn9nugUf2ddyARHsA79zsWl7szy7dV7HpBj645Sp6DU= charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251201184111-551c60ee5a5c/go.mod h1:BLGnNsQA++rg5IEiTVeLix8AKte880DQWjc8Afs3Nw8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= github.com/charmbracelet/keygen v0.5.1 h1:zBkkYPtmKDVTw+cwUyY6ZwGDhRxXkEp0Oxs9sqMLqxI= github.com/charmbracelet/keygen v0.5.1/go.mod h1:zznJVmK/GWB6dAtjluqn2qsttiCBhA5MZSiwb80fcHw= github.com/charmbracelet/log/v2 v2.0.0-20251106192421-eb64aaa963a0 h1:lxHzxsHd4P7o7+5D5OcEItYkQ1xY3ovNg8Dc5ftd3rI= github.com/charmbracelet/log/v2 v2.0.0-20251106192421-eb64aaa963a0/go.mod h1:Q7oMtlboDPnnrYiJDXNwdWmJblOmuOnycPKczlVju6I= github.com/charmbracelet/ssh v0.0.0-20241211182756-4fe22b0f1b7c h1:treQxMBdI2PaD4eOYfFux8stfCkUxhuUxaqGcxKqVpI= github.com/charmbracelet/ssh v0.0.0-20241211182756-4fe22b0f1b7c/go.mod h1:CY1xbl2z+ZeBmNWItKZyxx0zgDgnhmR57+DTsHOobJ4= github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI= github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI= github.com/charmbracelet/wish/v2 v2.0.0-20251106193208-3cd15da8229f h1:yR3ru/zfVX4cnyhs5GPL1dxArAtxL/IZzJ9/mt1IoeI= github.com/charmbracelet/wish/v2 v2.0.0-20251106193208-3cd15da8229f/go.mod h1:YW+dfIwHgy7eKZqSffA+Fx9EEW2YyXKUGkAdijsvpGI= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/exp/charmtone v0.0.0-20250627134340-c144409e381c h1:2GELBLPgfSbHU53bsQhR9XIgNuVZ6w+Rz8RWV5Lq+A4= github.com/charmbracelet/x/exp/charmtone v0.0.0-20250627134340-c144409e381c/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: examples/layout/main.go ================================================ package main // This example demonstrates various Lip Gloss style and layout features. import ( "fmt" "image/color" "os" "strings" "charm.land/lipgloss/v2" "github.com/charmbracelet/x/term" "github.com/rivo/uniseg" ) const ( // In real life situations we'd adjust the document to fit the width we've // detected. In the case of this example we're hardcoding the width, and // later using the detected width only to truncate in order to avoid jaggy // wrapping. width = 96 // How wide to render various columns in the layout. columnWidth = 30 ) var ( // Whether the detected background color is dark. We detect this at app start. hasDarkBG bool // A helper function for choosing either a light or dark color based on the // detected background color. We create this at app start. lightDark lipgloss.LightDarkFunc ) func main() { // Detect the background color. hasDarkBG = lipgloss.HasDarkBackground(os.Stdin, os.Stdout) // Create a new helper function for choosing either a light or dark color // based on the detected background color. lightDark = lipgloss.LightDark(hasDarkBG) // Style definitions. var ( // General. subtle = lightDark(lipgloss.Color("#D9DCCF"), lipgloss.Color("#383838")) highlight = lightDark(lipgloss.Color("#874BFD"), lipgloss.Color("#7D56F4")) special = lightDark(lipgloss.Color("#43BF6D"), lipgloss.Color("#73F59F")) divider = lipgloss.NewStyle(). SetString("•"). Padding(0, 1). Foreground(subtle). String() url = lipgloss.NewStyle().Foreground(special).Render // Tabs. activeTabBorder = lipgloss.Border{ Top: "─", Bottom: " ", Left: "│", Right: "│", TopLeft: "╭", TopRight: "╮", BottomLeft: "┘", BottomRight: "└", } tabBorder = lipgloss.Border{ Top: "─", Bottom: "─", Left: "│", Right: "│", TopLeft: "╭", TopRight: "╮", BottomLeft: "┴", BottomRight: "┴", } tab = lipgloss.NewStyle(). Border(tabBorder, true). BorderForeground(highlight). Padding(0, 1) activeTab = tab.Border(activeTabBorder, true) tabGap = tab. BorderTop(false). BorderLeft(false). BorderRight(false) // Title. titleStyle = lipgloss.NewStyle(). MarginLeft(1). MarginRight(5). Padding(0, 1). Italic(true). Foreground(lipgloss.Color("#FFF7DB")). SetString("Lip Gloss") descStyle = lipgloss.NewStyle().MarginTop(1) infoStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderTop(true). BorderForeground(subtle) // Dialog. dialogBoxStyle = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("#874BFD")). Padding(1, 0). BorderTop(true). BorderLeft(true). BorderRight(true). BorderBottom(true) buttonStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFF7DB")). Background(lipgloss.Color("#888B7E")). Padding(0, 3). MarginTop(1) activeButtonStyle = buttonStyle. Foreground(lipgloss.Color("#FFF7DB")). Background(lipgloss.Color("#F25D94")). MarginRight(2). Underline(true) // List. list = lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), false, true, false, false). BorderForeground(subtle). MarginRight(1). Height(8). Width(width / 3) listHeader = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderBottom(true). BorderForeground(subtle). MarginRight(2). Render listItem = lipgloss.NewStyle().PaddingLeft(2).Render checkMark = lipgloss.NewStyle().SetString("✓"). Foreground(special). PaddingRight(1). String() listDone = func(s string) string { return checkMark + lipgloss.NewStyle(). Strikethrough(true). Foreground(lightDark(lipgloss.Color("#969B86"), lipgloss.Color("#696969"))). Render(s) } // Paragraphs/History. historyStyle = lipgloss.NewStyle(). Align(lipgloss.Left). Foreground(lipgloss.Color("#FAFAFA")). Background(highlight). Margin(1, 3, 0, 0). Padding(1, 2). Height(19). Width(columnWidth) // Status Bar. statusNugget = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFFDF5")). Padding(0, 1) statusBarStyle = lipgloss.NewStyle(). Foreground(lightDark(lipgloss.Color("#343433"), lipgloss.Color("#C1C6B2"))). Background(lightDark(lipgloss.Color("#D9DCCF"), lipgloss.Color("#353533"))) statusStyle = lipgloss.NewStyle(). Inherit(statusBarStyle). Foreground(lipgloss.Color("#FFFDF5")). Background(lipgloss.Color("#FF5F87")). Padding(0, 1). MarginRight(1) encodingStyle = statusNugget. Background(lipgloss.Color("#A550DF")). Align(lipgloss.Right) statusText = lipgloss.NewStyle().Inherit(statusBarStyle) fishCakeStyle = statusNugget.Background(lipgloss.Color("#6124DF")) // Floating thing. floatingStyle = lipgloss.NewStyle(). Italic(true). Foreground(lipgloss.Color("#FFF7DB")). Background(lipgloss.Color("#F25D94")). Padding(1, 6). Align(lipgloss.Center) // Page. docStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2) ) physicalWidth, _, _ := term.GetSize(os.Stdout.Fd()) doc := strings.Builder{} // Tabs. { 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. { var ( colors = colorGrid(1, 5) title strings.Builder ) for i, v := range colors { const offset = 2 fmt.Fprint(&title, titleStyle.MarginLeft(i*offset).Background(v[0])) if i < len(colors)-1 { title.WriteRune('\n') } } desc := lipgloss.JoinVertical(lipgloss.Left, descStyle.Render("Style Definitions for Nice Terminal Layouts"), infoStyle.Render("From Charm"+divider+url("https://github.com/charmbracelet/lipgloss")), ) row := lipgloss.JoinHorizontal(lipgloss.Top, title.String(), desc) doc.WriteString(row + "\n\n") } // Dialog. { okButton := activeButtonStyle.Render("Yes") cancelButton := buttonStyle.Render("Maybe") grad := applyGradient( lipgloss.NewStyle(), "Are you sure you want to eat marmalade?", lipgloss.Color("#EDFF82"), lipgloss.Color("#F25D94"), ) question := lipgloss.NewStyle(). Width(50). Align(lipgloss.Center). Render(grad) buttons := lipgloss.JoinHorizontal(lipgloss.Top, okButton, cancelButton) ui := lipgloss.JoinVertical(lipgloss.Center, question, buttons) dialog := lipgloss.Place(width, 9, lipgloss.Center, lipgloss.Center, dialogBoxStyle.Render(ui), lipgloss.WithWhitespaceChars("猫咪"), lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(subtle)), ) doc.WriteString(dialog + "\n\n") } // Color grid. colors := func() string { colors := colorGrid(14, 8) b := strings.Builder{} for _, x := range colors { for _, y := range x { s := lipgloss.NewStyle().SetString(" ").Background(y) b.WriteString(s.String()) } b.WriteRune('\n') } return b.String() }() lists := lipgloss.JoinHorizontal(lipgloss.Top, list.Render( lipgloss.JoinVertical(lipgloss.Left, listHeader("Citrus Fruits to Try"), listDone("Grapefruit"), listDone("Yuzu"), listItem("Citron"), listItem("Kumquat"), listItem("Pomelo"), ), ), list.Render( lipgloss.JoinVertical(lipgloss.Left, listHeader("Actual Lip Gloss Vendors"), listItem("Glossier"), listItem("Claire‘s Boutique"), listDone("Nyx"), listItem("Mac"), listDone("Milk"), ), ), ) doc.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, lists, lipgloss.NewStyle().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. { w := lipgloss.Width lightDarkState := "Light" if hasDarkBG { lightDarkState = "Dark" } statusKey := statusStyle.Render("STATUS") encoding := encodingStyle.Render("UTF-8") fishCake := fishCakeStyle.Render("🍥 Fish Cake") statusVal := statusText. Width(width - w(statusKey) - w(encoding) - w(fishCake)). Render("Ravishingly " + lightDarkState + "!") bar := lipgloss.JoinHorizontal(lipgloss.Top, statusKey, statusVal, encoding, fishCake, ) doc.WriteString(statusBarStyle.Width(width).Render(bar)) } if physicalWidth > 0 { docStyle = docStyle.MaxWidth(physicalWidth) } // Render the document. document := docStyle.Render(doc.String()) // Surprise! Composite some bonus content on top of the document. modal := floatingStyle.Render("Now with Compositing!") layers := []*lipgloss.Layer{ lipgloss.NewLayer(document), lipgloss.NewLayer(modal).X(58).Y(44), } comp := lipgloss.NewCompositor(layers...) // 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. lipgloss.Println(comp.Render()) } // colorGrid blends colors from 4 corner quadrants, into a box region. func colorGrid(xSteps, ySteps int) [][]color.Color { leftColors := lipgloss.Blend1D(ySteps, lipgloss.Color("#F25D94"), lipgloss.Color("#643AFF")) rightColors := lipgloss.Blend1D(ySteps, lipgloss.Color("#EDFF82"), lipgloss.Color("#14F9D5")) grid := make([][]color.Color, ySteps) for y := range ySteps { rowColors := lipgloss.Blend1D(xSteps, leftColors[y], rightColors[y]) grid[y] = make([]color.Color, xSteps) for x := range xSteps { grid[y][x] = rowColors[x] } } return grid } // applyGradient applies a gradient to the given string. func applyGradient(base lipgloss.Style, input string, from, to color.Color) string { // We want to get the graphemes of the input string, which is the number of // characters as a human would see them. // // We definitely don't want to use len(), because that returns the // bytes. The rune count would get us closer but there are times, like with // emojis, where the rune count is greater than the number of actual // characters. g := uniseg.NewGraphemes(input) var chars []string for g.Next() { chars = append(chars, g.Str()) } gradient := lipgloss.Blend1D(len(chars), from, to) var output strings.Builder for i, char := range chars { output.WriteString(base.Foreground(gradient[i]).Render(char)) } return output.String() } ================================================ FILE: examples/list/duckduckgoose/main.go ================================================ package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/list" ) func duckDuckGooseEnumerator(items list.Items, i int) string { if items.At(i).Value() == "Goose" { return "Honk →" } return " " } func main() { enumStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#00d787")).MarginRight(1) itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("255")) l := list.New("Duck", "Duck", "Duck", "Goose", "Duck"). ItemStyle(itemStyle). EnumeratorStyle(enumStyle). Enumerator(duckDuckGooseEnumerator) lipgloss.Println(l) } ================================================ FILE: examples/list/glow/main.go ================================================ package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/list" ) type Document struct { Name string Time string } var faint = lipgloss.NewStyle().Faint(true) func (d Document) String() string { return d.Name + "\n" + faint.Render(d.Time) } var docs = []Document{ {"README.md", "2 minutes ago"}, {"Example.md", "1 hour ago"}, {"secrets.md", "1 week ago"}, } const selected = 1 func main() { baseStyle := lipgloss.NewStyle(). MarginBottom(1). MarginLeft(1) dimColor := lipgloss.Color("250") hightlightColor := lipgloss.Color("#EE6FF8") l := list.New(). Enumerator(func(_ list.Items, i int) string { if i == selected { return "│\n│" } return " " }). ItemStyleFunc(func(_ list.Items, i int) lipgloss.Style { st := baseStyle if selected == i { return st.Foreground(hightlightColor) } return st.Foreground(dimColor) }). EnumeratorStyleFunc(func(_ list.Items, i int) lipgloss.Style { if selected == i { return lipgloss.NewStyle().Foreground(hightlightColor) } return lipgloss.NewStyle().Foreground(dimColor) }) for _, d := range docs { l.Item(d.String()) } lipgloss.Print("\n", l, "\n") } ================================================ FILE: examples/list/grocery/main.go ================================================ package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/list" ) var purchased = []string{ "Bananas", "Barley", "Cashews", "Coconut Milk", "Dill", "Eggs", "Fish Cake", "Leeks", "Papaya", } func groceryEnumerator(items list.Items, i int) string { for _, p := range purchased { if items.At(i).Value() == p { return "✓" } } return "•" } var dimEnumStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("240")). MarginRight(1) var highlightedEnumStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("10")). MarginRight(1) func enumStyleFunc(items list.Items, i int) lipgloss.Style { for _, p := range purchased { if items.At(i).Value() == p { return highlightedEnumStyle } } return dimEnumStyle } func itemStyleFunc(items list.Items, i int) lipgloss.Style { itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("255")) for _, p := range purchased { if items.At(i).Value() == p { return itemStyle.Strikethrough(true) } } return itemStyle } func main() { l := list.New( "Artichoke", "Baking Flour", "Bananas", "Barley", "Bean Sprouts", "Cashew Apple", "Cashews", "Coconut Milk", "Curry Paste", "Currywurst", "Dill", "Dragonfruit", "Dried Shrimp", "Eggs", "Fish Cake", "Furikake", "Jicama", "Kohlrabi", "Leeks", "Lentils", "Licorice Root", ). Enumerator(groceryEnumerator). EnumeratorStyleFunc(enumStyleFunc). ItemStyleFunc(itemStyleFunc) lipgloss.Println(l) } ================================================ FILE: examples/list/roman/main.go ================================================ package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/list" ) func main() { enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1) itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("255")).MarginRight(1) l := list.New( "Glossier", "Claire’s Boutique", "Nyx", "Mac", "Milk", ). Enumerator(list.Roman). EnumeratorStyle(enumeratorStyle). ItemStyle(itemStyle) lipgloss.Println(l) } ================================================ FILE: examples/list/simple/main.go ================================================ package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/list" ) func main() { l := list.New( "A", "B", "C", list.New( "D", "E", "F", ).Enumerator(list.Roman), "G", ) lipgloss.Println(l) } ================================================ FILE: examples/list/sublist/main.go ================================================ package main import ( "os" "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/list" "charm.land/lipgloss/v2/table" ) func main() { hasDarkBG := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) lightDark := lipgloss.LightDark(hasDarkBG) purple := lipgloss.NewStyle(). Foreground(lipgloss.Color("99")). MarginRight(1) pink := lipgloss.NewStyle(). Foreground(lipgloss.Color("212")). MarginRight(1) base := lipgloss.NewStyle(). MarginBottom(1). MarginLeft(1) faint := lipgloss.NewStyle().Faint(true) dim := lipgloss.Color("250") highlight := lipgloss.Color("#EE6FF8") special := lightDark(lipgloss.Color("#43BF6D"), lipgloss.Color("#73F59F")) checklistEnumStyle := func(items list.Items, index int) lipgloss.Style { switch index { case 1, 2, 4: return lipgloss.NewStyle(). Foreground(special). PaddingRight(1) default: return lipgloss.NewStyle().PaddingRight(1) } } checklistEnum := func(items list.Items, index int) string { switch index { case 1, 2, 4: return "✓" default: return "•" } } checklistStyle := func(items list.Items, index int) lipgloss.Style { switch index { case 1, 2, 4: return lipgloss.NewStyle(). Strikethrough(true). Foreground(lightDark(lipgloss.Color("#969B86"), lipgloss.Color("#696969"))) default: return lipgloss.NewStyle() } } gradient := lipgloss.Blend1D(5, lipgloss.Color("#F25D94"), lipgloss.Color("#643AFF")) titleStyle := lipgloss.NewStyle(). Italic(true). Foreground(lipgloss.Color("#FFF7DB")) lipglossStyleFunc := func(items list.Items, index int) lipgloss.Style { if index == items.Length()-1 { return titleStyle.Padding(1, 2).Margin(0, 0, 1, 0).MaxWidth(20).Background(gradient[index]) } return titleStyle.Padding(0, 5-index, 0, index+2).MaxWidth(20).Background(gradient[index]) } 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." l := list.New(). EnumeratorStyle(purple). Item("Lip Gloss"). Item("Blush"). Item("Eye Shadow"). Item("Mascara"). Item("Foundation"). Item( list.New(). EnumeratorStyle(pink). Item("Citrus Fruits to Try"). Item( list.New(). ItemStyleFunc(checklistStyle). EnumeratorStyleFunc(checklistEnumStyle). Enumerator(checklistEnum). Item("Grapefruit"). Item("Yuzu"). Item("Citron"). Item("Kumquat"). Item("Pomelo"), ). Item("Actual Lip Gloss Vendors"). Item( list.New(). ItemStyleFunc(checklistStyle). EnumeratorStyleFunc(checklistEnumStyle). Enumerator(checklistEnum). Item("Glossier"). Item("Claire‘s Boutique"). Item("Nyx"). Item("Mac"). Item("Milk"). Item( list.New(). EnumeratorStyle(purple). Enumerator(list.Dash). ItemStyleFunc(lipglossStyleFunc). Item("Lip Gloss"). Item("Lip Gloss"). Item("Lip Gloss"). Item("Lip Gloss"). Item( list.New(). EnumeratorStyle(lipgloss.NewStyle().Foreground(gradient[4]).MarginRight(1)). Item("\nStyle Definitions for Nice Terminal Layouts\n─────"). Item("From Charm"). Item("https://github.com/charmbracelet/lipgloss"). Item( list.New(). EnumeratorStyle(lipgloss.NewStyle().Foreground(gradient[3]).MarginRight(1)). Item("Emperors: Julio-Claudian dynasty"). Item( lipgloss.NewStyle().Padding(1).Render( list.New( "Augustus", "Tiberius", "Caligula", "Claudius", "Nero", ).Enumerator(list.Roman).String(), ), ). Item( lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#FAFAFA")). Background(lipgloss.Color("#7D56F4")). AlignHorizontal(lipgloss.Center). AlignVertical(lipgloss.Center). Padding(1, 3). Margin(0, 1, 1, 1). Width(40). Render(history), ). Item( table.New(). Width(30). BorderStyle(purple.MarginRight(0)). StyleFunc(func(row, col int) lipgloss.Style { style := lipgloss.NewStyle() if col == 0 { style = style.Align(lipgloss.Center) } else { style = style.Align(lipgloss.Right).PaddingRight(2) } if row == 0 { return style.Bold(true).Align(lipgloss.Center).PaddingRight(0) } return style.Faint(true) }). Headers("ITEM", "QUANTITY"). Row("Apple", "6"). Row("Banana", "10"). Row("Orange", "2"). Row("Strawberry", "12"), ). Item("Documents"). Item( list.New(). Enumerator(func(_ list.Items, i int) string { if i == 1 { return "│\n│" } return " " }). ItemStyleFunc(func(_ list.Items, i int) lipgloss.Style { if i == 1 { return base.Foreground(highlight) } return base.Foreground(dim) }). EnumeratorStyleFunc(func(_ list.Items, i int) lipgloss.Style { if i == 1 { return lipgloss.NewStyle().Foreground(highlight) } return lipgloss.NewStyle().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_™") lipgloss.Println(l) } ================================================ FILE: examples/ssh/main.go ================================================ package main // This example demonstrates how to use a colorprofile to accurately detect // client terminal color capabilities for Lip Gloss rendering with Wish, a // package for building custom SSH servers. // // For details on wish see: https://github.com/charmbracelet/wish/ import ( "fmt" "log" "strings" "charm.land/lipgloss/v2" "github.com/charmbracelet/colorprofile" "github.com/charmbracelet/ssh" "github.com/charmbracelet/wish/v2" ) // Available styles. type styles struct { bold lipgloss.Style faint lipgloss.Style italic lipgloss.Style underline lipgloss.Style strikethrough lipgloss.Style red lipgloss.Style green lipgloss.Style yellow lipgloss.Style blue lipgloss.Style magenta lipgloss.Style cyan lipgloss.Style gray lipgloss.Style } // Create new styles. func makeStyles() styles { return styles{ bold: lipgloss.NewStyle().SetString("bold").Bold(true), faint: lipgloss.NewStyle().SetString("faint").Faint(true), italic: lipgloss.NewStyle().SetString("italic").Italic(true), underline: lipgloss.NewStyle().SetString("underline").Underline(true), strikethrough: lipgloss.NewStyle().SetString("strikethrough").Strikethrough(true), red: lipgloss.NewStyle().SetString("red").Foreground(lipgloss.Color("#E88388")), green: lipgloss.NewStyle().SetString("green").Foreground(lipgloss.Color("#A8CC8C")), yellow: lipgloss.NewStyle().SetString("yellow").Foreground(lipgloss.Color("#DBAB79")), blue: lipgloss.NewStyle().SetString("blue").Foreground(lipgloss.Color("#71BEF2")), magenta: lipgloss.NewStyle().SetString("magenta").Foreground(lipgloss.Color("#D290E4")), cyan: lipgloss.NewStyle().SetString("cyan").Foreground(lipgloss.Color("#66C2CD")), gray: lipgloss.NewStyle().SetString("gray").Foreground(lipgloss.Color("#B9BFCA")), } } // Handle SSH requests. func handler(next ssh.Handler) ssh.Handler { return func(sess ssh.Session) { pty, _, active := sess.Pty() if !active { next(sess) return } environ := sess.Environ() environ = append(environ, fmt.Sprintf("TERM=%s", pty.Term)) output := colorprofile.NewWriter(pty.Slave, environ) width := pty.Window.Width // Initialize new styles. styles := makeStyles() str := strings.Builder{} fmt.Fprintf(&str, "\n\nProfile: %s\n%s %s %s %s %s", colorprofile.Detect(pty.Slave, environ), styles.bold, styles.faint, styles.italic, styles.underline, styles.strikethrough, ) fmt.Fprintf(&str, "\n%s %s %s %s %s %s %s", styles.red, styles.green, styles.yellow, styles.blue, styles.magenta, styles.cyan, styles.gray, ) fmt.Fprintf(&str, "\n%s %s %s %s %s %s %s\n\n", styles.red, styles.green, styles.yellow, styles.blue, styles.magenta, styles.cyan, styles.gray, ) hasDarkBG := lipgloss.HasDarkBackground(pty.Slave, pty.Slave) lightDark := lipgloss.LightDark(hasDarkBG) fmt.Fprintf(&str, "%s %s\n\n", styles.bold.UnsetString().Render("Has dark background?"), func() string { if hasDarkBG { return "Yep." } return "Nope!" }(), ) block := lipgloss.Place(width, lipgloss.Height(str.String()), lipgloss.Center, lipgloss.Center, str.String(), lipgloss.WithWhitespaceChars("/"), lipgloss.WithWhitespaceStyle( lipgloss.NewStyle().Foreground(lightDark( lipgloss.ANSIColor(250), lipgloss.ANSIColor(236), ))), ) // Render to client. output.WriteString(block) next(sess) } } func main() { port := 3456 s, err := wish.NewServer( ssh.AllocatePty(), wish.WithAddress(fmt.Sprintf(":%d", port)), wish.WithHostKeyPath("ssh_example"), wish.WithMiddleware(handler), ) if err != nil { log.Fatal(err) } log.Printf("SSH server listening on port %d", port) log.Printf("To connect from your local machine run: ssh localhost -p %d", port) if err := s.ListenAndServe(); err != nil { log.Fatal(err) } } ================================================ FILE: examples/table/ansi/main.go ================================================ package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/table" ) func main() { s := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render t := table.New() t.Row("Bubble Tea", s("Milky")) t.Row("Milk Tea", s("Also milky")) t.Row("Actual milk", s("Milky as well")) lipgloss.Println(t.Render()) } ================================================ FILE: examples/table/chess/main.go ================================================ package main import ( "strings" "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/table" ) func main() { labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("241")) board := [][]string{ {"♜", "♞", "♝", "♛", "♚", "♝", "♞", "♜"}, {"♟", "♟", "♟", "♟", "♟", "♟", "♟", "♟"}, {" ", " ", " ", " ", " ", " ", " ", " "}, {" ", " ", " ", " ", " ", " ", " ", " "}, {" ", " ", " ", " ", " ", " ", " ", " "}, {" ", " ", " ", " ", " ", " ", " ", " "}, {"♙", "♙", "♙", "♙", "♙", "♙", "♙", "♙"}, {"♖", "♘", "♗", "♕", "♔", "♗", "♘", "♖"}, } t := table.New(). Border(lipgloss.NormalBorder()). BorderRow(true). BorderColumn(true). Rows(board...). StyleFunc(func(row, col int) lipgloss.Style { return lipgloss.NewStyle().Padding(0, 1) }) ranks := labelStyle.Render(strings.Join([]string{" A", "B", "C", "D", "E", "F", "G", "H "}, " ")) files := labelStyle.Render(strings.Join([]string{" 1", "2", "3", "4", "5", "6", "7", "8 "}, "\n\n ")) lipgloss.Println( lipgloss.JoinVertical( lipgloss.Right, lipgloss.JoinHorizontal(lipgloss.Center, files, t.Render()), ranks, ) + "\n", ) } ================================================ FILE: examples/table/demo.tape ================================================ Output table.gif Set Height 900 Set Width 1600 Set Padding 80 Set FontSize 42 Hide Type "go build -o table" Enter Ctrl+L Show Sleep 0.5s Type "clear && ./table" Sleep 0.5s Enter Sleep 1s Screenshot "table.png" Sleep 1s Hide Type "rm table" Enter Show Sleep 1s ================================================ FILE: examples/table/languages/main.go ================================================ package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/table" ) const ( purple = "99" gray = "245" lightGray = "241" ) func main() { var ( // HeaderStyle is the lipgloss style used for the table headers. HeaderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(purple)).Bold(true).Align(lipgloss.Center) // CellStyle is the base lipgloss style used for the table rows. CellStyle = lipgloss.NewStyle().Padding(0, 1).Width(14) // OddRowStyle is the lipgloss style used for odd-numbered table rows. OddRowStyle = CellStyle.Foreground(lipgloss.Color(gray)) // EvenRowStyle is the lipgloss style used for even-numbered table rows. EvenRowStyle = CellStyle.Foreground(lipgloss.Color(lightGray)) // BorderStyle is the lipgloss style used for the table border. BorderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(purple)) ) rows := [][]string{ {"Chinese", "您好", "你好"}, {"Japanese", "こんにちは", "やあ"}, {"Arabic", "أهلين", "أهلا"}, {"Russian", "Здравствуйте", "Привет"}, {"Spanish", "Hola", "¿Qué tal?"}, } t := table.New(). Border(lipgloss.ThickBorder()). BorderStyle(BorderStyle). StyleFunc(func(row, col int) lipgloss.Style { var style lipgloss.Style switch { case row == table.HeaderRow: return HeaderStyle case row%2 == 0: style = EvenRowStyle default: style = OddRowStyle } // Make the second column a little wider. if col == 1 { style = style.Width(22) } // Arabic is a right-to-left language, so right align the text. if row < len(rows) && rows[row][0] == "Arabic" && col != 0 { style = style.Align(lipgloss.Right) } return style }). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...) t.Row("English", "You look absolutely fabulous.", "How's it going?") lipgloss.Println(t) } ================================================ FILE: examples/table/mindy/main.go ================================================ package main import ( "fmt" "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/table" ) func main() { labelStyle := lipgloss.NewStyle().Width(3).Align(lipgloss.Right) swatchStyle := lipgloss.NewStyle().Width(6) data := [][]string{} for i := 0; i < 13; i += 8 { data = append(data, makeRow(i, i+5)) } data = append(data, makeEmptyRow()) for i := 6; i < 15; i += 8 { data = append(data, makeRow(i, i+1)) } data = append(data, makeEmptyRow()) for i := 16; i < 231; i += 6 { data = append(data, makeRow(i, i+5)) } data = append(data, makeEmptyRow()) for i := 232; i < 256; i += 6 { data = append(data, makeRow(i, i+5)) } t := table.New(). Border(lipgloss.HiddenBorder()). Rows(data...). StyleFunc(func(row, col int) lipgloss.Style { color := lipgloss.Color(fmt.Sprint(data[row][col-col%2])) switch col % 2 { case 0: return labelStyle.Foreground(color) default: return swatchStyle.Background(color) } }) lipgloss.Println(t) } const rowLength = 12 func makeRow(start, end int) []string { var row []string for i := start; i <= end; i++ { row = append(row, fmt.Sprint(i)) row = append(row, "") } for i := len(row); i < rowLength; i++ { row = append(row, "") } return row } func makeEmptyRow() []string { return makeRow(0, -1) } ================================================ FILE: examples/table/pokemon/main.go ================================================ package main import ( "fmt" "image/color" "strings" "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/table" ) func main() { baseStyle := lipgloss.NewStyle().Padding(0, 1) headerStyle := baseStyle.Foreground(lipgloss.Color("252")).Bold(true) selectedStyle := baseStyle.Foreground(lipgloss.Color("#01BE85")).Background(lipgloss.Color("#00432F")) typeColors := map[string]color.Color{ "Bug": lipgloss.Color("#D7FF87"), "Electric": lipgloss.Color("#FDFF90"), "Fire": lipgloss.Color("#FF7698"), "Flying": lipgloss.Color("#FF87D7"), "Grass": lipgloss.Color("#75FBAB"), "Ground": lipgloss.Color("#FF875F"), "Normal": lipgloss.Color("#929292"), "Poison": lipgloss.Color("#7D5AFC"), "Water": lipgloss.Color("#00E2C7"), } dimTypeColors := map[string]color.Color{ "Bug": lipgloss.Color("#97AD64"), "Electric": lipgloss.Color("#FCFF5F"), "Fire": lipgloss.Color("#BA5F75"), "Flying": lipgloss.Color("#C97AB2"), "Grass": lipgloss.Color("#59B980"), "Ground": lipgloss.Color("#C77252"), "Normal": lipgloss.Color("#727272"), "Poison": lipgloss.Color("#634BD0"), "Water": lipgloss.Color("#439F8E"), } headers := []string{"#", "Name", "Type 1", "Type 2", "Japanese", "Official Rom."} data := [][]string{ {"1", "Bulbasaur", "Grass", "Poison", "フシギダネ", "Fushigidane"}, {"2", "Ivysaur", "Grass", "Poison", "フシギソウ", "Fushigisou"}, {"3", "Venusaur", "Grass", "Poison", "フシギバナ", "Fushigibana"}, {"4", "Charmander", "Fire", "", "ヒトカゲ", "Hitokage"}, {"5", "Charmeleon", "Fire", "", "リザード", "Lizardo"}, {"6", "Charizard", "Fire", "Flying", "リザードン", "Lizardon"}, {"7", "Squirtle", "Water", "", "ゼニガメ", "Zenigame"}, {"8", "Wartortle", "Water", "", "カメール", "Kameil"}, {"9", "Blastoise", "Water", "", "カメックス", "Kamex"}, {"10", "Caterpie", "Bug", "", "キャタピー", "Caterpie"}, {"11", "Metapod", "Bug", "", "トランセル", "Trancell"}, {"12", "Butterfree", "Bug", "Flying", "バタフリー", "Butterfree"}, {"13", "Weedle", "Bug", "Poison", "ビードル", "Beedle"}, {"14", "Kakuna", "Bug", "Poison", "コクーン", "Cocoon"}, {"15", "Beedrill", "Bug", "Poison", "スピアー", "Spear"}, {"16", "Pidgey", "Normal", "Flying", "ポッポ", "Poppo"}, {"17", "Pidgeotto", "Normal", "Flying", "ピジョン", "Pigeon"}, {"18", "Pidgeot", "Normal", "Flying", "ピジョット", "Pigeot"}, {"19", "Rattata", "Normal", "", "コラッタ", "Koratta"}, {"20", "Raticate", "Normal", "", "ラッタ", "Ratta"}, {"21", "Spearow", "Normal", "Flying", "オニスズメ", "Onisuzume"}, {"22", "Fearow", "Normal", "Flying", "オニドリル", "Onidrill"}, {"23", "Ekans", "Poison", "", "アーボ", "Arbo"}, {"24", "Arbok", "Poison", "", "アーボック", "Arbok"}, {"25", "Pikachu", "Electric", "", "ピカチュウ", "Pikachu"}, {"26", "Raichu", "Electric", "", "ライチュウ", "Raichu"}, {"27", "Sandshrew", "Ground", "", "サンド", "Sand"}, {"28", "Sandslash", "Ground", "", "サンドパン", "Sandpan"}, } CapitalizeHeaders := func(data []string) []string { for i := range data { data[i] = strings.ToUpper(data[i]) } return data } t := table.New(). Border(lipgloss.NormalBorder()). BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("238"))). Headers(CapitalizeHeaders(headers)...). Width(80). Rows(data...). StyleFunc(func(row, col int) lipgloss.Style { if row == table.HeaderRow { return headerStyle } if data[row][1] == "Pikachu" { return selectedStyle } even := row%2 == 0 switch col { case 2, 3: // Type 1 + 2 c := typeColors if even { c = dimTypeColors } color := c[fmt.Sprint(data[row][col])] return baseStyle.Foreground(color) } if even { return baseStyle.Foreground(lipgloss.Color("245")) } return baseStyle.Foreground(lipgloss.Color("252")) }) lipgloss.Println(t) } ================================================ FILE: examples/tree/background/main.go ================================================ package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/tree" ) func main() { darkBg := lipgloss.NewStyle(). Background(lipgloss.Color("0")). Padding(0, 1) headerItemStyle := lipgloss.NewStyle(). Background(lipgloss.Color("#ee6ff8")). Foreground(lipgloss.Color("#ecfe65")). Bold(true). Padding(0, 1) itemStyle := headerItemStyle.Background(lipgloss.Color("0")) t := tree.Root("# Table of Contents"). RootStyle(itemStyle). ItemStyle(itemStyle). EnumeratorStyle(darkBg). IndenterStyle(darkBg). Child( tree.Root("## Chapter 1"). Child("Chapter 1.1"). Child("Chapter 1.2"), ). Child( tree.Root("## Chapter 2"). Child("Chapter 2.1"). Child("Chapter 2.2"), ) lipgloss.Println(t) } ================================================ FILE: examples/tree/files/main.go ================================================ package main import ( "fmt" "os" "path/filepath" "strings" "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/tree" ) func addBranches(root *tree.Tree, path string) error { items, err := os.ReadDir(path) if err != nil { return err } for _, item := range items { if item.IsDir() { // It's a directory. // Skip directories that start with a dot. if strings.HasPrefix(item.Name(), ".") { continue } treeBranch := tree.Root(item.Name()) root.Child(treeBranch) // Recurse. branchPath := filepath.Join(path, item.Name()) if err := addBranches(treeBranch, branchPath); err != nil { return err } } else { // It's a file. // Skip files that start with a dot. if strings.HasPrefix(item.Name(), ".") { continue } root.Child(item.Name()) } } return nil } func main() { enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).PaddingRight(1) itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).Bold(true).PaddingRight(1) pwd, err := os.Getwd() if err != nil { fmt.Fprintf(os.Stderr, "Error getting current working directory: %v\n", err) os.Exit(1) } t := tree.Root(pwd). IndenterStyle(enumeratorStyle). EnumeratorStyle(enumeratorStyle). RootStyle(itemStyle). ItemStyle(itemStyle) if err := addBranches(t, "."); err != nil { fmt.Fprintf(os.Stderr, "Error building tree: %v\n", err) os.Exit(1) } lipgloss.Println(t) } ================================================ FILE: examples/tree/makeup/main.go ================================================ package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/tree" ) func main() { enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1) rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35")) itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")) t := tree. Root("⁜ Makeup"). Child( "Glossier", "Fenty Beauty", tree.New().Child( "Gloss Bomb Universal Lip Luminizer", "Hot Cheeks Velour Blushlighter", ), "Nyx", "Mac", "Milk", ). Enumerator(tree.RoundedEnumerator). EnumeratorStyle(enumeratorStyle). IndenterStyle(enumeratorStyle). RootStyle(rootStyle). ItemStyle(itemStyle) lipgloss.Println(t) } ================================================ FILE: examples/tree/rounded/main.go ================================================ package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/tree" ) func main() { itemStyle := lipgloss.NewStyle().MarginRight(1) enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).MarginRight(1) t := tree.Root("Groceries"). Child( tree.Root("Fruits"). Child( "Blood Orange", "Papaya", "Dragonfruit", "Yuzu", ), tree.Root("Items"). Child( "Cat Food", "Nutella", "Powdered Sugar", ), tree.Root("Veggies"). Child( "Leek", "Artichoke", ), ).ItemStyle(itemStyle). EnumeratorStyle(enumeratorStyle). Enumerator(tree.RoundedEnumerator). IndenterStyle(enumeratorStyle) lipgloss.Println(t) } ================================================ FILE: examples/tree/selection/main.go ================================================ package main import ( "fmt" "path" "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/tree" ) const selected = "/Users/bash/.config/doom-emacs" type styles struct { base, container, dir, selected, dimmed, toggle lipgloss.Style } func defaultStyles() styles { var s styles s.base = lipgloss.NewStyle() s.container = s.base. Margin(1, 2). Padding(1, 0) s.dir = s.base. Inline(true) s.toggle = s.base. Foreground(lipgloss.Color("5")). PaddingRight(1) s.selected = s.base. Background(lipgloss.Color("8")). Foreground(lipgloss.Color("207")). Bold(true) s.dimmed = s.base. Foreground(lipgloss.Color("241")) return s } type dir struct { name string open bool styles styles } func (d dir) String() string { t := d.styles.toggle.PaddingLeft(1).Render n := d.styles.dir.Render if d.open { return t("▼") + n(d.name) } return t("▶") + n(d.name) } // file implements the Node interface. type file struct { name string styles styles } func (s file) String() string { return path.Base(s.name) } func (s file) Hidden() bool { return false } func (s file) Children() tree.Children { return tree.NodeChildren(nil) } func (s file) Value() string { return s.String() } func (s file) SetValue(val any) { return } func (s file) SetHidden(val bool) { return } func isItemSelected(children tree.Children, index int) bool { child := children.At(index) if file, ok := child.(file); ok && file.name == selected { return true } return false } func itemStyle(children tree.Children, index int) lipgloss.Style { s := defaultStyles() if isItemSelected(children, index) { return s.selected } return s.base } func indenterStyle(children tree.Children, index int) lipgloss.Style { s := defaultStyles() if isItemSelected(children, index) { return s.dimmed.Background(s.selected.GetBackground()) } return s.dimmed } func main() { s := defaultStyles() t := tree.Root(dir{"~/charm", true, s}). Child( dir{"ayman", false, s}, tree.Root(dir{"bash", true, s}). Child( file{"/Users/bash/.config/doom-emacs", s}, ), tree.Root(dir{"carlos", true, s}). Child( tree.Root(dir{"emotes", true, s}). Child( file{"/home/caarlos0/Pictures/chefkiss.png", s}, file{"/home/caarlos0/Pictures/kekw.png", s}, ), ), dir{"maas", false, s}, ). Width(30). Indenter(Indenter). Enumerator(Enumerator). EnumeratorStyleFunc(indenterStyle). IndenterStyleFunc(indenterStyle). ItemStyleFunc(itemStyle) fmt.Println(s.container.Render(t.String())) } func Enumerator(children tree.Children, index int) string { return " │ " } func Indenter(children tree.Children, index int) string { return " │ " } ================================================ FILE: examples/tree/simple/main.go ================================================ package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/tree" ) func main() { t := tree.Root("."). Child("macOS"). Child( tree.New(). Root("Linux"). Child("NixOS"). Child("Arch Linux (btw)"). Child("Void Linux"), ). Child( tree.New(). Root("BSD"). Child("FreeBSD"). Child("OpenBSD"), ) lipgloss.Println(t) } ================================================ FILE: examples/tree/styles/main.go ================================================ package main import ( "fmt" "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/tree" ) func main() { purple := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1) pink := lipgloss.NewStyle().Foreground(lipgloss.Color("212")).MarginRight(1) t := tree.New(). Child( "Glossier", "Claire’s Boutique", tree.Root("Nyx"). Child("Lip Gloss", "Foundation"). EnumeratorStyle(pink). IndenterStyle(purple), "Mac", "Milk", ). EnumeratorStyle(purple). IndenterStyle(purple) fmt.Println(t) } ================================================ FILE: examples/tree/toggle/main.go ================================================ package main import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/tree" ) type styles struct { base, block, pink, dir, toggle, file lipgloss.Style } func defaultStyles() styles { var s styles s.base = lipgloss.NewStyle(). Background(lipgloss.Color("57")). Foreground(lipgloss.Color("225")) s.block = s.base. Padding(1, 3). Margin(1, 3). Width(40) s.pink = s.base. Foreground(lipgloss.Color("212")). PaddingRight(1) s.dir = s.base. Inline(true) s.toggle = s.base. Foreground(lipgloss.Color("207")). PaddingRight(1) s.file = s.base return s } type dir struct { name string open bool styles styles } func (d dir) String() string { t := d.styles.toggle.Render n := d.styles.dir.Render if d.open { return t("▼") + n(d.name) } return t("▶") + n(d.name) } type file struct { name string styles styles } func (s file) String() string { return s.styles.file.Render(s.name) } func main() { s := defaultStyles() t := tree.Root(dir{"~/charm", true, s}). Enumerator(tree.RoundedEnumerator). IndenterStyle(s.pink). EnumeratorStyle(s.pink). Child( dir{"ayman", false, s}, tree.Root(dir{"bash", true, s}). Child( tree.Root(dir{"tools", true, s}). Child( file{"zsh", s}, file{"doom-emacs", s}, ), ), tree.Root(dir{"carlos", true, s}). Child( tree.Root(dir{"emotes", true, s}). Child( file{"chefkiss.png", s}, file{"kekw.png", s}, ), ), dir{"maas", false, s}, ) lipgloss.Println(s.block.Render(t.String())) } ================================================ FILE: get.go ================================================ package lipgloss import ( "image/color" "strings" "github.com/charmbracelet/x/ansi" ) // GetBold returns the style's bold value. If no value is set false is returned. func (s Style) GetBold() bool { return s.getAsBool(boldKey, false) } // GetItalic returns the style's italic value. If no value is set false is // returned. func (s Style) GetItalic() bool { return s.getAsBool(italicKey, false) } // GetUnderline returns the style's underline value. If no value is set false is // returned. func (s Style) GetUnderline() bool { return s.ul != UnderlineNone } // GetUnderlineStyle returns the style's underline style. If no value is set // UnderlineNone is returned. func (s Style) GetUnderlineStyle() Underline { return s.ul } // GetUnderlineColor returns the style's underline color. If no value is set // NoColor{} is returned. func (s Style) GetUnderlineColor() color.Color { return s.getAsColor(underlineColorKey) } // GetStrikethrough returns the style's strikethrough value. If no value is set false // is returned. func (s Style) GetStrikethrough() bool { return s.getAsBool(strikethroughKey, false) } // GetReverse returns the style's reverse value. If no value is set false is // returned. func (s Style) GetReverse() bool { return s.getAsBool(reverseKey, false) } // GetBlink returns the style's blink value. If no value is set false is // returned. func (s Style) GetBlink() bool { return s.getAsBool(blinkKey, false) } // GetFaint returns the style's faint value. If no value is set false is // returned. func (s Style) GetFaint() bool { return s.getAsBool(faintKey, false) } // GetForeground returns the style's foreground color. If no value is set // NoColor{} is returned. func (s Style) GetForeground() color.Color { return s.getAsColor(foregroundKey) } // GetBackground returns the style's background color. If no value is set // NoColor{} is returned. func (s Style) GetBackground() color.Color { return s.getAsColor(backgroundKey) } // GetWidth returns the style's width setting. If no width is set 0 is // returned. func (s Style) GetWidth() int { return s.getAsInt(widthKey) } // GetHeight returns the style's height setting. If no height is set 0 is // returned. func (s Style) GetHeight() int { return s.getAsInt(heightKey) } // GetAlign returns the style's implicit horizontal alignment setting. // If no alignment is set Position.Left is returned. func (s Style) GetAlign() Position { v := s.getAsPosition(alignHorizontalKey) if v == Position(0) { return Left } return v } // GetAlignHorizontal returns the style's implicit horizontal alignment setting. // If no alignment is set Position.Left is returned. func (s Style) GetAlignHorizontal() Position { v := s.getAsPosition(alignHorizontalKey) if v == Position(0) { return Left } return v } // GetAlignVertical returns the style's implicit vertical alignment setting. // If no alignment is set Position.Top is returned. func (s Style) GetAlignVertical() Position { v := s.getAsPosition(alignVerticalKey) if v == Position(0) { return Top } return v } // GetPadding returns the style's top, right, bottom, and left padding values, // in that order. 0 is returned for unset values. func (s Style) GetPadding() (top, right, bottom, left int) { return s.getAsInt(paddingTopKey), s.getAsInt(paddingRightKey), s.getAsInt(paddingBottomKey), s.getAsInt(paddingLeftKey) } // GetPaddingTop returns the style's top padding. If no value is set 0 is // returned. func (s Style) GetPaddingTop() int { return s.getAsInt(paddingTopKey) } // GetPaddingRight returns the style's right padding. If no value is set 0 is // returned. func (s Style) GetPaddingRight() int { return s.getAsInt(paddingRightKey) } // GetPaddingBottom returns the style's bottom padding. If no value is set 0 is // returned. func (s Style) GetPaddingBottom() int { return s.getAsInt(paddingBottomKey) } // GetPaddingLeft returns the style's left padding. If no value is set 0 is // returned. func (s Style) GetPaddingLeft() int { return s.getAsInt(paddingLeftKey) } // GetPaddingChar returns the style's padding character. If no value is set a // space is returned. func (s Style) GetPaddingChar() rune { char := s.getAsRune(paddingCharKey) if char == 0 { return ' ' } return char } // GetHorizontalPadding returns the style's left and right padding. Unset // values are measured as 0. func (s Style) GetHorizontalPadding() int { return s.getAsInt(paddingLeftKey) + s.getAsInt(paddingRightKey) } // GetVerticalPadding returns the style's top and bottom padding. Unset values // are measured as 0. func (s Style) GetVerticalPadding() int { return s.getAsInt(paddingTopKey) + s.getAsInt(paddingBottomKey) } // GetColorWhitespace returns the style's whitespace coloring setting. If no // value is set false is returned. func (s Style) GetColorWhitespace() bool { return s.getAsBool(colorWhitespaceKey, false) } // GetMargin returns the style's top, right, bottom, and left margins, in that // order. 0 is returned for unset values. func (s Style) GetMargin() (top, right, bottom, left int) { return s.getAsInt(marginTopKey), s.getAsInt(marginRightKey), s.getAsInt(marginBottomKey), s.getAsInt(marginLeftKey) } // GetMarginTop returns the style's top margin. If no value is set 0 is // returned. func (s Style) GetMarginTop() int { return s.getAsInt(marginTopKey) } // GetMarginRight returns the style's right margin. If no value is set 0 is // returned. func (s Style) GetMarginRight() int { return s.getAsInt(marginRightKey) } // GetMarginBottom returns the style's bottom margin. If no value is set 0 is // returned. func (s Style) GetMarginBottom() int { return s.getAsInt(marginBottomKey) } // GetMarginLeft returns the style's left margin. If no value is set 0 is // returned. func (s Style) GetMarginLeft() int { return s.getAsInt(marginLeftKey) } // GetMarginChar returns the style's padding character. If no value is set a // space is returned. func (s Style) GetMarginChar() rune { char := s.getAsRune(marginCharKey) if char == 0 { return ' ' } return char } // GetHorizontalMargins returns the style's left and right margins. Unset // values are measured as 0. func (s Style) GetHorizontalMargins() int { return s.getAsInt(marginLeftKey) + s.getAsInt(marginRightKey) } // GetVerticalMargins returns the style's top and bottom margins. Unset values // are measured as 0. func (s Style) GetVerticalMargins() int { return s.getAsInt(marginTopKey) + s.getAsInt(marginBottomKey) } // GetBorder returns the style's border style (type Border) and value for the // top, right, bottom, and left in that order. If no value is set for the // border style, Border{} is returned. For all other unset values false is // returned. func (s Style) GetBorder() (b Border, top, right, bottom, left bool) { return s.getBorderStyle(), s.getAsBool(borderTopKey, false), s.getAsBool(borderRightKey, false), s.getAsBool(borderBottomKey, false), s.getAsBool(borderLeftKey, false) } // GetBorderStyle returns the style's border style (type Border). If no value // is set Border{} is returned. func (s Style) GetBorderStyle() Border { return s.getBorderStyle() } // GetBorderTop returns the style's top border setting. If no value is set // false is returned. func (s Style) GetBorderTop() bool { return s.getAsBool(borderTopKey, false) } // GetBorderRight returns the style's right border setting. If no value is set // false is returned. func (s Style) GetBorderRight() bool { return s.getAsBool(borderRightKey, false) } // GetBorderBottom returns the style's bottom border setting. If no value is // set false is returned. func (s Style) GetBorderBottom() bool { return s.getAsBool(borderBottomKey, false) } // GetBorderLeft returns the style's left border setting. If no value is // set false is returned. func (s Style) GetBorderLeft() bool { return s.getAsBool(borderLeftKey, false) } // GetBorderTopForeground returns the style's border top foreground color. If // no value is set NoColor{} is returned. func (s Style) GetBorderTopForeground() color.Color { return s.getAsColor(borderTopForegroundKey) } // GetBorderRightForeground returns the style's border right foreground color. // If no value is set NoColor{} is returned. func (s Style) GetBorderRightForeground() color.Color { return s.getAsColor(borderRightForegroundKey) } // GetBorderBottomForeground returns the style's border bottom foreground // color. If no value is set NoColor{} is returned. func (s Style) GetBorderBottomForeground() color.Color { return s.getAsColor(borderBottomForegroundKey) } // GetBorderLeftForeground returns the style's border left foreground // color. If no value is set NoColor{} is returned. func (s Style) GetBorderLeftForeground() color.Color { return s.getAsColor(borderLeftForegroundKey) } // GetBorderForegroundBlend returns the style's border blend foreground // colors. If no value is set, nil is returned. func (s Style) GetBorderForegroundBlend() []color.Color { return s.getAsColors(borderForegroundBlendKey) } // GetBorderForegroundBlendOffset returns the style's border blend offset. If no // value is set, 0 is returned. func (s Style) GetBorderForegroundBlendOffset() int { return s.getAsInt(borderForegroundBlendOffsetKey) } // GetBorderTopBackground returns the style's border top background color. If // no value is set NoColor{} is returned. func (s Style) GetBorderTopBackground() color.Color { return s.getAsColor(borderTopBackgroundKey) } // GetBorderRightBackground returns the style's border right background color. // If no value is set NoColor{} is returned. func (s Style) GetBorderRightBackground() color.Color { return s.getAsColor(borderRightBackgroundKey) } // GetBorderBottomBackground returns the style's border bottom background // color. If no value is set NoColor{} is returned. func (s Style) GetBorderBottomBackground() color.Color { return s.getAsColor(borderBottomBackgroundKey) } // GetBorderLeftBackground returns the style's border left background // color. If no value is set NoColor{} is returned. func (s Style) GetBorderLeftBackground() color.Color { return s.getAsColor(borderLeftBackgroundKey) } // GetBorderTopWidth returns the width of the top border. If borders contain // runes of varying widths, the widest rune is returned. If no border exists on // the top edge, 0 is returned. // // Deprecated: This function simply calls Style.GetBorderTopSize. func (s Style) GetBorderTopWidth() int { return s.GetBorderTopSize() } // GetBorderTopSize returns the width of the top border. If borders contain // runes of varying widths, the widest rune is returned. If no border exists on // the top edge, 0 is returned. func (s Style) GetBorderTopSize() int { if s.isBorderStyleSetWithoutSides() { return 1 } if !s.getAsBool(borderTopKey, false) { return 0 } return s.getBorderStyle().GetTopSize() } // GetBorderLeftSize returns the width of the left border. If borders contain // runes of varying widths, the widest rune is returned. If no border exists on // the left edge, 0 is returned. func (s Style) GetBorderLeftSize() int { if s.isBorderStyleSetWithoutSides() { return 1 } if !s.getAsBool(borderLeftKey, false) { return 0 } return s.getBorderStyle().GetLeftSize() } // GetBorderBottomSize returns the width of the bottom border. If borders // contain runes of varying widths, the widest rune is returned. If no border // exists on the left edge, 0 is returned. func (s Style) GetBorderBottomSize() int { if s.isBorderStyleSetWithoutSides() { return 1 } if !s.getAsBool(borderBottomKey, false) { return 0 } return s.getBorderStyle().GetBottomSize() } // GetBorderRightSize returns the width of the right border. If borders // contain runes of varying widths, the widest rune is returned. If no border // exists on the right edge, 0 is returned. func (s Style) GetBorderRightSize() int { if s.isBorderStyleSetWithoutSides() { return 1 } if !s.getAsBool(borderRightKey, false) { return 0 } return s.getBorderStyle().GetRightSize() } // GetHorizontalBorderSize returns the width of the horizontal borders. If // borders contain runes of varying widths, the widest rune is returned. If no // border exists on the horizontal edges, 0 is returned. func (s Style) GetHorizontalBorderSize() int { return s.GetBorderLeftSize() + s.GetBorderRightSize() } // GetVerticalBorderSize returns the width of the vertical borders. If // borders contain runes of varying widths, the widest rune is returned. If no // border exists on the vertical edges, 0 is returned. func (s Style) GetVerticalBorderSize() int { return s.GetBorderTopSize() + s.GetBorderBottomSize() } // GetInline returns the style's inline setting. If no value is set false is // returned. func (s Style) GetInline() bool { return s.getAsBool(inlineKey, false) } // GetMaxWidth returns the style's max width setting. If no value is set 0 is // returned. func (s Style) GetMaxWidth() int { return s.getAsInt(maxWidthKey) } // GetMaxHeight returns the style's max height setting. If no value is set 0 is // returned. func (s Style) GetMaxHeight() int { return s.getAsInt(maxHeightKey) } // GetTabWidth returns the style's tab width setting. If no value is set 4 is // returned which is the implicit default. func (s Style) GetTabWidth() int { return s.getAsInt(tabWidthKey) } // GetUnderlineSpaces returns whether or not the style is set to underline // spaces. If not value is set false is returned. func (s Style) GetUnderlineSpaces() bool { return s.getAsBool(underlineSpacesKey, false) } // GetStrikethroughSpaces returns whether or not the style is set to strikethrough // spaces. If not value is set false is returned. func (s Style) GetStrikethroughSpaces() bool { return s.getAsBool(strikethroughSpacesKey, false) } // GetHorizontalFrameSize returns the sum of the style's horizontal margins, padding // and border widths. // // Provisional: this method may be renamed. func (s Style) GetHorizontalFrameSize() int { return s.GetHorizontalMargins() + s.GetHorizontalPadding() + s.GetHorizontalBorderSize() } // GetVerticalFrameSize returns the sum of the style's vertical margins, padding // and border widths. // // Provisional: this method may be renamed. func (s Style) GetVerticalFrameSize() int { return s.GetVerticalMargins() + s.GetVerticalPadding() + s.GetVerticalBorderSize() } // GetFrameSize returns the sum of the margins, padding and border width for // both the horizontal and vertical margins. func (s Style) GetFrameSize() (x, y int) { return s.GetHorizontalFrameSize(), s.GetVerticalFrameSize() } // GetTransform returns the transform set on the style. If no transform is set // nil is returned. func (s Style) GetTransform() func(string) string { return s.getAsTransform(transformKey) } // GetHyperlink returns the hyperlink along with its parameters. If no // hyperlink is set, empty strings are returned. func (s Style) GetHyperlink() (link, params string) { if s.isSet(linkKey) { link = s.link } if s.isSet(linkParamsKey) { params = s.linkParams } return } // Returns whether or not the given property is set. func (s Style) isSet(k propKey) bool { return s.props.has(k) } func (s Style) getAsRune(k propKey) rune { if !s.isSet(k) { return 0 } switch k { //nolint:exhaustive case paddingCharKey: return s.paddingChar case marginCharKey: return s.marginChar } return 0 } func (s Style) getAsBool(k propKey, defaultVal bool) bool { if !s.isSet(k) { return defaultVal } return s.attrs&int(k) != 0 } func (s Style) getAsColors(k propKey) (colors []color.Color) { if !s.isSet(k) { return nil } switch k { //nolint:exhaustive case borderForegroundBlendKey: return s.borderBlendFgColor } return nil } func (s Style) getAsColor(k propKey) color.Color { if !s.isSet(k) { return noColor } var c color.Color switch k { //nolint:exhaustive case foregroundKey: c = s.fgColor case backgroundKey: c = s.bgColor case marginBackgroundKey: c = s.marginBgColor case borderTopForegroundKey: c = s.borderTopFgColor case borderRightForegroundKey: c = s.borderRightFgColor case borderBottomForegroundKey: c = s.borderBottomFgColor case borderLeftForegroundKey: c = s.borderLeftFgColor case borderTopBackgroundKey: c = s.borderTopBgColor case borderRightBackgroundKey: c = s.borderRightBgColor case borderBottomBackgroundKey: c = s.borderBottomBgColor case borderLeftBackgroundKey: c = s.borderLeftBgColor case underlineColorKey: c = s.ulColor } if c != nil { return c } return noColor } func (s Style) getAsInt(k propKey) int { if !s.isSet(k) { return 0 } switch k { //nolint:exhaustive case widthKey: return s.width case heightKey: return s.height case paddingTopKey: return s.paddingTop case paddingRightKey: return s.paddingRight case paddingBottomKey: return s.paddingBottom case paddingLeftKey: return s.paddingLeft case marginTopKey: return s.marginTop case marginRightKey: return s.marginRight case marginBottomKey: return s.marginBottom case marginLeftKey: return s.marginLeft case borderForegroundBlendOffsetKey: return s.borderForegroundBlendOffset case maxWidthKey: return s.maxWidth case maxHeightKey: return s.maxHeight case tabWidthKey: return s.tabWidth } return 0 } func (s Style) getAsPosition(k propKey) Position { if !s.isSet(k) { return Position(0) } switch k { //nolint:exhaustive case alignHorizontalKey: return s.alignHorizontal case alignVerticalKey: return s.alignVertical } return Position(0) } func (s Style) getBorderStyle() Border { if !s.isSet(borderStyleKey) { return noBorder } return s.borderStyle } func (s Style) getAsTransform(propKey) func(string) string { if !s.isSet(transformKey) { return nil } return s.transform } // Split a string into lines, additionally returning the size of the widest // line. func getLines(s string) (lines []string, widest int) { s = strings.ReplaceAll(s, "\t", " ") s = strings.ReplaceAll(s, "\r\n", "\n") lines = strings.Split(s, "\n") for _, l := range lines { w := ansi.StringWidth(l) if widest < w { widest = w } } return lines, widest } // isBorderStyleSetWithoutSides returns true if the border style is set but no // sides are set. This is used to determine if the border should be rendered by // default. func (s Style) isBorderStyleSetWithoutSides() bool { var ( border = s.getBorderStyle() topSet = s.isSet(borderTopKey) rightSet = s.isSet(borderRightKey) bottomSet = s.isSet(borderBottomKey) leftSet = s.isSet(borderLeftKey) ) return border != noBorder && !(topSet || rightSet || bottomSet || leftSet) //nolint:staticcheck } ================================================ FILE: go.mod ================================================ module charm.land/lipgloss/v2 retract v2.0.0-beta1 // We add a "." after the "beta" in the version number. go 1.25.0 require ( github.com/aymanbagabas/go-udiff v0.4.1 github.com/charmbracelet/colorprofile v0.4.3 github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 github.com/charmbracelet/x/ansi v0.11.6 github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f github.com/charmbracelet/x/term v0.2.2 github.com/clipperhouse/displaywidth v0.11.0 github.com/lucasb-eyer/go-colorful v1.3.0 github.com/rivo/uniseg v0.4.7 golang.org/x/sys v0.42.0 ) require ( github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/sync v0.18.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI= github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= ================================================ FILE: join.go ================================================ package lipgloss import ( "math" "strings" "github.com/charmbracelet/x/ansi" ) // 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 // at the bottom. // // If you just want to align to the top, center or bottom you may as well just // use the helper constants Top, Center, and Bottom. // // Example: // // blockB := "...\n...\n..." // blockA := "...\n...\n...\n...\n..." // // // Join 20% from the top // str := lipgloss.JoinHorizontal(0.2, blockA, blockB) // // // Join on the top edge // str := lipgloss.JoinHorizontal(lipgloss.Top, blockA, blockB) func JoinHorizontal(pos Position, strs ...string) string { if len(strs) == 0 { return "" } if len(strs) == 1 { return strs[0] } var ( // Groups of strings broken into multiple lines blocks = make([][]string, len(strs)) // Max line widths for the above text blocks maxWidths = make([]int, len(strs)) // Height of the tallest block maxHeight int ) // Break text blocks into lines and get max widths for each text block for i, str := range strs { blocks[i], maxWidths[i] = getLines(str) if len(blocks[i]) > maxHeight { maxHeight = len(blocks[i]) } } // Add extra lines to make each side the same height for i := range blocks { if len(blocks[i]) >= maxHeight { continue } extraLines := make([]string, maxHeight-len(blocks[i])) switch pos { case Top: blocks[i] = append(blocks[i], extraLines...) case Bottom: blocks[i] = append(extraLines, blocks[i]...) default: // Somewhere in the middle n := len(extraLines) split := int(math.Round(float64(n) * pos.value())) top := n - split bottom := n - top blocks[i] = append(extraLines[top:], blocks[i]...) blocks[i] = append(blocks[i], extraLines[bottom:]...) } } // Merge lines var b strings.Builder for i := range blocks[0] { // remember, all blocks have the same number of members now for j, block := range blocks { b.WriteString(block[i]) // Also make lines the same length b.WriteString(strings.Repeat(" ", maxWidths[j]-ansi.StringWidth(block[i]))) } if i < len(blocks[0])-1 { b.WriteRune('\n') } } return b.String() } // JoinVertical is a utility function for vertically joining two potentially // multi-lined strings along a horizontal axis. The first argument is the // position, with 0 being all the way to the left and 1 being all the way to // the right. // // If you just want to align to the left, right or center you may as well just // use the helper constants Left, Center, and Right. // // Example: // // blockB := "...\n...\n..." // blockA := "...\n...\n...\n...\n..." // // // Join 20% from the top // str := lipgloss.JoinVertical(0.2, blockA, blockB) // // // Join on the right edge // str := lipgloss.JoinVertical(lipgloss.Right, blockA, blockB) func JoinVertical(pos Position, strs ...string) string { if len(strs) == 0 { return "" } if len(strs) == 1 { return strs[0] } var ( blocks = make([][]string, len(strs)) maxWidth int ) for i := range strs { var w int blocks[i], w = getLines(strs[i]) if w > maxWidth { maxWidth = w } } var b strings.Builder for i, block := range blocks { for j, line := range block { w := maxWidth - ansi.StringWidth(line) switch pos { case Left: b.WriteString(line) b.WriteString(strings.Repeat(" ", w)) case Right: b.WriteString(strings.Repeat(" ", w)) b.WriteString(line) default: // Somewhere in the middle if w < 1 { b.WriteString(line) break } split := int(math.Round(float64(w) * pos.value())) right := w - split left := w - right b.WriteString(strings.Repeat(" ", left)) b.WriteString(line) b.WriteString(strings.Repeat(" ", right)) } // Write a newline as long as we're not on the last line of the // last block. if !(i == len(blocks)-1 && j == len(block)-1) { //nolint:staticcheck b.WriteRune('\n') } } } return b.String() } ================================================ FILE: join_test.go ================================================ package lipgloss import "testing" func TestJoinVertical(t *testing.T) { type test struct { name string result string expected string } tests := []test{ {"pos0", JoinVertical(Left, "A", "BBBB"), "A \nBBBB"}, {"pos1", JoinVertical(Right, "A", "BBBB"), " A\nBBBB"}, {"pos0.25", JoinVertical(0.25, "A", "BBBB"), " A \nBBBB"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.result != test.expected { t.Errorf("Got \n%s\n, expected \n%s\n", test.result, test.expected) } }) } } func TestJoinHorizontal(t *testing.T) { type test struct { name string result string expected string } tests := []test{ {"pos0", JoinHorizontal(Top, "A", "B\nB\nB\nB"), "AB\n B\n B\n B"}, {"pos1", JoinHorizontal(Bottom, "A", "B\nB\nB\nB"), " B\n B\n B\nAB"}, {"pos0.25", JoinHorizontal(0.25, "A", "B\nB\nB\nB"), " B\nAB\n B\n B"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.result != test.expected { t.Errorf("Got \n%s\n, expected \n%s\n", test.result, test.expected) } }) } } ================================================ FILE: layer.go ================================================ package lipgloss import ( "fmt" "image" "slices" uv "github.com/charmbracelet/ultraviolet" ) // Layer represents a visual layer with content and positioning. It's a pure // data structure that defines the layer hierarchy without any computation. type Layer struct { id string content string width, height int x, y, z int layers []*Layer } // NewLayer creates a new [Layer] with the given content and optional child layers. func NewLayer(content string, layers ...*Layer) *Layer { l := &Layer{ content: content, } l.AddLayers(layers...) return l } // GetContent returns the content of the Layer. func (l *Layer) GetContent() string { return l.content } // Width returns the width of the Layer. func (l *Layer) Width() int { return l.width } // Height returns the height of the Layer. func (l *Layer) Height() int { return l.height } // GetID returns the ID of the Layer. func (l *Layer) GetID() string { return l.id } // ID sets the ID of the Layer. func (l *Layer) ID(id string) *Layer { l.id = id return l } // X sets the x-coordinate of the Layer relative to its parent. func (l *Layer) X(x int) *Layer { l.x = x return l } // Y sets the y-coordinate of the Layer relative to its parent. func (l *Layer) Y(y int) *Layer { l.y = y return l } // Z sets the z-index of the Layer relative to its parent. func (l *Layer) Z(z int) *Layer { l.z = z return l } // GetX returns the x-coordinate of the Layer relative to its parent. func (l *Layer) GetX() int { return l.x } // GetY returns the y-coordinate of the Layer relative to its parent. func (l *Layer) GetY() int { return l.y } // GetZ returns the z-index of the Layer relative to its parent. func (l *Layer) GetZ() int { return l.z } // AddLayers adds child layers to the Layer. func (l *Layer) AddLayers(layers ...*Layer) *Layer { for i, layer := range layers { if layer == nil { panic(fmt.Sprintf("layer at index %d is nil", i)) } l.layers = append(l.layers, layer) } area := l.boundsWithOffset(0, 0) l.width = area.Dx() l.height = area.Dy() return l } // GetLayer returns a descendant layer by its ID, or nil if not found. // Layers with empty IDs are skipped. func (l *Layer) GetLayer(id string) *Layer { if id == "" { return nil } if l.id == id { return l } for _, child := range l.layers { if found := child.GetLayer(id); found != nil { return found } } return nil } // MaxZ returns the maximum z-index among this layer and all its descendants. func (l *Layer) MaxZ() int { maxZ := l.z for _, child := range l.layers { childMaxZ := child.MaxZ() if childMaxZ > maxZ { maxZ = childMaxZ } } return maxZ } // boundsWithOffset calculates bounds with parent offset applied. func (l *Layer) boundsWithOffset(parentX, parentY int) image.Rectangle { absX := l.x + parentX absY := l.y + parentY width, height := Width(l.content), Height(l.content) bounds := image.Rectangle{ Min: image.Pt(absX, absY), Max: image.Pt(absX+width, absY+height), } for _, child := range l.layers { bounds = bounds.Union(child.boundsWithOffset(absX, absY)) } return bounds } var _ uv.Drawable = (*Layer)(nil) // Draw draws the content of the layer on the screen at the specified area. func (l *Layer) Draw(scr uv.Screen, area uv.Rectangle) { content := uv.NewStyledString(l.content) content.Draw(scr, area) } // LayerHit represents the result of a hit test on a [Layer]. type LayerHit struct { id string layer *Layer bounds image.Rectangle } // Empty returns true if the LayerHit represents no hit. func (lh LayerHit) Empty() bool { return lh.layer == nil } // ID returns the ID of the hit Layer. func (lh LayerHit) ID() string { return lh.id } // Layer returns the layer that was hit. func (lh LayerHit) Layer() *Layer { return lh.layer } // Bounds returns the bounds of the LayerHit. func (lh LayerHit) Bounds() image.Rectangle { return lh.bounds } // Compositor manages the composition of layers. It flattens a layer hierarchy // once and provides efficient drawing and hit testing operations. All computation // related to layers happens in the Compositor. type Compositor struct { root *Layer layers []compositeLayer index map[string]*Layer bounds image.Rectangle } // compositeLayer holds a flattened layer with its calculated absolute position and bounds. type compositeLayer struct { layer *Layer absX int absY int bounds image.Rectangle } // NewCompositor creates a new Compositor with an internal root layer. Optional // layers can be provided which will be added as children of the root. The layer // hierarchy is flattened and sorted by z-index for efficient rendering and hit testing. func NewCompositor(layers ...*Layer) *Compositor { root := NewLayer("") root.AddLayers(layers...) c := &Compositor{ root: root, index: make(map[string]*Layer), } c.flatten() return c } // AddLayers adds layers to the compositor's root and refreshes the internal state. func (c *Compositor) AddLayers(layers ...*Layer) *Compositor { c.root.AddLayers(layers...) c.flatten() return c } // flatten builds the internal flattened layer list and calculates overall bounds. func (c *Compositor) flatten() { c.layers = nil c.index = make(map[string]*Layer) c.flattenRecursive(c.root, 0, 0) // Sort by absolute z-index (lowest to highest for drawing) slices.SortFunc(c.layers, func(a, b compositeLayer) int { return a.layer.z - b.layer.z }) // Calculate overall bounds if len(c.layers) > 0 { c.bounds = c.layers[0].bounds for i := 1; i < len(c.layers); i++ { c.bounds = c.bounds.Union(c.layers[i].bounds) } } } // flattenRecursive recursively collects all layers with their absolute positions. func (c *Compositor) flattenRecursive(layer *Layer, parentX, parentY int) { absX := layer.x + parentX absY := layer.y + parentY width, height := Width(layer.content), Height(layer.content) bounds := image.Rectangle{ Min: image.Pt(absX, absY), Max: image.Pt(absX+width, absY+height), } c.layers = append(c.layers, compositeLayer{ layer: layer, absX: absX, absY: absY, bounds: bounds, }) // Index layer by ID if it has one if layer.id != "" { c.index[layer.id] = layer } for _, child := range layer.layers { c.flattenRecursive(child, absX, absY) } } // Bounds returns the overall bounds of all layers in the compositor. func (c *Compositor) Bounds() image.Rectangle { return c.bounds } // Draw draws all layers onto the given [uv.Screen] in z-index order. func (c *Compositor) Draw(scr uv.Screen, area image.Rectangle) { for _, cl := range c.layers { if cl.bounds.Overlaps(area) { cl.layer.Draw(scr, cl.bounds) } } } // Hit performs a hit test at the given (x, y) coordinates. If a layer is hit, // it returns the ID of the top-most layer at that point. Layers with empty IDs // are ignored. If no layer is hit, it returns an empty [LayerHit]. func (c *Compositor) Hit(x, y int) LayerHit { var hit LayerHit pt := image.Pt(x, y) // Check from highest z to lowest (reverse order) for i := len(c.layers) - 1; i >= 0; i-- { cl := c.layers[i] if cl.layer.id != "" && pt.In(cl.bounds) { hit.id = cl.layer.id hit.layer = cl.layer hit.bounds = cl.bounds return hit } } return hit } // GetLayer returns a layer by its ID, or nil if not found. // Layers with empty IDs are not indexed and cannot be retrieved. func (c *Compositor) GetLayer(id string) *Layer { if id == "" { return nil } return c.index[id] } // Refresh re-flattens the layer hierarchy. Call this after modifying the layer // tree structure or positions to update the compositor's internal state. func (c *Compositor) Refresh() { c.flatten() } // Render renders the compositor into a styled string. This is a helper // function that creates a temporary canvas, draws the compositor onto it, and // returns the resulting string. func (c *Compositor) Render() string { width, height := c.bounds.Dx(), c.bounds.Dy() canvas := NewCanvas(width, height) return canvas.Compose(c).Render() } ================================================ FILE: lipgloss.go ================================================ // Package lipgloss provides style definitions for nice terminal layouts. Built // with TUIs in mind. package lipgloss ================================================ FILE: list/enumerator.go ================================================ package list import ( "fmt" "strings" ) // Enumerator enumerates a list. Given a list of items and the index of the // current enumeration, it returns the prefix that should be displayed for the // current item. // // For example, a simple Arabic numeral enumeration would be: // // func Arabic(_ Items, i int) string { // return fmt.Sprintf("%d.", i+1) // } // // There are several predefined enumerators: // - Alphabet // - Arabic // - Bullet // - Dash // - Roman // // Or, define your own. type Enumerator func(items Items, index int) string // Indenter indents the children of a tree. // // Indenters allow for displaying nested tree items with connecting borders // to sibling nodes. // // For example, the default indenter would be: // // func TreeIndenter(children Children, index int) string { // if children.Length()-1 == index { // return "│ " // } // // return " " // } type Indenter func(items Items, index int) string // Alphabet is the enumeration for alphabetical listing. // // Example: // a. Foo // b. Bar // c. Baz // d. Qux. func Alphabet(_ Items, i int) string { if i >= abcLen*abcLen+abcLen { return fmt.Sprintf("%c%c%c.", 'A'+i/abcLen/abcLen-1, 'A'+(i/abcLen)%abcLen-1, 'A'+i%abcLen) } if i >= abcLen { return fmt.Sprintf("%c%c.", 'A'+i/abcLen-1, 'A'+(i)%abcLen) } return fmt.Sprintf("%c.", 'A'+i%abcLen) } const abcLen = 26 // Arabic is the enumeration for arabic numerals listing. // // Example: // 1. Foo // 2. Bar // 3. Baz // 4. Qux. func Arabic(_ Items, i int) string { return fmt.Sprintf("%d.", i+1) } // Roman is the enumeration for roman numerals listing. // // Example: // I. Foo // II. Bar // III. Baz // IV. Qux. func Roman(_ Items, i int) string { var ( roman = []string{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"} arabic = []int{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1} result strings.Builder ) for v, value := range arabic { for i >= value-1 { i -= value result.WriteString(roman[v]) } } result.WriteRune('.') return result.String() } // Bullet is the enumeration for bullet listing. // // Example: // • Foo // • Bar // • Baz // • Qux. func Bullet(Items, int) string { return "•" } // Asterisk is an enumeration using asterisks. // // Example: // * Foo // * Bar // * Baz // * Qux. func Asterisk(Items, int) string { return "*" } // Dash is an enumeration using dashes. // // Example: // - Foo // - Bar // - Baz // - Qux. func Dash(Items, int) string { return "-" } ================================================ FILE: list/list.go ================================================ // Package list allows you to build lists, as simple or complicated as you need. // // Simply, define a list with some items and set it's rendering properties, like // enumerator and styling: // // groceries := list.New( // "Bananas", // "Barley", // "Cashews", // "Milk", // list.New( // "Almond Milk" // "Coconut Milk" // "Full Fat Milk" // ) // "Eggs", // "Fish Cake", // "Leeks", // "Papaya", // ) // // fmt.Println(groceries) package list //nolint:revive import ( "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/tree" ) // List represents a list of items that can be displayed. Lists can contain // lists as items, they will be rendered as nested (sub)lists. // // In fact, lists can contain anything as items, like lipgloss.Table or lipgloss.Tree. type List struct{ tree *tree.Tree } // New returns a new list with the given items. // // alphabet := list.New( // "A", // "B", // "C", // "D", // "E", // "F", // ... // ) // // Items can be other lists, trees, tables, rendered markdown; // anything you want, really. func New(items ...any) *List { l := &List{tree: tree.New()} return l.Items(items...). Enumerator(Bullet). Indenter(func(Items, int) string { return " " }) } // Items represents the list items. type Items tree.Children // StyleFunc is the style function that determines the style of an item. // // It takes the list items and index of the list and determines the lipgloss // Style to use for that index. // // Example: // // l := list.New(). // Item("Red"). // Item("Green"). // Item("Blue"). // ItemStyleFunc(func(items list.Items, i int) lipgloss.Style { // switch { // case i == 0: // return RedStyle // case i == 1: // return GreenStyle // case i == 2: // return BlueStyle // } // }) type StyleFunc func(items Items, index int) lipgloss.Style // Hidden returns whether this list is hidden. 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. func (l *List) Hide(hide bool) *List { l.tree.Hide(hide) return l } // Offset sets the start and end offset for the list. // // Example: // l := list.New("A", "B", "C", "D"). // Offset(1, -1) // // fmt.Println(l) // // • B // • C // • D func (l *List) Offset(start, end int) *List { l.tree.Offset(start, end) return l } // Value returns the value of this node. func (l *List) Value() string { return l.tree.Value() } func (l *List) String() string { return l.tree.String() } // EnumeratorStyle sets the enumerator style for all enumerators. // // To set the enumerator style conditionally based on the item value or index, // use [EnumeratorStyleFunc]. func (l *List) EnumeratorStyle(style lipgloss.Style) *List { l.tree.EnumeratorStyle(style) return l } // EnumeratorStyleFunc sets the enumerator style function for the list items. // // Use this to conditionally set different styles based on the current items, // sibling items, or index values (i.e. even or odd). // // Example: // // l := list.New(). // EnumeratorStyleFunc(func(_ list.Items, i int) lipgloss.Style { // if selected == i { // return lipgloss.NewStyle().Foreground(brightPink) // } // return lipgloss.NewStyle() // }) func (l *List) EnumeratorStyleFunc(f StyleFunc) *List { l.tree.EnumeratorStyleFunc(func(children tree.Children, index int) lipgloss.Style { return f(children, index) }) return l } // IndenterStyle sets the enumerator style for all enumerators. // // To set the enumerator style conditionally based on the item value or index, // use [IndenterStyleFunc]. func (l *List) IndenterStyle(style lipgloss.Style) *List { l.tree.IndenterStyle(style) return l } // IndenterStyleFunc sets the enumerator style function for the list items. // // Use this to conditionally set different styles based on the current items, // sibling items, or index values (i.e. even or odd). // // Example: // // l := list.New(). // IndenterStyleFunc(func(_ list.Items, i int) lipgloss.Style { // if selected == i { // return lipgloss.NewStyle().Foreground(brightPink) // } // return lipgloss.NewStyle() // }) func (l *List) IndenterStyleFunc(f StyleFunc) *List { l.tree.IndenterStyleFunc(func(children tree.Children, index int) lipgloss.Style { return f(children, index) }) return l } // Indenter sets the indenter implementation. This is used to change the way // the tree is indented. The default indentor places a border connecting sibling // elements and no border for the last child. // // └── Foo // └── Bar // └── Baz // └── Qux // └── Quux // // You can define your own indenter. // // func ArrowIndenter(children tree.Children, index int) string { // return "→ " // } // // → Foo // → → Bar // → → → Baz // → → → → Qux // → → → → → Quux func (l *List) Indenter(indenter Indenter) *List { l.tree.Indenter( func(children tree.Children, index int) string { return indenter(children, index) }, ) return l } // ItemStyle sets the item style for all items. // // To set the item style conditionally based on the item value or index, // use [ItemStyleFunc]. func (l *List) ItemStyle(style lipgloss.Style) *List { l.tree.ItemStyle(style) return l } // ItemStyleFunc sets the item style function for the list items. // // Use this to conditionally set different styles based on the current items, // sibling items, or index values. // // Example: // // l := list.New(). // ItemStyleFunc(func(_ list.Items, i int) lipgloss.Style { // if selected == i { // return lipgloss.NewStyle().Foreground(brightPink) // } // return lipgloss.NewStyle() // }) func (l *List) ItemStyleFunc(f StyleFunc) *List { l.tree.ItemStyleFunc(func(children tree.Children, index int) lipgloss.Style { return f(children, index) }) return l } // Item appends an item to the list. // // l := list.New(). // Item("Foo"). // Item("Bar"). // Item("Baz") func (l *List) Item(item any) *List { switch item := item.(type) { case *List: l.tree.Child(item.tree) default: l.tree.Child(item) } return l } // Items appends multiple items to the list. // // l := list.New(). // Items("Foo", "Bar", "Baz"), func (l *List) Items(items ...any) *List { for _, item := range items { l.Item(item) } return l } // Enumerator sets the list enumerator. // // There are several predefined enumerators: // • Alphabet // • Arabic // • Bullet // • Dash // • Roman // // Or, define your own. // // func echoEnumerator(items list.Items, i int) string { // return items.At(i).Value() + ". " // } // // l := list.New("Foo", "Bar", "Baz").Enumerator(echoEnumerator) // fmt.Println(l) // // Foo. Foo // Bar. Bar // Baz. Baz func (l *List) Enumerator(enumerator Enumerator) *List { l.tree.Enumerator(func(c tree.Children, i int) string { return enumerator(c, i) }) return l } ================================================ FILE: list/list_test.go ================================================ package list_test import ( "strings" "testing" "unicode" "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/list" "charm.land/lipgloss/v2/tree" "github.com/aymanbagabas/go-udiff" "github.com/charmbracelet/x/exp/golden" ) // XXX: can't write multi-line examples if the underlying string uses // lipgloss.JoinVertical. func TestList(t *testing.T) { l := list.New(). Item("Foo"). Item("Bar"). Item("Baz") golden.RequireEqual(t, []byte(l.String())) } func TestListItems(t *testing.T) { l := list.New(). Items([]string{"Foo", "Bar", "Baz"}) golden.RequireEqual(t, []byte(l.String())) } func TestSublist(t *testing.T) { l := list.New(). Item("Foo"). Item("Bar"). Item(list.New("Hi", "Hello", "Halo").Enumerator(list.Roman)). Item("Qux") golden.RequireEqual(t, []byte(l.String())) } func TestSublistItems(t *testing.T) { l := list.New( "A", "B", "C", list.New( "D", "E", "F", ).Enumerator(list.Roman), "G", ) golden.RequireEqual(t, []byte(l.String())) } func TestComplexSublist(t *testing.T) { style1 := lipgloss.NewStyle(). Foreground(lipgloss.Color("99")). PaddingRight(1) style2 := lipgloss.NewStyle(). Foreground(lipgloss.Color("212")). PaddingRight(1) l := list.New(). Item("Foo"). Item("Bar"). Item(list.New("foo2", "bar2")). Item("Qux"). Item( list.New("aaa", "bbb"). EnumeratorStyle(style1). Enumerator(list.Roman), ). Item("Deep"). Item( list.New(). EnumeratorStyle(style2). IndenterStyle(style2). Enumerator(list.Alphabet). Item("foo"). Item("Deeper"). Item( list.New(). IndenterStyle(style1). EnumeratorStyle(style1). Enumerator(list.Arabic). Item("a"). Item("b"). Item("Even Deeper, inherit parent renderer"). Item( list.New(). Enumerator(list.Asterisk). IndenterStyle(style2). EnumeratorStyle(style2). Item("sus"). Item("d minor"). Item("f#"). Item("One ore level, with another renderer"). Item( list.New(). IndenterStyle(style1). EnumeratorStyle(style1). Enumerator(list.Dash). Item("a\nmultine\nstring"). Item("hoccus poccus"). Item("abra kadabra"). Item("And finally, a tree within all this"). Item( tree.New(). IndenterStyle(style2). EnumeratorStyle(style2). Child("another\nmultine\nstring"). Child("something"). Child("a subtree"). Child( tree.New(). IndenterStyle(style2). EnumeratorStyle(style2). Child("yup"). Child("many itens"). Child("another"), ). Child("hallo"). Child("wunderbar!"), ). Item("this is a tree\nand other obvious statements"), ), ), ). Item("bar"), ). Item("Baz") golden.RequireEqual(t, []byte(l.String())) } func TestMultiline(t *testing.T) { l := list.New(). Item("Item1\nline 2\nline 3"). Item("Item2\nline 2\nline 3"). Item("3") golden.RequireEqual(t, []byte(l.String())) } func TestListIntegers(t *testing.T) { l := list.New(). Item("1"). Item("2"). Item("3") golden.RequireEqual(t, []byte(l.String())) } func TestEnumerators(t *testing.T) { tests := map[string]struct { enumerator list.Enumerator expected string }{ "alphabet": { enumerator: list.Alphabet, expected: ` A. Foo B. Bar C. Baz `, }, "arabic": { enumerator: list.Arabic, expected: ` 1. Foo 2. Bar 3. Baz `, }, "roman": { enumerator: list.Roman, expected: ` I. Foo II. Bar III. Baz `, }, "bullet": { enumerator: list.Bullet, expected: ` • Foo • Bar • Baz `, }, "asterisk": { enumerator: list.Asterisk, expected: ` * Foo * Bar * Baz `, }, "dash": { enumerator: list.Dash, expected: ` - Foo - Bar - Baz `, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { l := list.New(). Enumerator(test.enumerator). Item("Foo"). Item("Bar"). Item("Baz") golden.RequireEqual(t, []byte(l.String())) }) } } func TestEnumeratorsTransform(t *testing.T) { tests := map[string]struct { enumeration list.Enumerator style lipgloss.Style expected string }{ "alphabet lower": { enumeration: list.Alphabet, style: lipgloss.NewStyle().PaddingRight(1).Transform(strings.ToLower), expected: ` a. Foo b. Bar c. Baz `, }, "arabic)": { enumeration: list.Arabic, style: lipgloss.NewStyle().PaddingRight(1).Transform(func(s string) string { return strings.Replace(s, ".", ")", 1) }), expected: ` 1) Foo 2) Bar 3) Baz `, }, "roman within ()": { enumeration: list.Roman, style: lipgloss.NewStyle().Transform(func(s string) string { return "(" + strings.Replace(strings.ToLower(s), ".", "", 1) + ") " }), expected: ` (i) Foo (ii) Bar (iii) Baz `, }, "bullet is dash": { enumeration: list.Bullet, style: lipgloss.NewStyle().Transform(func(s string) string { return "- " // this is better done by replacing the enumerator. }), expected: ` - Foo - Bar - Baz `, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { l := list.New(). EnumeratorStyle(test.style). Enumerator(test.enumeration). Item("Foo"). Item("Bar"). Item("Baz") golden.RequireEqual(t, []byte(l.String())) }) } } func TestBullet(t *testing.T) { tests := []struct { enum list.Enumerator i int exp string }{ {list.Alphabet, 0, "A"}, {list.Alphabet, 25, "Z"}, {list.Alphabet, 26, "AA"}, {list.Alphabet, 51, "AZ"}, {list.Alphabet, 52, "BA"}, {list.Alphabet, 79, "CB"}, {list.Alphabet, 701, "ZZ"}, {list.Alphabet, 702, "AAA"}, {list.Alphabet, 801, "ADV"}, {list.Alphabet, 1000, "ALM"}, {list.Roman, 0, "I"}, {list.Roman, 25, "XXVI"}, {list.Roman, 26, "XXVII"}, {list.Roman, 50, "LI"}, {list.Roman, 100, "CI"}, {list.Roman, 701, "DCCII"}, {list.Roman, 1000, "MI"}, } for _, test := range tests { prefix := test.enum(nil, test.i) bullet := strings.TrimSuffix(prefix, ".") if bullet != test.exp { t.Errorf("expected: %s, got: %s\n", test.exp, bullet) } } } func TestEnumeratorsAlign(t *testing.T) { fooList := strings.Split(strings.TrimSuffix(strings.Repeat("Foo ", 100), " "), " ") l := list.New().Enumerator(list.Roman) for _, f := range fooList { l.Item(f) } golden.RequireEqual(t, []byte(l.String())) } func TestSubListItems2(t *testing.T) { l := list.New().Items( "S", list.New().Items("neovim", "vscode"), "HI", list.New().Items([]string{"vim", "doom emacs"}), "Parent 2", list.New().Item("I like fuzzy socks"), ) golden.RequireEqual(t, []byte(l.String())) } // assertEqual verifies the strings are equal, assuming its terminal output. func assertEqual(tb testing.TB, expected, got string) { tb.Helper() cleanExpected := trimSpace(expected) cleanGot := trimSpace(got) diff := udiff.Unified("expected", "got", cleanExpected, cleanGot) if diff != "" { tb.Fatalf("expected:\n\n%s\n\ngot:\n\n%s\n\ndiff:\n\n%s\n\n", cleanExpected, cleanGot, diff) } } func trimSpace(s string) string { var result []string //nolint: prealloc ss := strings.Split(s, "\n") for i, line := range ss { if strings.TrimSpace(line) == "" && (i == 0 || i == len(ss)-1) { continue } result = append(result, strings.TrimRightFunc(line, unicode.IsSpace)) } return strings.Join(result, "\n") } ================================================ FILE: list/testdata/TestComplexSublist.golden ================================================ • Foo • Bar • foo2 • bar2 • Qux I. aaa II. bbb • Deep A. foo B. Deeper   1. a   2. b   3. Even Deeper, inherit parent renderer     * sus     * d minor     * f#     * One ore level, with another renderer       - a         multine         string       - hoccus poccus       - abra kadabra       - And finally, a tree within all this         ├── another         │  multine         │  string         ├── something         ├── a subtree         │  ├── yup         │  ├── many itens         │  └── another         ├── hallo         └── wunderbar!       - this is a tree         and other obvious statements C. bar • Baz ================================================ FILE: list/testdata/TestEnumerators/alphabet.golden ================================================ A. Foo B. Bar C. Baz ================================================ FILE: list/testdata/TestEnumerators/arabic.golden ================================================ 1. Foo 2. Bar 3. Baz ================================================ FILE: list/testdata/TestEnumerators/asterisk.golden ================================================ * Foo * Bar * Baz ================================================ FILE: list/testdata/TestEnumerators/bullet.golden ================================================ • Foo • Bar • Baz ================================================ FILE: list/testdata/TestEnumerators/dash.golden ================================================ - Foo - Bar - Baz ================================================ FILE: list/testdata/TestEnumerators/roman.golden ================================================ I. Foo II. Bar III. Baz ================================================ FILE: list/testdata/TestEnumeratorsAlign.golden ================================================ I. Foo II. Foo III. Foo IV. Foo V. Foo VI. Foo VII. Foo VIII. Foo IX. Foo X. Foo XI. Foo XII. Foo XIII. Foo XIV. Foo XV. Foo XVI. Foo XVII. Foo XVIII. Foo XIX. Foo XX. Foo XXI. Foo XXII. Foo XXIII. Foo XXIV. Foo XXV. Foo XXVI. Foo XXVII. Foo XXVIII. Foo XXIX. Foo XXX. Foo XXXI. Foo XXXII. Foo XXXIII. Foo XXXIV. Foo XXXV. Foo XXXVI. Foo XXXVII. Foo XXXVIII. Foo XXXIX. Foo XL. Foo XLI. Foo XLII. Foo XLIII. Foo XLIV. Foo XLV. Foo XLVI. Foo XLVII. Foo XLVIII. Foo XLIX. Foo L. Foo LI. Foo LII. Foo LIII. Foo LIV. Foo LV. Foo LVI. Foo LVII. Foo LVIII. Foo LIX. Foo LX. Foo LXI. Foo LXII. Foo LXIII. Foo LXIV. Foo LXV. Foo LXVI. Foo LXVII. Foo LXVIII. Foo LXIX. Foo LXX. Foo LXXI. Foo LXXII. Foo LXXIII. Foo LXXIV. Foo LXXV. Foo LXXVI. Foo LXXVII. Foo LXXVIII. Foo LXXIX. Foo LXXX. Foo LXXXI. Foo LXXXII. Foo LXXXIII. Foo LXXXIV. Foo LXXXV. Foo LXXXVI. Foo LXXXVII. Foo LXXXVIII. Foo LXXXIX. Foo XC. Foo XCI. Foo XCII. Foo XCIII. Foo XCIV. Foo XCV. Foo XCVI. Foo XCVII. Foo XCVIII. Foo XCIX. Foo C. Foo ================================================ FILE: list/testdata/TestEnumeratorsTransform/alphabet_lower.golden ================================================ a. Foo b. Bar c. Baz ================================================ FILE: list/testdata/TestEnumeratorsTransform/arabic).golden ================================================ 1) Foo 2) Bar 3) Baz ================================================ FILE: list/testdata/TestEnumeratorsTransform/bullet_is_dash.golden ================================================ - Foo - Bar - Baz ================================================ FILE: list/testdata/TestEnumeratorsTransform/roman_within_().golden ================================================ (i) Foo (ii) Bar (iii) Baz ================================================ FILE: list/testdata/TestList.golden ================================================ • Foo • Bar • Baz ================================================ FILE: list/testdata/TestListIntegers.golden ================================================ • 1 • 2 • 3 ================================================ FILE: list/testdata/TestListItems.golden ================================================ • Foo • Bar • Baz ================================================ FILE: list/testdata/TestMultiline.golden ================================================ • Item1 line 2 line 3 • Item2 line 2 line 3 • 3 ================================================ FILE: list/testdata/TestSubListItems2.golden ================================================ • S • neovim • vscode • HI • vim • doom emacs • Parent 2 • I like fuzzy socks ================================================ FILE: list/testdata/TestSublist.golden ================================================ • Foo • Bar I. Hi II. Hello III. Halo • Qux ================================================ FILE: list/testdata/TestSublistItems.golden ================================================ • A • B • C I. D II. E III. F • G ================================================ FILE: position.go ================================================ package lipgloss import ( "math" "strings" "github.com/charmbracelet/x/ansi" ) // Position represents a position along a horizontal or vertical axis. It's in // situations where an axis is involved, like alignment, joining, placement and // so on. // // A value of 0 represents the start (the left or top) and 1 represents the end // (the right or bottom). 0.5 represents the center. // // There are constants Top, Bottom, Center, Left and Right in this package that // can be used to aid readability. type Position float64 func (p Position) value() float64 { return math.Min(1, math.Max(0, float64(p))) } // Position aliases. const ( Top Position = 0.0 Bottom Position = 1.0 Center Position = 0.5 Left Position = 0.0 Right Position = 1.0 ) // 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...) } // 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. func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string { lines, contentWidth := getLines(str) gap := width - contentWidth if gap <= 0 { return str } ws := newWhitespace(opts...) var b strings.Builder for i, l := range lines { // Is this line shorter than the longest line? short := max(0, contentWidth-ansi.StringWidth(l)) switch pos { case Left: b.WriteString(l) b.WriteString(ws.render(gap + short)) case Right: b.WriteString(ws.render(gap + short)) b.WriteString(l) default: // somewhere in the middle totalGap := gap + short split := int(math.Round(float64(totalGap) * pos.value())) left := totalGap - split right := totalGap - left b.WriteString(ws.render(left)) b.WriteString(l) b.WriteString(ws.render(right)) } if i < len(lines)-1 { b.WriteRune('\n') } } return b.String() } // PlaceVertical places a string or text block vertically in an unstyled block // of a given height. If the given height is shorter than the height of the // string (measured by its newlines) then this will be a noop. func PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string { contentHeight := strings.Count(str, "\n") + 1 gap := height - contentHeight if gap <= 0 { return str } ws := newWhitespace(opts...) _, width := getLines(str) emptyLine := ws.render(width) b := strings.Builder{} switch pos { case Top: b.WriteString(str) b.WriteRune('\n') for i := range gap { b.WriteString(emptyLine) if i < gap-1 { b.WriteRune('\n') } } case Bottom: b.WriteString(strings.Repeat(emptyLine+"\n", gap)) b.WriteString(str) default: // Somewhere in the middle split := int(math.Round(float64(gap) * pos.value())) top := gap - split bottom := gap - top b.WriteString(strings.Repeat(emptyLine+"\n", top)) b.WriteString(str) for range bottom { b.WriteRune('\n') b.WriteString(emptyLine) } } return b.String() } ================================================ FILE: query.go ================================================ package lipgloss import ( "fmt" "image/color" "os" "runtime" "github.com/charmbracelet/x/term" ) func backgroundColor(in term.File, out term.File) (color.Color, error) { state, err := term.MakeRaw(in.Fd()) if err != nil { return nil, fmt.Errorf("error setting raw state to detect background color: %w", err) } defer term.Restore(in.Fd(), state) //nolint:errcheck bg, err := queryBackgroundColor(in, out) if err != nil { return nil, err } return bg, nil } // BackgroundColor queries the terminal's background color. Typically, you'll // want to query against stdin and either stdout or stderr, depending on what // you're writing to. // // This function is intended for standalone Lip Gloss use only. If you're using // Bubble Tea, listen for tea.BackgroundColorMsg in your update function. func BackgroundColor(in term.File, out term.File) (bg color.Color, err error) { if runtime.GOOS == "windows" { //nolint:nestif // On Windows, when the input/output is redirected or piped, we need to // open the console explicitly. // See https://learn.microsoft.com/en-us/windows/console/getstdhandle#remarks if !term.IsTerminal(in.Fd()) { f, err := os.OpenFile("CONIN$", os.O_RDWR, 0o644) //nolint:gosec if err != nil { return nil, fmt.Errorf("error opening CONIN$: %w", err) } in = f } if !term.IsTerminal(out.Fd()) { f, err := os.OpenFile("CONOUT$", os.O_RDWR, 0o644) //nolint:gosec if err != nil { return nil, fmt.Errorf("error opening CONOUT$: %w", err) } out = f } return backgroundColor(in, out) } // NOTE: On Unix, one of the given files must be a tty. for _, f := range []term.File{in, out} { if bg, err = backgroundColor(f, f); err == nil { return bg, nil } } return } // HasDarkBackground detects whether the terminal has a light or dark // background. // // Typically, you'll want to query against stdin and either stdout or stderr // depending on what you're writing to. // // hasDarkBG := HasDarkBackground(os.Stdin, os.Stdout) // lightDark := LightDark(hasDarkBG) // myHotColor := lightDark("#ff0000", "#0000ff") // // This is intended for use in standalone Lip Gloss only. In Bubble Tea, listen // for tea.BackgroundColorMsg in your Update function. // // case tea.BackgroundColorMsg: // hasDarkBackground = msg.IsDark() // // By default, this function will return true if it encounters an error. func HasDarkBackground(in term.File, out term.File) bool { bg, err := BackgroundColor(in, out) if err != nil || bg == nil { return true } return isDarkColor(bg) } ================================================ FILE: ranges.go ================================================ package lipgloss import ( "strings" "github.com/charmbracelet/x/ansi" ) // StyleRanges applying styling to ranges in a string. Existing styles will be // taken into account. Ranges should not overlap. func StyleRanges(s string, ranges ...Range) string { if len(ranges) == 0 { return s } var buf strings.Builder lastIdx := 0 stripped := ansi.Strip(s) // Use Truncate and TruncateLeft to style match.MatchedIndexes without // losing the original option style: for _, rng := range ranges { // Add the text before this match if rng.Start > lastIdx { buf.WriteString(ansi.Cut(s, lastIdx, rng.Start)) } // Add the matched range with its highlight buf.WriteString(rng.Style.Render(ansi.Cut(stripped, rng.Start, rng.End))) lastIdx = rng.End } // Add any remaining text after the last match buf.WriteString(ansi.TruncateLeft(s, lastIdx, "")) return buf.String() } // NewRange returns a range and style that can be used with [StyleRanges]. func NewRange(start, end int, style Style) Range { return Range{start, end, style} } // Range is a range of text and associated styling to be used with // [StyleRanges]. type Range struct { Start, End int Style Style } ================================================ FILE: ranges_test.go ================================================ package lipgloss import ( "testing" ) func TestStyleRanges(t *testing.T) { tests := []struct { name string input string ranges []Range expected string }{ { name: "empty ranges", input: "hello world", ranges: []Range{}, expected: "hello world", }, { name: "single range in middle", input: "hello world", ranges: []Range{ NewRange(6, 11, NewStyle().Bold(true)), }, expected: "hello \x1b[1mworld\x1b[m", }, { name: "multiple ranges", input: "hello world", ranges: []Range{ NewRange(0, 5, NewStyle().Bold(true)), NewRange(6, 11, NewStyle().Italic(true)), }, expected: "\x1b[1mhello\x1b[m \x1b[3mworld\x1b[m", }, { name: "overlapping with existing ANSI", input: "hello \x1b[32mworld\x1b[m", ranges: []Range{ NewRange(0, 5, NewStyle().Bold(true)), }, expected: "\x1b[1mhello\x1b[m \x1b[32mworld\x1b[m", }, { name: "style at start", input: "hello world", ranges: []Range{ NewRange(0, 5, NewStyle().Bold(true)), }, expected: "\x1b[1mhello\x1b[m world", }, { name: "style at end", input: "hello world", ranges: []Range{ NewRange(6, 11, NewStyle().Bold(true)), }, expected: "hello \x1b[1mworld\x1b[m", }, { name: "multiple styles with gap", input: "hello beautiful world", ranges: []Range{ NewRange(0, 5, NewStyle().Bold(true)), NewRange(16, 23, NewStyle().Italic(true)), }, expected: "\x1b[1mhello\x1b[m beautiful \x1b[3mworld\x1b[m", }, { name: "adjacent ranges", input: "hello world", ranges: []Range{ NewRange(0, 5, NewStyle().Bold(true)), NewRange(6, 11, NewStyle().Italic(true)), }, expected: "\x1b[1mhello\x1b[m \x1b[3mworld\x1b[m", }, { name: "wide-width characters", input: "Hello 你好 世界", ranges: []Range{ NewRange(0, 5, NewStyle().Bold(true)), // "Hello" NewRange(7, 10, NewStyle().Italic(true)), // "你好" NewRange(11, 50, NewStyle().Bold(true)), // "世界" }, expected: "\x1b[1mHello\x1b[m \x1b[3m你好\x1b[m \x1b[1m世界\x1b[m", }, { name: "ansi and emoji", input: "\x1b[90m\ue615\x1b[39m \x1b[3mDownloads", ranges: []Range{ NewRange(2, 5, NewStyle().Foreground(Color("2"))), }, expected: "\x1b[90m\ue615\x1b[39m \x1b[3m\x1b[32mDow\x1b[m\x1b[90m\x1b[39m\x1b[3mnloads", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := StyleRanges(tt.input, tt.ranges...) if result != tt.expected { t.Errorf("StyleRanges()\n got = %q\nwant = %q\n", result, tt.expected) } }) } } ================================================ FILE: runes.go ================================================ package lipgloss import ( "strings" ) // StyleRunes apply a given style to runes at the given indices in the string. // Note that you must provide styling options for both matched and unmatched // runes. Indices out of bounds will be ignored. func StyleRunes(str string, indices []int, matched, unmatched Style) string { // Convert slice of indices to a map for easier lookups m := make(map[int]struct{}) for _, i := range indices { m[i] = struct{}{} } var ( out strings.Builder group strings.Builder style Style runes = []rune(str) ) for i, r := range runes { group.WriteRune(r) _, matches := m[i] _, nextMatches := m[i+1] if matches != nextMatches || i == len(runes)-1 { // Flush if matches { style = matched } else { style = unmatched } out.WriteString(style.Render(group.String())) group.Reset() } } return out.String() } ================================================ FILE: runes_test.go ================================================ package lipgloss import ( "testing" ) func TestStyleRunes(t *testing.T) { matchedStyle := NewStyle().Reverse(true) unmatchedStyle := NewStyle() tt := []struct { name string input string indices []int expected string }{ { "hello 0", "hello", []int{0}, "\x1b[7mh\x1b[mello", }, { "你好 1", "你好", []int{1}, "你\x1b[7m好\x1b[m", }, { "hello 你好 6,7", "hello 你好", []int{6, 7}, "hello \x1b[7m你好\x1b[m", }, { "hello 1,3", "hello", []int{1, 3}, "h\x1b[7me\x1b[ml\x1b[7ml\x1b[mo", }, { "你好 0,1", "你好", []int{0, 1}, "\x1b[7m你好\x1b[m", }, } fn := func(str string, indices []int) string { return StyleRunes(str, indices, matchedStyle, unmatchedStyle) } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { res := fn(tc.input, tc.indices) if res != tc.expected { t.Errorf("Expected:\n\n`%q`\n`%q`\n\nActual Output:\n\n`%q`\n`%q`\n\n", tc.expected, tc.expected, res, res) } }) } } ================================================ FILE: set.go ================================================ package lipgloss import ( "image/color" "strings" ) // Set a value on the underlying rules map. func (s *Style) set(key propKey, value any) { // We don't allow negative integers on any of our other values, so just keep // them at zero or above. We could use uints instead, but the // conversions are a little tedious, so we're sticking with ints for // sake of usability. switch key { case foregroundKey: s.fgColor = colorOrNil(value) case backgroundKey: s.bgColor = colorOrNil(value) case underlineColorKey: s.ulColor = colorOrNil(value) case underlineKey: s.ul = value.(Underline) case widthKey: s.width = max(0, value.(int)) case heightKey: s.height = max(0, value.(int)) case alignHorizontalKey: s.alignHorizontal = value.(Position) case alignVerticalKey: s.alignVertical = value.(Position) case paddingTopKey: s.paddingTop = max(0, value.(int)) case paddingRightKey: s.paddingRight = max(0, value.(int)) case paddingBottomKey: s.paddingBottom = max(0, value.(int)) case paddingLeftKey: s.paddingLeft = max(0, value.(int)) case paddingCharKey: s.paddingChar = value.(rune) case marginTopKey: s.marginTop = max(0, value.(int)) case marginRightKey: s.marginRight = max(0, value.(int)) case marginBottomKey: s.marginBottom = max(0, value.(int)) case marginLeftKey: s.marginLeft = max(0, value.(int)) case marginBackgroundKey: s.marginBgColor = colorOrNil(value) case marginCharKey: s.marginChar = value.(rune) case borderStyleKey: s.borderStyle = value.(Border) case borderTopForegroundKey: s.borderTopFgColor = colorOrNil(value) case borderRightForegroundKey: s.borderRightFgColor = colorOrNil(value) case borderBottomForegroundKey: s.borderBottomFgColor = colorOrNil(value) case borderLeftForegroundKey: s.borderLeftFgColor = colorOrNil(value) case borderForegroundBlendKey: s.borderBlendFgColor = value.([]color.Color) case borderForegroundBlendOffsetKey: s.borderForegroundBlendOffset = value.(int) case borderTopBackgroundKey: s.borderTopBgColor = colorOrNil(value) case borderRightBackgroundKey: s.borderRightBgColor = colorOrNil(value) case borderBottomBackgroundKey: s.borderBottomBgColor = colorOrNil(value) case borderLeftBackgroundKey: s.borderLeftBgColor = colorOrNil(value) case maxWidthKey: s.maxWidth = max(0, value.(int)) case maxHeightKey: s.maxHeight = max(0, value.(int)) case tabWidthKey: // TabWidth is the only property that may have a negative value (and // that negative value can be no less than -1). s.tabWidth = value.(int) case transformKey: s.transform = value.(func(string) string) case linkKey: s.link = value.(string) case linkParamsKey: s.linkParams = value.(string) default: if v, ok := value.(bool); ok { //nolint:nestif if v { s.attrs |= int(key) } else { s.attrs &^= int(key) } } else if attrs, ok := value.(int); ok { // bool attrs if attrs&int(key) != 0 { s.attrs |= int(key) } else { s.attrs &^= int(key) } } } // Set the prop on s.props = s.props.set(key) } // setFrom sets the property from another style. func (s *Style) setFrom(key propKey, i Style) { switch key { case foregroundKey: s.set(foregroundKey, i.fgColor) case backgroundKey: s.set(backgroundKey, i.bgColor) case underlineColorKey: s.set(underlineColorKey, i.ulColor) case underlineKey: s.set(underlineKey, i.ul) case widthKey: s.set(widthKey, i.width) case heightKey: s.set(heightKey, i.height) case alignHorizontalKey: s.set(alignHorizontalKey, i.alignHorizontal) case alignVerticalKey: s.set(alignVerticalKey, i.alignVertical) case paddingTopKey: s.set(paddingTopKey, i.paddingTop) case paddingRightKey: s.set(paddingRightKey, i.paddingRight) case paddingBottomKey: s.set(paddingBottomKey, i.paddingBottom) case paddingLeftKey: s.set(paddingLeftKey, i.paddingLeft) case paddingCharKey: s.set(paddingCharKey, i.paddingChar) case marginTopKey: s.set(marginTopKey, i.marginTop) case marginRightKey: s.set(marginRightKey, i.marginRight) case marginBottomKey: s.set(marginBottomKey, i.marginBottom) case marginLeftKey: s.set(marginLeftKey, i.marginLeft) case marginBackgroundKey: s.set(marginBackgroundKey, i.marginBgColor) case marginCharKey: s.set(marginCharKey, i.marginChar) case borderStyleKey: s.set(borderStyleKey, i.borderStyle) case borderTopForegroundKey: s.set(borderTopForegroundKey, i.borderTopFgColor) case borderRightForegroundKey: s.set(borderRightForegroundKey, i.borderRightFgColor) case borderBottomForegroundKey: s.set(borderBottomForegroundKey, i.borderBottomFgColor) case borderLeftForegroundKey: s.set(borderLeftForegroundKey, i.borderLeftFgColor) case borderForegroundBlendKey: s.set(borderForegroundBlendKey, i.borderBlendFgColor) case borderForegroundBlendOffsetKey: s.set(borderForegroundBlendOffsetKey, i.borderForegroundBlendOffset) case borderTopBackgroundKey: s.set(borderTopBackgroundKey, i.borderTopBgColor) case borderRightBackgroundKey: s.set(borderRightBackgroundKey, i.borderRightBgColor) case borderBottomBackgroundKey: s.set(borderBottomBackgroundKey, i.borderBottomBgColor) case borderLeftBackgroundKey: s.set(borderLeftBackgroundKey, i.borderLeftBgColor) case maxWidthKey: s.set(maxWidthKey, i.maxWidth) case maxHeightKey: s.set(maxHeightKey, i.maxHeight) case tabWidthKey: s.set(tabWidthKey, i.tabWidth) case transformKey: s.set(transformKey, i.transform) default: // Set attributes for set bool properties s.set(key, i.attrs) } } func colorOrNil(c any) color.Color { if c, ok := c.(color.Color); ok { return c } return nil } // Bold sets a bold formatting rule. func (s Style) Bold(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 { 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]. func (s Style) Underline(v bool) Style { if v { return s.UnderlineStyle(UnderlineSingle) } return s.UnderlineStyle(UnderlineNone) } // UnderlineStyle sets the underline style. This can be used to set the underline // to be a single, double, curly, dotted, or dashed line. // // Note that not all terminal emulators support underline styles. If a style is // not supported, it will typically fall back to a single underline but this is // not guaranteed. This depends on the terminal emulator being used. func (s Style) UnderlineStyle(u Underline) Style { s.set(underlineKey, u) return s } // UnderlineColor sets the color of the underline. By default, the underline // will be the same color as the foreground. // // Note that not all terminal emulators support colored underlines. If color is // not supported, it might produce unexpected results. This depends on the // terminal emulator being used. func (s Style) UnderlineColor(c color.Color) Style { s.set(underlineColorKey, c) 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. func (s Style) Strikethrough(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 } // Blink sets a rule for blinking foreground text. func (s Style) Blink(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 } // 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) func (s Style) Foreground(c color.Color) Style { s.set(foregroundKey, c) return s } // Background sets a background color. func (s Style) Background(c color.Color) Style { s.set(backgroundKey, c) 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. func (s Style) Width(i int) Style { s.set(widthKey, 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. func (s Style) Height(i int) Style { s.set(heightKey, 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. // // With two arguments, the value is applied to the horizontal and vertical // alignments, in that order. func (s Style) Align(p ...Position) Style { if len(p) > 0 { s.set(alignHorizontalKey, p[0]) } if len(p) > 1 { s.set(alignVerticalKey, p[1]) } return s } // AlignHorizontal sets a horizontal text alignment rule. func (s Style) AlignHorizontal(p Position) Style { s.set(alignHorizontalKey, p) return s } // AlignVertical sets a vertical text alignment rule. func (s Style) AlignVertical(p Position) Style { s.set(alignVerticalKey, p) 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. // // With two arguments, the value is applied to the vertical and horizontal // sides, in that order. // // With three arguments, the value is applied to the top side, the horizontal // sides, and the bottom side, in that order. // // With four arguments, the value is applied clockwise starting from the top // side, followed by the right side, then the bottom, and finally the left. // // With more than four arguments no padding will be added. func (s Style) Padding(i ...int) Style { top, right, bottom, left, ok := whichSidesInt(i...) if !ok { return s } s.set(paddingTopKey, top) s.set(paddingRightKey, right) s.set(paddingBottomKey, bottom) s.set(paddingLeftKey, left) return s } // PaddingLeft adds padding on the left. func (s Style) PaddingLeft(i int) Style { s.set(paddingLeftKey, i) return s } // PaddingRight adds padding on the right. func (s Style) PaddingRight(i int) Style { s.set(paddingRightKey, 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 } // PaddingBottom adds padding to the bottom of the block. func (s Style) PaddingBottom(i int) Style { s.set(paddingBottomKey, 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: // // ```go // s := lipgloss.NewStyle().PaddingChar(lipgloss.NBSP) // ``` func (s Style) PaddingChar(r rune) Style { s.set(paddingCharKey, r) return s } // ColorWhitespace determines whether or not the background color should be // applied to the padding. This is true by default as it's more than likely the // desired and expected behavior, but it can be disabled for certain graphic // effects. // // Deprecated: Just use margins and padding. func (s Style) ColorWhitespace(v bool) Style { s.set(colorWhitespaceKey, v) return s } // Margin is a shorthand method for setting margins on all sides at once. // // With one argument, the value is applied to all sides. // // With two arguments, the value is applied to the vertical and horizontal // sides, in that order. // // With three arguments, the value is applied to the top side, the horizontal // sides, and the bottom side, in that order. // // With four arguments, the value is applied clockwise starting from the top // side, followed by the right side, then the bottom, and finally the left. // // With more than four arguments no margin will be added. func (s Style) Margin(i ...int) Style { top, right, bottom, left, ok := whichSidesInt(i...) if !ok { return s } s.set(marginTopKey, top) s.set(marginRightKey, right) s.set(marginBottomKey, bottom) s.set(marginLeftKey, left) return s } // MarginLeft sets the value of the left margin. func (s Style) MarginLeft(i int) Style { s.set(marginLeftKey, i) return s } // MarginRight sets the value of the right margin. func (s Style) MarginRight(i int) Style { s.set(marginRightKey, i) return s } // MarginTop sets the value of the top margin. func (s Style) MarginTop(i int) Style { s.set(marginTopKey, i) return s } // MarginBottom sets the value of the bottom margin. func (s Style) MarginBottom(i int) Style { s.set(marginBottomKey, 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. func (s Style) MarginBackground(c color.Color) Style { s.set(marginBackgroundKey, c) return s } // MarginChar sets the character used for the margin. This is useful for // rendering blocks with a specific character, such as a space or a dot. func (s Style) MarginChar(r rune) Style { s.set(marginCharKey, r) return s } // Border is shorthand for setting the border style and which sides should // have a border at once. The variadic argument sides works as follows: // // With one value, the value is applied to all sides. // // With two values, the values are applied to the vertical and horizontal // sides, in that order. // // With three values, the values are applied to the top side, the horizontal // sides, and the bottom side, in that order. // // With four values, the values are applied clockwise starting from the top // side, followed by the right side, then the bottom, and finally the left. // // With more than four arguments the border will be applied to all sides. // // Examples: // // // Applies borders to the top and bottom only // lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true, false) // // // Applies rounded borders to the right and bottom only // lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), false, true, true, false) func (s Style) Border(b Border, sides ...bool) Style { s.set(borderStyleKey, b) top, right, bottom, left, ok := whichSidesBool(sides...) if !ok { top = true right = true bottom = true left = true } s.set(borderTopKey, top) s.set(borderRightKey, right) s.set(borderBottomKey, bottom) s.set(borderLeftKey, left) return s } // BorderStyle defines the Border on a style. A Border contains a series of // definitions for the sides and corners of a border. // // Note that if border visibility has not been set for any sides when setting // the border style, the border will be enabled for all sides during rendering. // // You can define border characters as you'd like, though several default // styles are included: NormalBorder(), RoundedBorder(), BlockBorder(), // OuterHalfBlockBorder(), InnerHalfBlockBorder(), ThickBorder(), // and DoubleBorder(). // // Example: // // lipgloss.NewStyle().BorderStyle(lipgloss.ThickBorder()) func (s Style) BorderStyle(b Border) Style { s.set(borderStyleKey, b) return s } // BorderTop determines whether or not to draw a top border. func (s Style) BorderTop(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 } // BorderBottom determines whether or not to draw a bottom border. func (s Style) BorderBottom(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 } // BorderForeground is a shorthand function for setting all of the // foreground colors of the borders at once. The arguments work as follows: // // With one argument, the argument is applied to all sides. // // With two arguments, the arguments are applied to the vertical and horizontal // sides, in that order. // // With three arguments, the arguments are applied to the top side, the // horizontal sides, and the bottom side, in that order. // // With four arguments, the arguments are applied clockwise starting from the // top side, followed by the right side, then the bottom, and finally the left. // // With more than four arguments nothing will be set. func (s Style) BorderForeground(c ...color.Color) Style { if len(c) == 0 { return s } top, right, bottom, left, ok := whichSidesColor(c...) if !ok { return s } s.set(borderTopForegroundKey, top) s.set(borderRightForegroundKey, right) s.set(borderBottomForegroundKey, bottom) s.set(borderLeftForegroundKey, left) return s } // BorderTopForeground set the foreground color for the top of the border. func (s Style) BorderTopForeground(c color.Color) Style { s.set(borderTopForegroundKey, c) return s } // BorderRightForeground sets the foreground color for the right side of the // border. func (s Style) BorderRightForeground(c color.Color) Style { s.set(borderRightForegroundKey, c) return s } // BorderBottomForeground sets the foreground color for the bottom of the // border. func (s Style) BorderBottomForeground(c color.Color) Style { s.set(borderBottomForegroundKey, c) return s } // BorderLeftForeground sets the foreground color for the left side of the // border. func (s Style) BorderLeftForeground(c color.Color) Style { s.set(borderLeftForegroundKey, c) return s } // BorderForegroundBlend sets the foreground colors for the border blend. At least // 2 colors are required to use blending, otherwise this will no-op with 0 colors, // and pass to BorderForeground with 1 color. This will override all other border // foreground colors when used. // // When providing colors, in most cases (e.g. when all border sides are enabled), // you will want to provide a wrapping-set of colors, so the start and end color // are either the same, or very similar. For example: // // lipgloss.NewStyle().BorderForegroundBlend( // lipgloss.Color("#00FA68"), // lipgloss.Color("#9900FF"), // lipgloss.Color("#ED5353"), // lipgloss.Color("#9900FF"), // lipgloss.Color("#00FA68"), // ) func (s Style) BorderForegroundBlend(c ...color.Color) Style { if len(c) == 0 { return s } // Insufficient colors to use blending, pass to BorderForeground. if len(c) == 1 { return s.BorderForeground(c...) } s.set(borderForegroundBlendKey, c) return s } // BorderForegroundBlendOffset sets the border blend offset cells, starting from // the top left corner. Value can be positive or negative, and does not need to // equal the dimensions of the border region. Direction (when positive) is as // follows ("o" is starting point): // // o --------> // ┌──────────┐ // ^ │ │ | // | │ │ | // | │ │ | // | │ │ v // └──────────┘ // <--------- func (s Style) BorderForegroundBlendOffset(v int) Style { s.set(borderForegroundBlendOffsetKey, v) return s } // BorderBackground is a shorthand function for setting all of the // background colors of the borders at once. The arguments work as follows: // // With one argument, the argument is applied to all sides. // // With two arguments, the arguments are applied to the vertical and horizontal // sides, in that order. // // With three arguments, the arguments are applied to the top side, the // horizontal sides, and the bottom side, in that order. // // With four arguments, the arguments are applied clockwise starting from the // top side, followed by the right side, then the bottom, and finally the left. // // With more than four arguments nothing will be set. func (s Style) BorderBackground(c ...color.Color) Style { if len(c) == 0 { return s } top, right, bottom, left, ok := whichSidesColor(c...) if !ok { return s } s.set(borderTopBackgroundKey, top) s.set(borderRightBackgroundKey, right) s.set(borderBottomBackgroundKey, bottom) s.set(borderLeftBackgroundKey, left) return s } // BorderTopBackground sets the background color of the top of the border. func (s Style) BorderTopBackground(c color.Color) Style { s.set(borderTopBackgroundKey, c) 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 } // BorderBottomBackground sets the background color of the bottom of the // border. func (s Style) BorderBottomBackground(c color.Color) Style { s.set(borderBottomBackgroundKey, c) return s } // BorderLeftBackground set the background color of the left side of the // border. func (s Style) BorderLeftBackground(c color.Color) Style { s.set(borderLeftBackgroundKey, c) 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. // It works well with Style.MaxWidth. // // Because this in intended to be used at the time of render, this method will // not mutate the style and instead return a copy. // // Example: // // var userInput string = "..." // var userStyle = text.Style{ /* ... */ } // fmt.Println(userStyle.Inline(true).Render(userInput)) func (s Style) Inline(v bool) Style { o := s // copy o.set(inlineKey, v) return o } // MaxWidth applies a max width to a given style. This is useful in enforcing // a certain width at render time, particularly with arbitrary strings and // styles. // // Because this in intended to be used at the time of render, this method will // not mutate the style and instead return a copy. // // Example: // // var userInput string = "..." // var userStyle = text.Style{ /* ... */ } // fmt.Println(userStyle.MaxWidth(16).Render(userInput)) func (s Style) MaxWidth(n int) Style { o := s // copy o.set(maxWidthKey, n) return o } // 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. // // Because this in intended to be used at the time of render, this method will // not mutate the style and instead returns a copy. func (s Style) MaxHeight(n int) Style { o := s // copy o.set(maxHeightKey, n) return o } // NoTabConversion can be passed to [Style.TabWidth] to disable the replacement // of tabs with spaces at render time. const NoTabConversion = -1 // TabWidth sets the number of spaces that a tab (/t) should be rendered as. // When set to 0, tabs will be removed. To disable the replacement of tabs with // spaces entirely, set this to [NoTabConversion]. // // By default, tabs will be replaced with 4 spaces. func (s Style) TabWidth(n int) Style { if n <= -1 { n = -1 } s.set(tabWidthKey, n) return s } // UnderlineSpaces determines whether to underline spaces between words. By // default, this is true. Spaces can also be underlined without underlining the // text itself. func (s Style) UnderlineSpaces(v bool) Style { s.set(underlineSpacesKey, v) return s } // 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. func (s Style) StrikethroughSpaces(v bool) Style { s.set(strikethroughSpacesKey, v) return s } // Transform applies a given function to a string at render time, allowing for // the string being rendered to be manipuated. // // Example: // // s := NewStyle().Transform(strings.ToUpper) // fmt.Println(s.Render("raow!") // "RAOW!" func (s Style) Transform(fn func(string) string) Style { s.set(transformKey, fn) return s } // Hyperlink sets a hyperlink on a style. This is useful for rendering text that // can be clicked on in a terminal emulator that supports hyperlinks. // // Example: // // s := lipgloss.NewStyle().Hyperlink("https://charm.sh") // s := lipgloss.NewStyle().Hyperlink("https://charm.sh", "id=1") func (s Style) Hyperlink(link string, params ...string) Style { s.set(linkKey, link) if len(params) > 0 { s.set(linkParamsKey, strings.Join(params, ":")) } return s } // whichSidesInt is a helper method for setting values on sides of a block based // on the number of arguments. It follows the CSS shorthand rules for blocks // like margin, padding. and borders. Here are how the rules work: // // 0 args: do nothing // 1 arg: all sides // 2 args: top -> bottom // 3 args: top -> horizontal -> bottom // 4 args: top -> right -> bottom -> left // 5+ args: do nothing. func whichSidesInt(i ...int) (top, right, bottom, left int, ok bool) { switch len(i) { case 1: top = i[0] bottom = i[0] left = i[0] right = i[0] ok = true case 2: //nolint:mnd top = i[0] bottom = i[0] left = i[1] right = i[1] ok = true case 3: //nolint:mnd top = i[0] left = i[1] right = i[1] bottom = i[2] ok = true case 4: //nolint:mnd top = i[0] right = i[1] bottom = i[2] left = i[3] ok = true } return top, right, bottom, left, ok } // whichSidesBool is like whichSidesInt, except it operates on a series of // boolean values. See the comment on whichSidesInt for details on how this // works. func whichSidesBool(i ...bool) (top, right, bottom, left bool, ok bool) { switch len(i) { case 1: top = i[0] bottom = i[0] left = i[0] right = i[0] ok = true case 2: //nolint:mnd top = i[0] bottom = i[0] left = i[1] right = i[1] ok = true case 3: //nolint:mnd top = i[0] left = i[1] right = i[1] bottom = i[2] ok = true case 4: //nolint:mnd top = i[0] right = i[1] bottom = i[2] left = i[3] ok = true } return top, right, bottom, left, ok } // whichSidesColor is like whichSides, except it operates on a series of // boolean values. See the comment on whichSidesInt for details on how this // works. func whichSidesColor(i ...color.Color) (top, right, bottom, left color.Color, ok bool) { switch len(i) { case 1: top = i[0] bottom = i[0] left = i[0] right = i[0] ok = true case 2: //nolint:mnd top = i[0] bottom = i[0] left = i[1] right = i[1] ok = true case 3: //nolint:mnd top = i[0] left = i[1] right = i[1] bottom = i[2] ok = true case 4: //nolint:mnd top = i[0] right = i[1] bottom = i[2] left = i[3] ok = true } return top, right, bottom, left, ok } ================================================ FILE: size.go ================================================ package lipgloss import ( "strings" "github.com/charmbracelet/x/ansi" ) // Width returns the cell width of characters in the string. ANSI sequences are // ignored and characters wider than one cell (such as Chinese characters and // emojis) are appropriately measured. // // You should use this instead of len(string) or len([]rune(string) as neither // will give you accurate results. func Width(str string) (width int) { for l := range strings.SplitSeq(str, "\n") { w := ansi.StringWidth(l) if w > width { width = w } } return width } // 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]. func Height(str string) int { return strings.Count(str, "\n") + 1 } // Size returns the width and height of the string in cells. ANSI sequences are // ignored and characters wider than one cell (such as Chinese characters and // emojis) are appropriately measured. func Size(str string) (width, height int) { width = Width(str) height = Height(str) return width, height } ================================================ FILE: size_test.go ================================================ package lipgloss import ( "strings" "testing" ) func BenchmarkWidthSimple(b *testing.B) { simpleStrings := []string{ "ab", "abcdef", "abcdefghij", } for _, str := range simpleStrings { b.Run("len-"+str, func(b *testing.B) { for b.Loop() { _ = Width(str) } }) } } func BenchmarkWidthMultiLine(b *testing.B) { multiLineStrings := []struct { name string str string }{ {"2-lines", "Line 1\nLine 2"}, {"10-lines", "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10"}, {"50-lines", strings.Repeat("Line\n", 49) + "Line"}, } for _, tc := range multiLineStrings { b.Run(tc.name, func(b *testing.B) { for b.Loop() { _ = Width(tc.str) } }) } } ================================================ FILE: style.go ================================================ package lipgloss import ( "image/color" "strings" "unicode" "github.com/charmbracelet/x/ansi" ) const ( // NBSP is the non-breaking space rune. NBSP = '\u00A0' tabWidthDefault = 4 ) // Property for a key. type propKey int64 // Available properties. const ( // Boolean props come first. boldKey propKey = 1 << iota italicKey strikethroughKey reverseKey blinkKey faintKey underlineSpacesKey strikethroughSpacesKey colorWhitespaceKey // Non-boolean props. underlineKey foregroundKey backgroundKey underlineColorKey widthKey heightKey alignHorizontalKey alignVerticalKey // Padding. paddingTopKey paddingRightKey paddingBottomKey paddingLeftKey paddingCharKey // Margins. marginTopKey marginRightKey marginBottomKey marginLeftKey marginBackgroundKey marginCharKey // Border runes. borderStyleKey // Border edges. borderTopKey borderRightKey borderBottomKey borderLeftKey // Border foreground colors. borderTopForegroundKey borderRightForegroundKey borderBottomForegroundKey borderLeftForegroundKey borderForegroundBlendKey borderForegroundBlendOffsetKey // Border background colors. borderTopBackgroundKey borderRightBackgroundKey borderBottomBackgroundKey borderLeftBackgroundKey inlineKey maxWidthKey maxHeightKey tabWidthKey transformKey // Hyperlink. linkKey linkParamsKey ) // props is a set of properties. type props int64 // set sets a property. func (p props) set(k propKey) props { return p | props(k) } // unset unsets a property. func (p props) unset(k propKey) props { return p &^ props(k) } // has checks if a property is set. func (p props) has(k propKey) bool { return p&props(k) != 0 } // Underline is the style of the underline. // // Caveats: // - Not all terminals support all underline styles. // - Some terminals may render unsupported styles as standard underlines. // - Terminal themes may affect the visibility of different underline styles. type Underline = ansi.Underline const ( // UnderlineNone is no underline. UnderlineNone = ansi.UnderlineNone // UnderlineSingle is a single underline. This is the default when underline is enabled. UnderlineSingle = ansi.UnderlineSingle // UnderlineDouble is a double underline. UnderlineDouble = ansi.UnderlineDouble // UnderlineCurly is a curly underline. UnderlineCurly = ansi.UnderlineCurly // UnderlineDotted is a dotted underline. UnderlineDotted = ansi.UnderlineDotted // UnderlineDashed is a dashed underline. UnderlineDashed = ansi.UnderlineDashed ) // NewStyle returns a new, empty Style. While it's syntactic sugar for the // [Style]{} primitive, it's recommended to use this function for creating styles // in case the underlying implementation changes. func NewStyle() Style { return Style{} } // Style contains a set of rules that comprise a style as a whole. type Style struct { props props value string // hyperlink link, linkParams string // we store bool props values here attrs int // props that have values fgColor color.Color bgColor color.Color ulColor color.Color ul Underline width int height int alignHorizontal Position alignVertical Position paddingTop int paddingRight int paddingBottom int paddingLeft int paddingChar rune marginTop int marginRight int marginBottom int marginLeft int marginBgColor color.Color marginChar rune borderStyle Border borderTopFgColor color.Color borderRightFgColor color.Color borderBottomFgColor color.Color borderLeftFgColor color.Color borderBlendFgColor []color.Color borderForegroundBlendOffset int borderTopBgColor color.Color borderRightBgColor color.Color borderBottomBgColor color.Color borderLeftBgColor color.Color maxWidth int maxHeight int tabWidth int transform func(string) string } // joinString joins a list of strings into a single string separated with a // space. func joinString(strs ...string) string { return strings.Join(strs, " ") } // SetString sets the underlying string value for this style. To render once // the underlying string is set, use the [Style.String]. This method is // a convenience for cases when having a stringer implementation is handy, such // as when using fmt.Sprintf. You can also simply define a style and render out // strings directly with [Style.Render]. func (s Style) SetString(strs ...string) Style { s.value = joinString(strs...) return s } // Value returns the raw, unformatted, underlying string value for this style. func (s Style) Value() string { return s.value } // String implements stringer for a Style, returning the rendered result based // on the rules in this style. An underlying string value must be set with // Style.SetString prior to using this method. func (s Style) String() string { return s.Render() } // Copy returns a copy of this style, including any underlying string values. // // Deprecated: to copy just use assignment (i.e. a := b). All methods also // return a new style. func (s Style) Copy() Style { return s } // Inherit overlays the style in the argument onto this style by copying each explicitly // set value from the argument style onto this style if it is not already explicitly set. // Existing set values are kept intact and not overwritten. // // Margins, padding, and underlying string values are not inherited. func (s Style) Inherit(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 } // 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...) } var ( str = joinString(strs...) te ansi.Style teSpace ansi.Style teWhitespace ansi.Style bold = s.getAsBool(boldKey, false) italic = s.getAsBool(italicKey, false) strikethrough = s.getAsBool(strikethroughKey, false) reverse = s.getAsBool(reverseKey, false) blink = s.getAsBool(blinkKey, false) faint = s.getAsBool(faintKey, false) fg = s.getAsColor(foregroundKey) bg = s.getAsColor(backgroundKey) ul = s.getAsColor(underlineColorKey) underline = s.ul != UnderlineNone width = s.getAsInt(widthKey) height = s.getAsInt(heightKey) horizontalAlign = s.getAsPosition(alignHorizontalKey) verticalAlign = s.getAsPosition(alignVerticalKey) topPadding = s.getAsInt(paddingTopKey) rightPadding = s.getAsInt(paddingRightKey) bottomPadding = s.getAsInt(paddingBottomKey) leftPadding = s.getAsInt(paddingLeftKey) horizontalBorderSize = s.GetHorizontalBorderSize() verticalBorderSize = s.GetVerticalBorderSize() colorWhitespace = s.getAsBool(colorWhitespaceKey, true) inline = s.getAsBool(inlineKey, false) maxWidth = s.getAsInt(maxWidthKey) maxHeight = s.getAsInt(maxHeightKey) underlineSpaces = s.getAsBool(underlineSpacesKey, false) || (underline && s.getAsBool(underlineSpacesKey, true)) strikethroughSpaces = s.getAsBool(strikethroughSpacesKey, false) || (strikethrough && s.getAsBool(strikethroughSpacesKey, true)) // Do we need to style whitespace (padding and space outside // paragraphs) separately? styleWhitespace = reverse // Do we need to style spaces separately? useSpaceStyler = (underline && !underlineSpaces) || (strikethrough && !strikethroughSpaces) || underlineSpaces || strikethroughSpaces transform = s.getAsTransform(transformKey) link, linkParams = s.GetHyperlink() ) if transform != nil { str = transform(str) } if s.props == 0 { return s.maybeConvertTabs(str) } if bold { te = te.Bold() } if italic { te = te.Italic(true) } if underline { te = te.Underline(true) } if reverse { teWhitespace = teWhitespace.Reverse(true) te = te.Reverse(true) } if blink { te = te.Blink(true) } if faint { te = te.Faint() } if fg != noColor { te = te.ForegroundColor(fg) if styleWhitespace { teWhitespace = teWhitespace.ForegroundColor(fg) } if useSpaceStyler { teSpace = teSpace.ForegroundColor(fg) } } if bg != noColor { te = te.BackgroundColor(bg) if colorWhitespace { teWhitespace = teWhitespace.BackgroundColor(bg) } if useSpaceStyler { teSpace = teSpace.BackgroundColor(bg) } } if ul != noColor { te = te.UnderlineColor(ul) if colorWhitespace { teWhitespace = teWhitespace.UnderlineColor(ul) } if useSpaceStyler { teSpace = teSpace.UnderlineColor(ul) } } if underline { te = te.UnderlineStyle(s.ul) } if strikethrough { te = te.Strikethrough(true) } if underlineSpaces { teSpace = teSpace.Underline(true) } if strikethroughSpaces { teSpace = teSpace.Strikethrough(true) } // Potentially convert tabs to spaces str = s.maybeConvertTabs(str) // carriage returns can cause strange behaviour when rendering. str = strings.ReplaceAll(str, "\r\n", "\n") // Strip newlines in single line mode if inline { str = strings.ReplaceAll(str, "\n", "") } // Include borders in block size. width -= horizontalBorderSize height -= verticalBorderSize // Word wrap if !inline && width > 0 { wrapAt := width - leftPadding - rightPadding str = Wrap(str, wrapAt, "") } // Render core text { var b strings.Builder isFirst := true for line := range strings.SplitSeq(str, "\n") { if isFirst { isFirst = false } else { b.WriteRune('\n') } if useSpaceStyler { // Look for spaces and apply a different styler for _, r := range line { if unicode.IsSpace(r) { b.WriteString(teSpace.Styled(string(r))) continue } b.WriteString(te.Styled(string(r))) } } else { b.WriteString(te.Styled(line)) } } str = b.String() if len(link) > 0 { str = ansi.SetHyperlink(link, linkParams) + str + ansi.ResetHyperlink() } } // Padding if !inline { //nolint:nestif padChar := s.paddingChar if padChar == 0 { padChar = ' ' } if leftPadding > 0 { var st *ansi.Style if colorWhitespace || styleWhitespace { st = &teWhitespace } str = padLeft(str, leftPadding, st, padChar) } if rightPadding > 0 { var st *ansi.Style if colorWhitespace || styleWhitespace { st = &teWhitespace } str = padRight(str, rightPadding, st, padChar) } if topPadding > 0 { str = strings.Repeat("\n", topPadding) + str } if bottomPadding > 0 { str += strings.Repeat("\n", bottomPadding) } } // Height if height > 0 { str = alignTextVertical(str, verticalAlign, height, nil) } // Set alignment. This will also pad short lines with spaces so that all // lines are the same length, so we run it under a few different conditions // beyond alignment. { numLines := strings.Count(str, "\n") if numLines != 0 || width != 0 { var st *ansi.Style if colorWhitespace || styleWhitespace { st = &teWhitespace } str = alignTextHorizontal(str, horizontalAlign, width, st) } } if !inline { str = s.applyBorder(str) str = s.applyMargins(str, inline) } // Truncate according to MaxWidth if maxWidth > 0 { lines := strings.Split(str, "\n") for i := range lines { lines[i] = ansi.Truncate(lines[i], maxWidth, "") } str = strings.Join(lines, "\n") } // Truncate according to MaxHeight if maxHeight > 0 { lines := strings.Split(str, "\n") height := min(maxHeight, len(lines)) if len(lines) > 0 { str = strings.Join(lines[:height], "\n") } } return str } func (s Style) maybeConvertTabs(str string) string { tw := tabWidthDefault if s.isSet(tabWidthKey) { tw = s.getAsInt(tabWidthKey) } switch tw { case -1: return str case 0: return strings.ReplaceAll(str, "\t", "") default: return strings.ReplaceAll(str, "\t", strings.Repeat(" ", tw)) } } func (s Style) applyMargins(str string, inline bool) string { var ( topMargin = s.getAsInt(marginTopKey) rightMargin = s.getAsInt(marginRightKey) bottomMargin = s.getAsInt(marginBottomKey) leftMargin = s.getAsInt(marginLeftKey) style ansi.Style ) bgc := s.getAsColor(marginBackgroundKey) if bgc != noColor { style = style.BackgroundColor(bgc) } // Add left and right margin marginChar := s.marginChar if marginChar == 0 { marginChar = ' ' } str = padLeft(str, leftMargin, &style, marginChar) str = padRight(str, rightMargin, &style, marginChar) // Top/bottom margin if !inline { _, width := getLines(str) spaces := strings.Repeat(" ", width) if topMargin > 0 { str = style.Styled(strings.Repeat(spaces+"\n", topMargin)) + str } if bottomMargin > 0 { str += style.Styled(strings.Repeat("\n"+spaces, bottomMargin)) } } return str } // Apply left padding. func padLeft(str string, n int, style *ansi.Style, r rune) string { return pad(str, -n, style, r) } // Apply right padding. func padRight(str string, n int, style *ansi.Style, r rune) string { return pad(str, n, style, r) } // pad adds padding to either the left or right side of a string. // Positive values add to the right side while negative values // add to the left side. // r is the rune to use for padding. We use " " for margins and // "\u00A0" for padding so that the padding is preserved when the // string is copied and pasted. func pad(str string, n int, style *ansi.Style, r rune) string { if n == 0 { return str } sp := strings.Repeat(string(r), abs(n)) if style != nil { sp = style.Styled(sp) } b := strings.Builder{} isFirst := true for line := range strings.SplitSeq(str, "\n") { if isFirst { isFirst = false } else { b.WriteRune('\n') } switch { // pad right case n > 0: b.WriteString(line) b.WriteString(sp) // pad left default: b.WriteString(sp) b.WriteString(line) } } return b.String() } func abs(a int) int { if a < 0 { return -a } return a } ================================================ FILE: style_test.go ================================================ package lipgloss import ( "fmt" "reflect" "strings" "testing" ) func TestUnderline(t *testing.T) { t.Parallel() tt := []struct { style Style expected string }{ { NewStyle().Underline(true), "\x1b[4;4ma\x1b[m\x1b[4;4mb\x1b[m\x1b[4m \x1b[m\x1b[4;4mc\x1b[m", }, { NewStyle().Underline(true).UnderlineSpaces(true), "\x1b[4;4ma\x1b[m\x1b[4;4mb\x1b[m\x1b[4m \x1b[m\x1b[4;4mc\x1b[m", }, { NewStyle().Underline(true).UnderlineSpaces(false), "\x1b[4;4ma\x1b[m\x1b[4;4mb\x1b[m \x1b[4;4mc\x1b[m", }, { NewStyle().UnderlineSpaces(true), "ab\x1b[4m \x1b[mc", }, { NewStyle().UnderlineStyle(UnderlineCurly), "\x1b[4;4:3ma\x1b[m\x1b[4;4:3mb\x1b[m\x1b[4m \x1b[m\x1b[4;4:3mc\x1b[m", }, { NewStyle().UnderlineStyle(UnderlineCurly).UnderlineColor(Color("#FF0000")), "\x1b[4;58;2;255;0;0;4:3ma\x1b[m\x1b[4;58;2;255;0;0;4:3mb\x1b[m\x1b[58;2;255;0;0;4m \x1b[m\x1b[4;58;2;255;0;0;4:3mc\x1b[m", }, } for i, tc := range tt { s := tc.style.SetString("ab c") res := s.Render() if res != tc.expected { t.Errorf("Test %d, expected:\n`%q`\n\nActual output:\n`%q`\n\n", i, tc.expected, res) } } } func TestGetUnderlineColor(t *testing.T) { t.Parallel() red := Color("#FF0000") s := NewStyle().Underline(true).UnderlineColor(red) if s.GetUnderlineColor() != red { t.Errorf("GetUnderlineColor() = %v, want %v", s.GetUnderlineColor(), red) } } func TestStrikethrough(t *testing.T) { t.Parallel() tt := []struct { style Style expected string }{ { NewStyle().Strikethrough(true), "\x1b[9ma\x1b[m\x1b[9mb\x1b[m\x1b[9m \x1b[m\x1b[9mc\x1b[m", }, { NewStyle().Strikethrough(true).StrikethroughSpaces(true), "\x1b[9ma\x1b[m\x1b[9mb\x1b[m\x1b[9m \x1b[m\x1b[9mc\x1b[m", }, { NewStyle().Strikethrough(true).StrikethroughSpaces(false), "\x1b[9ma\x1b[m\x1b[9mb\x1b[m \x1b[9mc\x1b[m", }, { NewStyle().StrikethroughSpaces(true), "ab\x1b[9m \x1b[mc", }, } for i, tc := range tt { s := tc.style.SetString("ab c") res := s.Render() if res != tc.expected { t.Errorf("Test %d, expected:\n`%q`\n\nActual output:\n`%q`\n\n", i, tc.expected, res) } } } func TestStyleRender(t *testing.T) { t.Parallel() tt := []struct { style Style expected string }{ { NewStyle().Foreground(Color("#5A56E0")), "\x1b[38;2;90;86;224mhello\x1b[m", }, { NewStyle().Bold(true), "\x1b[1mhello\x1b[m", }, { NewStyle().Italic(true), "\x1b[3mhello\x1b[m", }, { NewStyle().Underline(true), "\x1b[4;4mh\x1b[m\x1b[4;4me\x1b[m\x1b[4;4ml\x1b[m\x1b[4;4ml\x1b[m\x1b[4;4mo\x1b[m", }, { NewStyle().Blink(true), "\x1b[5mhello\x1b[m", }, { NewStyle().Faint(true), "\x1b[2mhello\x1b[m", }, } for i, tc := range tt { s := tc.style.SetString("hello") res := s.Render() if res != tc.expected { t.Errorf("Test %d, expected:\n`%q`\n\nActual output:\n`%q`\n\n", i, tc.expected, res) } } } func TestValueCopy(t *testing.T) { t.Parallel() s := NewStyle(). Bold(true) i := s i.Bold(false) requireEqual(t, s.GetBold(), i.GetBold()) } func TestStyleInherit(t *testing.T) { t.Parallel() s := NewStyle(). Bold(true). Italic(true). Underline(true). Strikethrough(true). Blink(true). Faint(true). Foreground(Color("#ffffff")). Background(Color("#111111")). Margin(1, 1, 1, 1). Padding(1, 1, 1, 1) i := NewStyle().Inherit(s) requireEqual(t, s.GetBold(), i.GetBold()) requireEqual(t, s.GetItalic(), i.GetItalic()) requireEqual(t, s.GetUnderline(), i.GetUnderline()) requireEqual(t, s.GetUnderlineSpaces(), i.GetUnderlineSpaces()) requireEqual(t, s.GetStrikethrough(), i.GetStrikethrough()) requireEqual(t, s.GetStrikethroughSpaces(), i.GetStrikethroughSpaces()) requireEqual(t, s.GetBlink(), i.GetBlink()) requireEqual(t, s.GetFaint(), i.GetFaint()) requireEqual(t, s.GetForeground(), i.GetForeground()) requireEqual(t, s.GetBackground(), i.GetBackground()) requireNotEqual(t, s.GetMarginLeft(), i.GetMarginLeft()) requireNotEqual(t, s.GetMarginRight(), i.GetMarginRight()) requireNotEqual(t, s.GetMarginTop(), i.GetMarginTop()) requireNotEqual(t, s.GetMarginBottom(), i.GetMarginBottom()) requireNotEqual(t, s.GetPaddingLeft(), i.GetPaddingLeft()) requireNotEqual(t, s.GetPaddingRight(), i.GetPaddingRight()) requireNotEqual(t, s.GetPaddingTop(), i.GetPaddingTop()) requireNotEqual(t, s.GetPaddingBottom(), i.GetPaddingBottom()) } func TestStyleCopy(t *testing.T) { t.Parallel() s := NewStyle(). Bold(true). Italic(true). Underline(true). Strikethrough(true). Blink(true). Faint(true). Foreground(Color("#ffffff")). Background(Color("#111111")). Margin(1, 1, 1, 1). Padding(1, 1, 1, 1). TabWidth(2) i := s // copy requireEqual(t, s.GetBold(), i.GetBold()) requireEqual(t, s.GetItalic(), i.GetItalic()) requireEqual(t, s.GetUnderline(), i.GetUnderline()) requireEqual(t, s.GetUnderlineSpaces(), i.GetUnderlineSpaces()) requireEqual(t, s.GetStrikethrough(), i.GetStrikethrough()) requireEqual(t, s.GetStrikethroughSpaces(), i.GetStrikethroughSpaces()) requireEqual(t, s.GetBlink(), i.GetBlink()) requireEqual(t, s.GetFaint(), i.GetFaint()) requireEqual(t, s.GetForeground(), i.GetForeground()) requireEqual(t, s.GetBackground(), i.GetBackground()) requireEqual(t, s.GetMarginLeft(), i.GetMarginLeft()) requireEqual(t, s.GetMarginRight(), i.GetMarginRight()) requireEqual(t, s.GetMarginTop(), i.GetMarginTop()) requireEqual(t, s.GetMarginBottom(), i.GetMarginBottom()) requireEqual(t, s.GetPaddingLeft(), i.GetPaddingLeft()) requireEqual(t, s.GetPaddingRight(), i.GetPaddingRight()) requireEqual(t, s.GetPaddingTop(), i.GetPaddingTop()) requireEqual(t, s.GetPaddingBottom(), i.GetPaddingBottom()) requireEqual(t, s.GetTabWidth(), i.GetTabWidth()) } func TestStyleUnset(t *testing.T) { t.Parallel() s := NewStyle().Bold(true) requireTrue(t, s.GetBold()) s = s.UnsetBold() requireFalse(t, s.GetBold()) s = NewStyle().Italic(true) requireTrue(t, s.GetItalic()) s = s.UnsetItalic() requireFalse(t, s.GetItalic()) s = NewStyle().Underline(true) requireTrue(t, s.GetUnderline()) s = s.UnsetUnderline() requireFalse(t, s.GetUnderline()) s = NewStyle().UnderlineSpaces(true) requireTrue(t, s.GetUnderlineSpaces()) s = s.UnsetUnderlineSpaces() requireFalse(t, s.GetUnderlineSpaces()) s = NewStyle().Strikethrough(true) requireTrue(t, s.GetStrikethrough()) s = s.UnsetStrikethrough() requireFalse(t, s.GetStrikethrough()) s = NewStyle().StrikethroughSpaces(true) requireTrue(t, s.GetStrikethroughSpaces()) s = s.UnsetStrikethroughSpaces() requireFalse(t, s.GetStrikethroughSpaces()) s = NewStyle().Reverse(true) requireTrue(t, s.GetReverse()) s = s.UnsetReverse() requireFalse(t, s.GetReverse()) s = NewStyle().Blink(true) requireTrue(t, s.GetBlink()) s = s.UnsetBlink() requireFalse(t, s.GetBlink()) s = NewStyle().Faint(true) requireTrue(t, s.GetFaint()) s = s.UnsetFaint() requireFalse(t, s.GetFaint()) s = NewStyle().Inline(true) requireTrue(t, s.GetInline()) s = s.UnsetInline() requireFalse(t, s.GetInline()) // colors col := Color("#ffffff") s = NewStyle().Foreground(col) requireEqual(t, col, s.GetForeground()) s = s.UnsetForeground() requireNotEqual(t, col, s.GetForeground()) s = NewStyle().Background(col) requireEqual(t, col, s.GetBackground()) s = s.UnsetBackground() requireNotEqual(t, col, s.GetBackground()) // margins s = NewStyle().Margin(1, 2, 3, 4) requireEqual(t, 1, s.GetMarginTop()) s = s.UnsetMarginTop() requireEqual(t, 0, s.GetMarginTop()) requireEqual(t, 2, s.GetMarginRight()) s = s.UnsetMarginRight() requireEqual(t, 0, s.GetMarginRight()) requireEqual(t, 3, s.GetMarginBottom()) s = s.UnsetMarginBottom() requireEqual(t, 0, s.GetMarginBottom()) requireEqual(t, 4, s.GetMarginLeft()) s = s.UnsetMarginLeft() requireEqual(t, 0, s.GetMarginLeft()) // padding s = NewStyle().Padding(1, 2, 3, 4).PaddingChar('x') requireEqual(t, 1, s.GetPaddingTop()) s = s.UnsetPaddingTop() requireEqual(t, 0, s.GetPaddingTop()) requireEqual(t, 2, s.GetPaddingRight()) s = s.UnsetPaddingRight() requireEqual(t, 0, s.GetPaddingRight()) requireEqual(t, 3, s.GetPaddingBottom()) s = s.UnsetPaddingBottom() requireEqual(t, 0, s.GetPaddingBottom()) requireEqual(t, 4, s.GetPaddingLeft()) s = s.UnsetPaddingLeft() requireEqual(t, 0, s.GetPaddingLeft()) requireEqual(t, 'x', s.GetPaddingChar()) s = s.UnsetPaddingChar() requireEqual(t, ' ', s.GetPaddingChar()) // border s = NewStyle().Border(normalBorder, true, true, true, true) requireTrue(t, s.GetBorderTop()) s = s.UnsetBorderTop() requireFalse(t, s.GetBorderTop()) requireTrue(t, s.GetBorderRight()) s = s.UnsetBorderRight() requireFalse(t, s.GetBorderRight()) requireTrue(t, s.GetBorderBottom()) s = s.UnsetBorderBottom() requireFalse(t, s.GetBorderBottom()) requireTrue(t, s.GetBorderLeft()) s = s.UnsetBorderLeft() requireFalse(t, s.GetBorderLeft()) // tab width s = NewStyle().TabWidth(2) requireEqual(t, s.GetTabWidth(), 2) s = s.UnsetTabWidth() requireNotEqual(t, s.GetTabWidth(), 4) } func TestStyleValue(t *testing.T) { t.Parallel() tt := []struct { name string text string style Style expected string }{ { name: "empty", text: "foo", style: NewStyle(), expected: "foo", }, { name: "set string", text: "foo", style: NewStyle().SetString("bar"), expected: "bar foo", }, { name: "set string with bold", text: "foo", style: NewStyle().SetString("bar").Bold(true), expected: "\x1b[1mbar foo\x1b[m", }, { name: "new style with string", text: "foo", style: NewStyle().SetString("bar", "foobar"), expected: "bar foobar foo", }, { name: "margin right", text: "foo", style: NewStyle().MarginRight(1), expected: "foo ", }, { name: "margin left", text: "foo", style: NewStyle().MarginLeft(1), expected: " foo", }, { name: "empty text margin right", text: "", style: NewStyle().MarginRight(1), expected: " ", }, { name: "empty text margin left", text: "", style: NewStyle().MarginLeft(1), expected: " ", }, } for i, tc := range tt { res := tc.style.Render(tc.text) if res != tc.expected { t.Errorf("Test %d, expected:\n`%q`\n\nActual output:\n`%q`\n\n", i, tc.expected, res) } } } func TestCustomPaddingChar(t *testing.T) { s := NewStyle().Padding(0, 3).PaddingChar('x') requireEqual(t, "xxxTESTxxx", s.Render("TEST")) } func TestTabConversion(t *testing.T) { s := NewStyle() requireEqual(t, "[ ]", s.Render("[\t]")) s = NewStyle().TabWidth(2) requireEqual(t, "[ ]", s.Render("[\t]")) s = NewStyle().TabWidth(0) requireEqual(t, "[]", s.Render("[\t]")) s = NewStyle().TabWidth(-1) requireEqual(t, "[\t]", s.Render("[\t]")) } func TestStringTransform(t *testing.T) { for i, tc := range []struct { input string fn func(string) string expected string }{ // No-op. { "hello", func(s string) string { return s }, "hello", }, // Uppercase. { "raow", strings.ToUpper, "RAOW", }, // English and Chinese. { "The quick brown 狐 jumped over the lazy 犬", func(s string) string { n := 0 rune := make([]rune, len(s)) for _, r := range s { rune[n] = r n++ } rune = rune[0:n] for i := range n / 2 { rune[i], rune[n-1-i] = rune[n-1-i], rune[i] } return string(rune) }, "犬 yzal eht revo depmuj 狐 nworb kciuq ehT", }, } { res := NewStyle().Bold(true).Transform(tc.fn).Render(tc.input) expected := "\x1b[1m" + tc.expected + "\x1b[m" if res != expected { t.Errorf("Test #%d:\nExpected: %q\nGot: %q", i+1, expected, res) } } } func requireTrue(tb testing.TB, b bool) { tb.Helper() requireEqual(tb, true, b) } func requireFalse(tb testing.TB, b bool) { tb.Helper() requireEqual(tb, false, b) } func requireEqual(tb testing.TB, a, b any) { tb.Helper() if !reflect.DeepEqual(a, b) { tb.Errorf("%v != %v", a, b) tb.FailNow() } } func requireNotEqual(tb testing.TB, a, b any) { tb.Helper() if reflect.DeepEqual(a, b) { tb.Errorf("%v == %v", a, b) tb.FailNow() } } func TestCarriageReturnInRender(t *testing.T) { out := fmt.Sprintf("%s\r\n%s\r\n", "Super duper california oranges", "Hello world") testStyle := NewStyle(). MarginLeft(1) got := testStyle.Render(string(out)) want := testStyle.Render(fmt.Sprintf("%s\n%s\n", "Super duper california oranges", "Hello world")) if got != want { t.Logf("got(detailed):\n%q\nwant(detailed):\n%q", got, want) t.Fatalf("got(string):\n%s\nwant(string):\n%s", got, want) } } func TestWidth(t *testing.T) { tests := []struct { name string style Style }{ {"width with borders", NewStyle().Padding(0, 2).Border(NormalBorder(), true)}, {"width no borders", NewStyle().Padding(0, 2)}, {"width unset borders", NewStyle().Padding(0, 2).Border(NormalBorder(), true).BorderLeft(false).BorderRight(false)}, {"width single-sided border", NewStyle().Padding(0, 2).Border(NormalBorder(), true).UnsetBorderBottom().UnsetBorderTop().UnsetBorderRight()}, } { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { content := "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." contentWidth := 80 - tc.style.GetHorizontalFrameSize() rendered := tc.style.Width(contentWidth).Render(content) if Width(rendered) != contentWidth { t.Log("\n" + rendered) t.Fatalf("got: %d\n, want: %d", Width(rendered), contentWidth) } }) } } } func TestHeight(t *testing.T) { tests := []struct { name string style Style }{ {"height with borders", NewStyle().Width(80).Padding(0, 2).Border(NormalBorder(), true)}, {"height no borders", NewStyle().Width(80).Padding(0, 2)}, {"height unset borders", NewStyle().Width(80).Padding(0, 2).Border(NormalBorder(), true).BorderBottom(false).BorderTop(false)}, {"height single-sided border", NewStyle().Width(80).Padding(0, 2).Border(NormalBorder(), true).UnsetBorderLeft().UnsetBorderBottom().UnsetBorderRight()}, } { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { content := "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." contentHeight := 20 - tc.style.GetVerticalFrameSize() rendered := tc.style.Height(contentHeight).Render(content) if Height(rendered) != contentHeight { t.Log("\n" + rendered) t.Fatalf("got: %d\n, want: %d", Height(rendered), contentHeight) } }) } } } func TestHyperlink(t *testing.T) { tests := []struct { name string style Style expected string }{ { name: "hyperlink", style: NewStyle().Hyperlink("https://example.com").SetString("https://example.com"), expected: "\x1b]8;;https://example.com\x07https://example.com\x1b]8;;\x07", }, { name: "hyperlink with text", style: NewStyle().Hyperlink("https://example.com", "id=123").SetString("example"), expected: "\x1b]8;id=123;https://example.com\x07example\x1b]8;;\x07", }, { name: "hyperlink with text and style", style: NewStyle().Hyperlink("https://example.com", "id=123").SetString("example"). Bold(true).Foreground(Color("234")), expected: "\x1b]8;id=123;https://example.com\x07\x1b[1;38;5;234mexample\x1b[m\x1b]8;;\x07", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.style.String() != tc.expected { t.Fatalf("got: %q, want: %q", tc.style.String(), tc.expected) } }) } } func TestUnsetHyperlink(t *testing.T) { tests := []struct { name string style Style expected string }{ { name: "unset hyperlink", style: NewStyle().Hyperlink("https://example.com").SetString("https://example.com").UnsetHyperlink(), expected: "https://example.com", }, { name: "unset hyperlink with text", style: NewStyle().Hyperlink("https://example.com", "id=123").SetString("example").UnsetHyperlink(), expected: "example", }, { name: "unset hyperlink with text and style", style: NewStyle().Hyperlink("https://example.com", "id=123").SetString("example"). Bold(true).Foreground(Color("234")).UnsetHyperlink(), expected: "\x1b[1;38;5;234mexample\x1b[m", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.style.String() != tc.expected { t.Fatalf("got: %q, want: %q", tc.style.String(), tc.expected) } }) } } func BenchmarkPad(b *testing.B) { tests := []struct { name string str string n int }{ {name: "pad-10", str: "foo bar", n: 10}, {name: "pad-100", str: "foo bar", n: 100}, {name: "pad-negative-10", str: "foo bar", n: -10}, {name: "pad-negative-100", str: "foo bar", n: -100}, } for _, tt := range tests { b.Run(tt.name, func(b *testing.B) { for b.Loop() { pad(tt.str, tt.n, nil, ' ') } }) } } func BenchmarkStyleRender(b *testing.B) { tests := []struct { name string style Style input string }{ { name: "simple-1-line", style: NewStyle(). Bold(true). Foreground(Color("#ffffff")), input: "Hello world", }, { name: "simple-5-lines", style: NewStyle(). Bold(true). Foreground(Color("#ffffff")), input: strings.Repeat("Hello world\n", 5), }, { name: "simple-5-lines-inline", style: NewStyle(). Bold(true). Foreground(Color("#ffffff")). Inline(true), input: strings.Repeat("Hello world\n", 5), }, { name: "simple-10-lines-5-height-40-width", style: NewStyle(). Bold(true). Foreground(Color("#ffffff")). Height(5). Width(40), input: strings.Repeat("Hello world\n", 10), }, { name: "simple-10-lines-width-maxwidth", style: NewStyle(). Bold(true). Foreground(Color("#ffffff")). Width(40). MaxWidth(40), input: strings.Repeat("Hello world\n", 10), }, { name: "simple-10-lines-width-maxwidth-borders", style: NewStyle(). Bold(true). Foreground(Color("#ffffff")). Width(40). MaxWidth(40). Border(RoundedBorder(), true), input: strings.Repeat("Hello world\n", 10), }, { name: "simple-10-lines-width-maxwidth-borders-padding-margins", style: NewStyle(). Bold(true). Foreground(Color("#ffffff")). Width(40). MaxWidth(40). Border(RoundedBorder(), true). Padding(1, 1). Margin(1, 1), input: strings.Repeat("Hello world\n", 10), }, } for _, tt := range tests { b.Run(tt.name, func(b *testing.B) { for b.Loop() { tt.style.Render(tt.input) } }) } } ================================================ FILE: table/resizing.go ================================================ package table import ( "math" "strings" "charm.land/lipgloss/v2" "github.com/charmbracelet/x/ansi" ) // resize resizes the table to fit the specified width. // // Given a user defined table width, we must ensure the table is exactly that // width. This must account for all borders, column, separators, and column // data. // // In the case where the table is narrower than the specified table width, // we simply expand the columns evenly to fit the width. // For example, a table with 3 columns takes up 50 characters total, and the // width specified is 80, we expand each column by 10 characters, adding 30 // to the total width. // // In the case where the table is wider than the specified table width, we // _could_ simply shrink the columns evenly but this would result in data // being truncated (perhaps unnecessarily). The naive approach could result // in very poor cropping of the table data. So, instead of shrinking columns // evenly, we calculate the median non-whitespace length of each column, and // shrink the columns based on the largest median. // // For example, // // ┌──────┬───────────────┬──────────┐ // │ Name │ Age of Person │ Location │ // ├──────┼───────────────┼──────────┤ // │ Kini │ 40 │ New York │ // │ Eli │ 30 │ London │ // │ Iris │ 20 │ Paris │ // └──────┴───────────────┴──────────┘ // // Median non-whitespace length vs column width of each column: // // Name: 4 / 5 // Age of Person: 2 / 15 // Location: 6 / 10 // // The biggest difference is 15 - 2, so we can shrink the 2nd column by 13. func (t *Table) resize() { hasHeaders := len(t.headers) > 0 rows := DataToMatrix(t.data) r := newResizer(t.width, t.height, t.headers, rows) r.wrap = t.wrap r.borderColumn = t.borderColumn r.yPaddings = make([][]int, len(r.allRows)) r.yOffset = t.yOffset r.useManualHeight = t.useManualHeight r.borderTop = t.borderTop r.borderBottom = t.borderBottom r.borderLeft = t.borderLeft r.borderRight = t.borderRight r.borderHeader = t.borderHeader r.borderRow = t.borderRow var allRows [][]string if hasHeaders { allRows = append([][]string{t.headers}, rows...) } else { allRows = rows } styleFunc := t.styleFunc if t.styleFunc == nil { styleFunc = DefaultStyles } r.rowHeights = r.defaultRowHeights() for i, row := range allRows { r.yPaddings[i] = make([]int, len(row)) for j := range row { column := &r.columns[j] // Making sure we're passing the right index to `styleFunc`. The header row should be `-1` and // the others should start from `0`. rowIndex := i if hasHeaders { rowIndex-- } style := styleFunc(rowIndex, j) column.xPadding = max(column.xPadding, style.GetHorizontalFrameSize()) column.fixedWidth = max(column.fixedWidth, style.GetWidth()) r.rowHeights[i] = max(r.rowHeights[i], style.GetHeight()) r.yPaddings[i][j] = style.GetVerticalFrameSize() } } // A table width wasn't specified. In this case, detect according to // content width. if r.tableWidth <= 0 { r.tableWidth = r.detectTableWidth() } t.widths, t.heights = r.optimizedWidths() t.firstVisibleRowIndex, t.lastVisibleRowIndex, t.overflowHeight = r.visibleRowIndexes() } // resizerColumn is a column in the resizer. type resizerColumn struct { index int min int max int median int rows [][]string xPadding int // horizontal padding fixedWidth int } // resizer is a table resizer. type resizer struct { tableWidth int tableHeight int headers []string allRows [][]string rowHeights []int columns []resizerColumn wrap bool borderColumn bool yPaddings [][]int // vertical paddings yOffset int useManualHeight bool borderTop bool borderBottom bool borderLeft bool borderRight bool borderHeader bool borderRow bool } // newResizer creates a new resizer. func newResizer(tableWidth, tableHeight int, headers []string, rows [][]string) *resizer { r := &resizer{ tableWidth: tableWidth, tableHeight: tableHeight, headers: headers, } if len(headers) > 0 { r.allRows = append([][]string{headers}, rows...) } else { r.allRows = rows } for _, row := range r.allRows { for i, cell := range row { cellLen := lipgloss.Width(cell) // Header or first row. Just add as is. if len(r.columns) <= i { r.columns = append(r.columns, resizerColumn{ index: i, min: cellLen, max: cellLen, median: cellLen, }) continue } r.columns[i].rows = append(r.columns[i].rows, row) r.columns[i].min = min(r.columns[i].min, cellLen) r.columns[i].max = max(r.columns[i].max, cellLen) } } for j := range r.columns { widths := make([]int, len(r.columns[j].rows)) for i, row := range r.columns[j].rows { widths[i] = lipgloss.Width(row[j]) } r.columns[j].median = median(widths) } return r } // optimizedWidths returns the optimized column widths and row heights. func (r *resizer) optimizedWidths() (colWidths, rowHeights []int) { if r.maxTotal() <= r.tableWidth { return r.expandTableWidth(), r.rowHeights } return r.shrinkTableWidth(), r.rowHeights } // detectTableWidth detects the table width. func (r *resizer) detectTableWidth() int { return r.maxCharCount() + r.totalHorizontalPadding() + r.totalHorizontalBorder() } // expandTableWidth expands the table width. func (r *resizer) expandTableWidth() (colWidths []int) { colWidths = r.maxColumnWidths() for { totalWidth := sum(colWidths) + r.totalHorizontalBorder() if totalWidth >= r.tableWidth { break } shorterColumnIndex := 0 shorterColumnWidth := math.MaxInt32 for j, width := range colWidths { if width == r.columns[j].fixedWidth { continue } if width < shorterColumnWidth { shorterColumnWidth = width shorterColumnIndex = j } } colWidths[shorterColumnIndex]++ } r.expandRowHeights(colWidths) return } // shrinkTableWidth shrinks the table width. func (r *resizer) shrinkTableWidth() (colWidths []int) { colWidths = r.maxColumnWidths() // Cut width of columns that are way too big. shrinkBiggestColumns := func(veryBigOnly bool) { for { totalWidth := sum(colWidths) + r.totalHorizontalBorder() if totalWidth <= r.tableWidth { break } bigColumnIndex := -math.MaxInt32 bigColumnWidth := -math.MaxInt32 for j, width := range colWidths { if width == r.columns[j].fixedWidth { continue } if veryBigOnly { if width >= (r.tableWidth/2) && width > bigColumnWidth { //nolint:mnd bigColumnWidth = width bigColumnIndex = j } } else { if width > bigColumnWidth { bigColumnWidth = width bigColumnIndex = j } } } if bigColumnIndex < 0 || colWidths[bigColumnIndex] == 0 { break } colWidths[bigColumnIndex]-- } } // Cut width of columns that differ the most from the median. shrinkToMedian := func() { for { totalWidth := sum(colWidths) + r.totalHorizontalBorder() if totalWidth <= r.tableWidth { break } biggestDiffToMedian := -math.MaxInt32 biggestDiffToMedianIndex := -math.MaxInt32 for j, width := range colWidths { if width == r.columns[j].fixedWidth { continue } diffToMedian := width - r.columns[j].median if diffToMedian > 0 && diffToMedian > biggestDiffToMedian { biggestDiffToMedian = diffToMedian biggestDiffToMedianIndex = j } } if biggestDiffToMedianIndex <= 0 || colWidths[biggestDiffToMedianIndex] == 0 { break } colWidths[biggestDiffToMedianIndex]-- } } shrinkBiggestColumns(true) shrinkToMedian() shrinkBiggestColumns(false) r.expandRowHeights(colWidths) return colWidths } // expandRowHeights expands the row heights. func (r *resizer) expandRowHeights(colWidths []int) { r.rowHeights = r.defaultRowHeights() if !r.wrap { return } hasHeaders := len(r.headers) > 0 for i, row := range r.allRows { for j, cell := range row { // NOTE(@andreynering): Headers always have a height of 1 (+ padding), even when wrap is enabled. if hasHeaders && i == 0 { r.rowHeights[i] = 1 + r.yPaddingForCell(i, j) continue } height := r.detectContentHeight(cell, colWidths[j]-r.xPaddingForCol(j)) + r.yPaddingForCell(i, j) r.rowHeights[i] = max(r.rowHeights[i], height) } } } // defaultRowHeights returns the default row heights. func (r *resizer) defaultRowHeights() (rowHeights []int) { rowHeights = make([]int, len(r.allRows)) for i := range rowHeights { if i < len(r.rowHeights) { rowHeights[i] = r.rowHeights[i] } rowHeights[i] = max(rowHeights[i], 1) } return } // maxColumnWidths returns the maximum column widths. func (r *resizer) maxColumnWidths() []int { maxColumnWidths := make([]int, len(r.columns)) for i, col := range r.columns { if col.fixedWidth > 0 { maxColumnWidths[i] = col.fixedWidth } else { maxColumnWidths[i] = col.max + r.xPaddingForCol(col.index) } } return maxColumnWidths } // columnCount returns the column count. func (r *resizer) columnCount() int { return len(r.columns) } // maxCharCount returns the maximum character count. func (r *resizer) maxCharCount() int { var count int for _, col := range r.columns { if col.fixedWidth > 0 { count += col.fixedWidth - r.xPaddingForCol(col.index) } else { count += col.max } } return count } // maxTotal returns the maximum total width. func (r *resizer) maxTotal() (maxTotal int) { for j, column := range r.columns { if column.fixedWidth > 0 { maxTotal += column.fixedWidth } else { maxTotal += column.max + r.xPaddingForCol(j) } } return } // totalHorizontalPadding returns the total padding. func (r *resizer) totalHorizontalPadding() (totalHorizontalPadding int) { for _, col := range r.columns { totalHorizontalPadding += col.xPadding } return } // xPaddingForCol returns the horizontal padding for a column. func (r *resizer) xPaddingForCol(j int) int { if j >= len(r.columns) { return 0 } return r.columns[j].xPadding } // yPaddingForCell returns the horizontal padding for a cell. func (r *resizer) yPaddingForCell(i, j int) int { if i >= len(r.yPaddings) || j >= len(r.yPaddings[i]) { return 0 } return r.yPaddings[i][j] } // totalHorizontalBorder returns the total border. func (r *resizer) totalHorizontalBorder() int { return btoi(r.borderLeft) + btoi(r.borderRight) + (r.columnCount()-1)*btoi(r.borderColumn) } // detectContentHeight detects the content height. func (r *resizer) detectContentHeight(content string, width int) (height int) { if width == 0 { return 1 } content = strings.ReplaceAll(content, "\r\n", "\n") for _, line := range strings.Split(content, "\n") { height += strings.Count(ansi.Wrap(line, width, ""), "\n") + 1 } return } // visibleRowIndexes calculates the indexes of the first and last visible rows // according to the current yOffset and tableHeight. If the table height is not // fixed or if the last row is visible, lastVisibleRowIndex will be -2. // Note that the calculated indexes ignore the header row, so 0 corresponds to // the first data row. The header row is always visible if it exists. // The last return value is the number of cells that the overflow row should // take up for the table to fill the available height. func (r *resizer) visibleRowIndexes() (firstVisibleRowIndex, lastVisibleRowIndex, overflowHeight int) { if !r.useManualHeight { return 0, -2, 0 } hasHeaders := len(r.headers) > 0 lastIndex := len(r.allRows) - 1 - btoi(hasHeaders) // Account for fixed elements (top/bottom borders, headers with their border). available := r.tableHeight - btoi(r.borderTop) - btoi(r.borderBottom) - bton(hasHeaders, r.rowHeights[0]) - btoi(hasHeaders && r.borderHeader) // The first row we add does not need a row border. available += btoi(r.borderRow) // Start from the offset with no visible rows. firstVisibleRowIndex = r.yOffset lastVisibleRowIndex = firstVisibleRowIndex - 1 // First add rows at the bottom until we reach the available height, or the last row. for available > 0 && lastVisibleRowIndex < lastIndex { row := r.rowHeights[lastVisibleRowIndex+1+btoi(hasHeaders)] + btoi(r.borderRow) overflow := bton(lastVisibleRowIndex+1 < lastIndex, 1+btoi(r.borderRow)+r.yPaddingForCell(lastVisibleRowIndex+2, 0)) if available-row-overflow < 0 { break } lastVisibleRowIndex++ available -= row } if lastVisibleRowIndex == lastIndex { // Then add rows at the top until we reach the available height, or the first row. for available > 0 && firstVisibleRowIndex > 0 { row := r.rowHeights[firstVisibleRowIndex-1+btoi(hasHeaders)] + btoi(r.borderRow) if available-row < 0 { break } firstVisibleRowIndex-- available -= row } } if lastVisibleRowIndex >= lastIndex { return firstVisibleRowIndex, -2, 0 } overflow := 1 + r.yPaddingForCell(lastVisibleRowIndex+1, 0) return firstVisibleRowIndex, lastVisibleRowIndex, overflow } ================================================ FILE: table/rows.go ================================================ package table // Data is the interface that wraps the basic methods of a table model. type Data interface { // At returns the contents of the cell at the given index. At(row, cell int) string // Rows returns the number of rows in the table. Rows() int // Columns returns the number of columns in the table. Columns() int } // StringData is a string-based implementation of the Data interface. type StringData struct { rows [][]string columns int } // NewStringData creates a new StringData with the given number of columns. func NewStringData(rows ...[]string) *StringData { m := StringData{columns: 0} for _, row := range rows { m.columns = max(m.columns, len(row)) m.rows = append(m.rows, row) } return &m } // Append appends the given row to the table. func (m *StringData) Append(row []string) { m.columns = max(m.columns, len(row)) m.rows = append(m.rows, row) } // At returns the contents of the cell at the given index. func (m *StringData) At(row, cell int) string { if row >= len(m.rows) || cell >= len(m.rows[row]) { return "" } return m.rows[row][cell] } // Columns returns the number of columns in the table. func (m *StringData) Columns() int { return m.columns } // Item appends the given row to the table. func (m *StringData) Item(rows ...string) *StringData { m.columns = max(m.columns, len(rows)) m.rows = append(m.rows, rows) return m } // Rows returns the number of rows in the table. func (m *StringData) Rows() int { return len(m.rows) } // Filter applies a filter on some data. type Filter struct { data Data filter func(row int) bool } // NewFilter initializes a new Filter. func NewFilter(data Data) *Filter { return &Filter{data: data} } // Filter applies the given filter function to the data. func (m *Filter) Filter(f func(row int) bool) *Filter { m.filter = f return m } // At returns the row at the given index. func (m *Filter) At(row, cell int) string { j := 0 for i := range m.data.Rows() { if m.filter(i) { if j == row { return m.data.At(i, cell) } j++ } } return "" } // Columns returns the number of columns in the table. func (m *Filter) Columns() int { return m.data.Columns() } // Rows returns the number of rows in the table. func (m *Filter) Rows() int { j := 0 for i := range m.data.Rows() { if m.filter(i) { j++ } } return j } // DataToMatrix is a helper function that converts an object that implements the // Data interface into a table. func DataToMatrix(data Data) (rows [][]string) { numRows := data.Rows() numCols := data.Columns() rows = make([][]string, numRows) for i := range numRows { rows[i] = make([]string, numCols) for j := range numCols { rows[i][j] = data.At(i, j) } } return } ================================================ FILE: table/table.go ================================================ // Package table provides a styled table renderer for terminals. package table import ( "strings" "charm.land/lipgloss/v2" "github.com/charmbracelet/x/ansi" ) // HeaderRow denotes the header's row index used when rendering headers. Use // this value when looking to customize header styles in StyleFunc. const HeaderRow int = -1 // StyleFunc is the style function that determines the style of a Cell. // // It takes the row and column of the cell as an input and determines the // lipgloss Style to use for that cell position. // // Example: // // t := table.New(). // Headers("Name", "Age"). // Row("Kini", 4). // Row("Eli", 1). // Row("Iris", 102). // StyleFunc(func(row, col int) lipgloss.Style { // switch { // case row == 0: // return HeaderStyle // case row%2 == 0: // return EvenRowStyle // default: // return OddRowStyle // } // }) type StyleFunc func(row, col int) lipgloss.Style // DefaultStyles is a TableStyleFunc that returns a new Style with no attributes. func DefaultStyles(_, _ int) lipgloss.Style { return lipgloss.NewStyle() } // Table is a type for rendering tables. type Table struct { baseStyle lipgloss.Style styleFunc StyleFunc border lipgloss.Border borderTop bool borderBottom bool borderLeft bool borderRight bool borderHeader bool borderColumn bool borderRow bool borderStyle lipgloss.Style headers []string data Data width int height int useManualHeight bool yOffset int wrap bool widths []int heights []int firstVisibleRowIndex int lastVisibleRowIndex int overflowHeight int } // New returns a new Table that can be modified through different // attributes. // // By default, a table has normal border, no styling, and no rows. func New() *Table { return &Table{ styleFunc: DefaultStyles, border: lipgloss.NormalBorder(), borderBottom: true, borderColumn: true, borderHeader: true, borderLeft: true, borderRight: true, borderTop: true, wrap: true, data: NewStringData(), } } // ClearRows clears the table rows. func (t *Table) ClearRows() *Table { t.data = NewStringData() return t } // BaseStyle sets the base style for the whole table. If you need to set a // background color for the whole table, use this. func (t *Table) BaseStyle(baseStyle lipgloss.Style) *Table { t.baseStyle = baseStyle t.borderStyle = t.borderStyle.Inherit(baseStyle) return t } // StyleFunc sets the style for a cell based on it's position (row, column). func (t *Table) StyleFunc(style StyleFunc) *Table { t.styleFunc = style return t } // style returns the style for a cell based on it's position (row, column). func (t *Table) style(row, col int) lipgloss.Style { if t.styleFunc == nil { return t.baseStyle } return t.styleFunc(row, col).Inherit(t.baseStyle) } // Data sets the table data. func (t *Table) Data(data Data) *Table { t.data = data return t } // GetData returns the table data. func (t *Table) GetData() Data { return t.data } // Rows appends rows to the table data. func (t *Table) Rows(rows ...[]string) *Table { for _, row := range rows { switch t.data.(type) { case *StringData: t.data.(*StringData).Append(row) } } return t } // Row appends a row to the table data. func (t *Table) Row(row ...string) *Table { switch t.data.(type) { case *StringData: t.data.(*StringData).Append(row) } return t } // Headers sets the table headers. func (t *Table) Headers(headers ...string) *Table { t.headers = headers return t } // GetHeaders returns the table headers. func (t *Table) GetHeaders() []string { return t.headers } // Border sets the table border. func (t *Table) Border(border lipgloss.Border) *Table { t.border = border return t } // BorderTop sets the top border. func (t *Table) BorderTop(v bool) *Table { t.borderTop = v return t } // BorderBottom sets the bottom border. func (t *Table) BorderBottom(v bool) *Table { t.borderBottom = v return t } // BorderLeft sets the left border. func (t *Table) BorderLeft(v bool) *Table { t.borderLeft = v return t } // BorderRight sets the right border. func (t *Table) BorderRight(v bool) *Table { t.borderRight = v return t } // BorderHeader sets the header separator border. func (t *Table) BorderHeader(v bool) *Table { t.borderHeader = v return t } // BorderColumn sets the column border separator. func (t *Table) BorderColumn(v bool) *Table { t.borderColumn = v return t } // BorderRow sets the row border separator. func (t *Table) BorderRow(v bool) *Table { t.borderRow = v return t } // BorderStyle sets the style for the table border. func (t *Table) BorderStyle(style lipgloss.Style) *Table { t.borderStyle = style.Inherit(t.baseStyle) return t } // GetBorderTop gets the top border. func (t *Table) GetBorderTop() bool { return t.borderTop } // GetBorderBottom gets the bottom border. func (t *Table) GetBorderBottom() bool { return t.borderBottom } // GetBorderLeft gets the left border. func (t *Table) GetBorderLeft() bool { return t.borderLeft } // GetBorderRight gets the right border. func (t *Table) GetBorderRight() bool { return t.borderRight } // GetBorderHeader gets the header separator border. func (t *Table) GetBorderHeader() bool { return t.borderHeader } // GetBorderColumn gets the column border separator. func (t *Table) GetBorderColumn() bool { return t.borderColumn } // GetBorderRow gets the row border separator. func (t *Table) GetBorderRow() bool { return t.borderRow } // 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. func (t *Table) Width(w int) *Table { t.width = w return t } // Height sets the table height. func (t *Table) Height(h int) *Table { t.height = h t.useManualHeight = true return t } // GetHeight returns the height of the table. func (t *Table) GetHeight() int { return t.height } // YOffset sets the table rendering offset. func (t *Table) YOffset(o int) *Table { t.yOffset = o return t } // GetYOffset returns the table rendering offset. func (t *Table) GetYOffset() int { return t.yOffset } // FirstVisibleRowIndex returns the index of the first visible row in the table. func (t *Table) FirstVisibleRowIndex() int { return t.firstVisibleRowIndex } // LastVisibleRowIndex returns the index of the last visible row in the table. func (t *Table) LastVisibleRowIndex() int { return t.lastVisibleRowIndex } // VisibleRows returns the number of visible rows in the table. func (t *Table) VisibleRows() int { if t.lastVisibleRowIndex == -2 { return t.data.Rows() - t.firstVisibleRowIndex } return t.lastVisibleRowIndex - t.firstVisibleRowIndex + 1 } // Wrap dictates whether or not the table content should wrap. // // This only applies to data cells. Headers are never wrapped. func (t *Table) Wrap(w bool) *Table { t.wrap = w return t } // String returns the table as a string. func (t *Table) String() string { hasHeaders := len(t.headers) > 0 hasRows := t.data != nil && t.data.Rows() > 0 if !hasHeaders && !hasRows { return "" } // Add empty cells to the headers, until it's the same length as the longest // row (only if there are at headers in the first place). if hasHeaders { for i := len(t.headers); i < t.data.Columns(); i++ { t.headers = append(t.headers, "") } } // Do all the sizing calculations for width and height. t.resize() var sb strings.Builder if t.borderTop { sb.WriteString(t.constructTopBorder()) sb.WriteString("\n") } if hasHeaders { sb.WriteString(t.constructHeaders()) } var bottom string if t.borderBottom { bottom = t.constructBottomBorder() } // If there are no data rows render nothing. if t.data.Rows() > 0 { for r := t.firstVisibleRowIndex; r < t.data.Rows(); r++ { if t.lastVisibleRowIndex != -2 && r > t.lastVisibleRowIndex { break } sb.WriteString(t.constructRow(r, false)) } // Add an overflow row to show that there are more rows not being rendered. if t.lastVisibleRowIndex != -2 { sb.WriteString(t.constructRow(t.lastVisibleRowIndex+1, true)) } } sb.WriteString(bottom) return lipgloss.NewStyle(). MaxHeight(min(t.height, t.computeHeight())). MaxWidth(t.width). Render(strings.TrimSuffix(sb.String(), "\n")) } // computeHeight computes the height of the table in it's current configuration. func (t *Table) computeHeight() int { hasHeaders := len(t.headers) > 0 return sum(t.heights) - 1 + btoi(hasHeaders) + btoi(t.borderTop) + btoi(t.borderBottom) + btoi(t.borderHeader) + t.data.Rows()*btoi(t.borderRow) } // Render returns the table as a string. func (t *Table) Render() string { return t.String() } // constructTopBorder constructs the top border for the table given it's current // border configuration and data. func (t *Table) constructTopBorder() string { var s strings.Builder if t.borderLeft { s.WriteString(t.borderStyle.Render(t.border.TopLeft)) } for i := range t.widths { s.WriteString(t.borderStyle.Render(strings.Repeat(t.border.Top, t.widths[i]))) if i < len(t.widths)-1 && t.borderColumn { s.WriteString(t.borderStyle.Render(t.border.MiddleTop)) } } if t.borderRight { s.WriteString(t.borderStyle.Render(t.border.TopRight)) } return s.String() } // constructBottomBorder constructs the bottom border for the table given it's current // border configuration and data. func (t *Table) constructBottomBorder() string { var s strings.Builder if t.borderLeft { s.WriteString(t.borderStyle.Render(t.border.BottomLeft)) } for i := range t.widths { s.WriteString(t.borderStyle.Render(strings.Repeat(t.border.Bottom, t.widths[i]))) if i < len(t.widths)-1 && t.borderColumn { s.WriteString(t.borderStyle.Render(t.border.MiddleBottom)) } } if t.borderRight { s.WriteString(t.borderStyle.Render(t.border.BottomRight)) } return s.String() } // constructHeaders constructs the headers for the table given it's current // header configuration and data. func (t *Table) constructHeaders() string { var s strings.Builder cells := make([]string, 0, len(t.headers)*2+1) height := t.heights[0] left := strings.Repeat(t.borderStyle.Render(t.border.Left)+"\n", height) if t.borderLeft { cells = append(cells, left) } for j, header := range t.headers { cellStyle := t.style(HeaderRow, j) // NOTE(@andreynering): We always truncate headers. header = t.truncateCell(header, HeaderRow, j) cells = append(cells, cellStyle. Height(height-cellStyle.GetVerticalMargins()). Width(t.widths[j]-cellStyle.GetHorizontalMargins()). Render(header), ) if j < len(t.headers)-1 && t.borderColumn { cells = append(cells, left) } } if t.borderRight { right := strings.Repeat(t.borderStyle.Render(t.border.Right)+"\n", height) cells = append(cells, right) } for i, cell := range cells { cells[i] = strings.TrimRight(cell, "\n") } s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, cells...) + "\n") if t.borderHeader { if t.borderLeft { s.WriteString(t.borderStyle.Render(t.border.MiddleLeft)) } for i := range t.headers { s.WriteString(t.borderStyle.Render(strings.Repeat(t.border.Top, t.widths[i]))) if i < len(t.headers)-1 && t.borderColumn { s.WriteString(t.borderStyle.Render(t.border.Middle)) } } if t.borderRight { s.WriteString(t.borderStyle.Render(t.border.MiddleRight)) } s.WriteString("\n") } return s.String() } // constructRow constructs the row for the table given an index and row data // based on the current configuration. If isOverflow is true, the row is // rendered as an overflow row (using ellipsis). func (t *Table) constructRow(index int, isOverflow bool) string { var s strings.Builder cells := make([]string, 0, t.data.Columns()*2+1) hasHeaders := len(t.headers) > 0 var height int if !isOverflow { height = t.heights[index+btoi(hasHeaders)] } else { height = t.overflowHeight } left := strings.Repeat(t.borderStyle.Render(t.border.Left)+"\n", height) if t.borderLeft { cells = append(cells, left) } for c := range t.data.Columns() { cell := "…" if !isOverflow { cell = t.data.At(index, c) } cellStyle := t.style(index, c) if !t.wrap { cell = t.truncateCell(cell, index, c) } cells = append(cells, cellStyle. // Account for the margins in the cell sizing. Height(height-cellStyle.GetVerticalMargins()). MaxHeight(height). Width(t.widths[c]-cellStyle.GetHorizontalMargins()). MaxWidth(t.widths[c]). Render(cell)) if c < t.data.Columns()-1 && t.borderColumn { cells = append(cells, left) } } if t.borderRight { right := strings.Repeat(t.borderStyle.Render(t.border.Right)+"\n", height) cells = append(cells, right) } for i, cell := range cells { cells[i] = strings.TrimRight(cell, "\n") } s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, cells...) + "\n") if t.borderRow && !isOverflow && index < t.data.Rows()-1 { if t.borderLeft { s.WriteString(t.borderStyle.Render(t.border.MiddleLeft)) } for i := range t.widths { s.WriteString(t.borderStyle.Render(strings.Repeat(t.border.Bottom, t.widths[i]))) if i < len(t.widths)-1 && t.borderColumn { s.WriteString(t.borderStyle.Render(t.border.Middle)) } } if t.borderRight { s.WriteString(t.borderStyle.Render(t.border.MiddleRight)) } s.WriteString("\n") } return s.String() } func (t *Table) truncateCell(cell string, rowIndex, colIndex int) string { hasHeaders := len(t.headers) > 0 height := t.heights[rowIndex+btoi(hasHeaders)] cellWidth := t.widths[colIndex] cellStyle := t.style(rowIndex, colIndex) // NOTE(@andreynering): We always truncate headers to 1 line. if rowIndex == HeaderRow { height = 1 } length := (cellWidth * height) - cellStyle.GetHorizontalPadding() - cellStyle.GetHorizontalMargins() return ansi.Truncate(cell, length, "…") } ================================================ FILE: table/table_test.go ================================================ package table import ( "fmt" "strings" "testing" "charm.land/lipgloss/v2" "github.com/charmbracelet/x/exp/golden" ) var TableStyle = func(row, col int) lipgloss.Style { switch { case row == HeaderRow: return lipgloss.NewStyle().Padding(0, 1).Align(lipgloss.Center) case row%2 == 0: return lipgloss.NewStyle().Padding(0, 1) default: return lipgloss.NewStyle().Padding(0, 1) } } func TestTable(t *testing.T) { table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). Row("Spanish", "Hola", "¿Qué tal?") golden.RequireEqual(t, []byte(table.String())) } func TestTableWithBackground(t *testing.T) { table := New(). Border(lipgloss.NormalBorder()). BaseStyle(lipgloss.NewStyle().Background(lipgloss.Color("18"))). BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("15"))). StyleFunc(func(row, col int) lipgloss.Style { return lipgloss.NewStyle().Foreground(lipgloss.Color("15")) }). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). Row("Spanish", "Hola", "¿Qué tal?") golden.RequireEqual(t, []byte(table.String())) } func TestTableExample(t *testing.T) { HeaderStyle := lipgloss.NewStyle().Padding(0, 1).Align(lipgloss.Center) EvenRowStyle := lipgloss.NewStyle().Padding(0, 1) OddRowStyle := lipgloss.NewStyle().Padding(0, 1) rows := [][]string{ {"Chinese", "您好", "你好"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Здравствуйте", "Привет"}, {"Spanish", "Hola", "¿Qué tal?"}, } table := New(). Border(lipgloss.NormalBorder()). BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))). StyleFunc(func(row, col int) lipgloss.Style { switch { case row == HeaderRow: return HeaderStyle case row%2 == 0: return EvenRowStyle default: return OddRowStyle } }). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...) // You can also add tables row-by-row table.Row("English", "You look absolutely fabulous.", "How's it going?") golden.RequireEqual(t, []byte(table.String())) } func TestTableEmpty(t *testing.T) { table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL") golden.RequireEqual(t, []byte(table.String())) } func TestTableNoStyleFunc(t *testing.T) { table := New(). Border(lipgloss.NormalBorder()). StyleFunc(nil). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). Row("Spanish", "Hola", "¿Qué tal?") golden.RequireEqual(t, []byte(table.String())) } func TestTableYOffset(t *testing.T) { table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). Row("Spanish", "Hola", "¿Qué tal?"). YOffset(1). Height(8) golden.RequireEqual(t, []byte(table.String())) } func TestTableBorder(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } table := New(). Border(lipgloss.DoubleBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) } func TestTableSetRows(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) } func TestMoreCellsThanHeaders(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL"). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) } func TestMoreCellsThanHeadersExtra(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet", "Privet", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL"). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) } func TestTableNoHeaders(t *testing.T) { table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). Row("Spanish", "Hola", "¿Qué tal?") golden.RequireEqual(t, []byte(table.String())) } func TestTableNoColumnSeparators(t *testing.T) { table := New(). Border(lipgloss.NormalBorder()). BorderColumn(false). StyleFunc(TableStyle). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). Row("Spanish", "Hola", "¿Qué tal?") golden.RequireEqual(t, []byte(table.String())) } func TestTableNoColumnSeparatorsWithHeaders(t *testing.T) { table := New(). Border(lipgloss.NormalBorder()). BorderColumn(false). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). Row("Spanish", "Hola", "¿Qué tal?") golden.RequireEqual(t, []byte(table.String())) } func TestInnerBordersOnly(t *testing.T) { table := New(). Border(lipgloss.NormalBorder()). BorderColumn(false). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). Row("Spanish", "Hola", "¿Qué tal?"). BorderTop(false). BorderRight(false). BorderBottom(false). BorderLeft(false). BorderRow(true). BorderColumn(true) golden.RequireEqual(t, []byte(table.String())) } func TestBorderColumnsWithExtraRows(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet", "Privet", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } table := New(). Border(lipgloss.NormalBorder()). BorderColumn(false). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL"). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) } func TestNew(t *testing.T) { table := New() expected := "" if table.String() != expected { t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, table.String()) } } func TestTableUnsetBorders(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...). BorderTop(false). BorderBottom(false). BorderLeft(false). BorderRight(false) golden.RequireEqual(t, []byte(table.String())) } func TestTableUnsetHeaderSeparator(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...). BorderHeader(false). BorderTop(false). BorderBottom(false). BorderLeft(false). BorderRight(false) golden.RequireEqual(t, []byte(table.String())) } func TestTableUnsetHeaderSeparatorWithBorder(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...). BorderHeader(false) golden.RequireEqual(t, []byte(table.String())) } func TestTableRowSeparators(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). BorderRow(true). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) } func TestTableHeights(t *testing.T) { styleFunc := func(row, col int) lipgloss.Style { if row == HeaderRow { return lipgloss.NewStyle().Padding(0, 1) } if col == 0 { return lipgloss.NewStyle().Width(18).Padding(1) } return lipgloss.NewStyle().Width(25).Padding(1, 2) } rows := [][]string{ {"Chutar o balde", `Literally translates to "kick the bucket." It's used when someone gives up or loses patience.`}, {"Engolir sapos", `Literally means "to swallow frogs." It's used to describe someone who has to tolerate or endure unpleasant situations.`}, {"Arroz de festa", `Literally means "party rice." It´s used to refer to someone who shows up everywhere.`}, } table := New(). Border(lipgloss.NormalBorder()). StyleFunc(styleFunc). Headers("EXPRESSION", "MEANING"). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) } func TestTableMultiLineRowSeparator(t *testing.T) { styleFunc := func(row, col int) lipgloss.Style { if row == HeaderRow { return lipgloss.NewStyle().Padding(0, 1) } if col == 0 { return lipgloss.NewStyle().Width(18).Padding(1) } return lipgloss.NewStyle().Width(25).Padding(1, 2) } table := New(). Border(lipgloss.NormalBorder()). StyleFunc(styleFunc). Headers("EXPRESSION", "MEANING"). BorderRow(true). Row("Chutar o balde", `Literally translates to "kick the bucket." It's used when someone gives up or loses patience.`). Row("Engolir sapos", `Literally means "to swallow frogs." It's used to describe someone who has to tolerate or endure unpleasant situations.`). Row("Arroz de festa", `Literally means "party rice." It´s used to refer to someone who shows up everywhere.`) golden.RequireEqual(t, []byte(table.String())) } func TestTableWidthExpand(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } table := New(). Width(80). StyleFunc(TableStyle). Border(lipgloss.NormalBorder()). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...) if lipgloss.Width(table.String()) != 80 { t.Fatalf("expected table width to be 80, got %d", lipgloss.Width(table.String())) } golden.RequireEqual(t, []byte(table.String())) } func TestTableWidthShrink(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } t.Run("NoBorders", func(t *testing.T) { table := New(). Width(30). StyleFunc(TableStyle). BorderLeft(false). BorderRight(false). Border(lipgloss.NormalBorder()). BorderColumn(false). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) }) t.Run("DefaultBorders", func(t *testing.T) { table := New(). Width(30). StyleFunc(TableStyle). Border(lipgloss.NormalBorder()). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) }) t.Run("OutlineBordersOnly", func(t *testing.T) { table := New(). Width(30). StyleFunc(TableStyle). Border(lipgloss.NormalBorder()). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...). BorderTop(true). BorderBottom(true). BorderLeft(true). BorderRight(true). BorderColumn(false). BorderRow(false). BorderHeader(true) golden.RequireEqual(t, []byte(table.String())) }) } func TestTableWidthSmartCrop(t *testing.T) { rows := [][]string{ {"Kini", "40", "New York"}, {"Eli", "30", "London"}, {"Iris", "20", "Paris"}, } table := New(). Width(25). StyleFunc(TableStyle). Border(lipgloss.NormalBorder()). Headers("Name", "Age of Person", "Location"). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) } func TestTableWidthSmartCropExtensive(t *testing.T) { rows := [][]string{ {"Chinese", "您好", "你好"}, {"Japanese", "こんにちは", "やあ"}, {"Arabic", "أهلين", "أهلا"}, {"Russian", "Здравствуйте", "Привет"}, {"Spanish", "Hola", "¿Qué tal?"}, {"English", "You look absolutely fabulous.", "How's it going?"}, } table := New(). Width(18). StyleFunc(TableStyle). Border(lipgloss.ThickBorder()). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Wrap(false). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) } func TestTableWidthSmartCropTiny(t *testing.T) { rows := [][]string{ {"Chinese", "您好", "你好"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Здравствуйте", "Привет"}, {"Spanish", "Hola", "¿Qué tal?"}, {"English", "You look absolutely fabulous.", "How's it going?"}, } table := New(). Width(1). StyleFunc(TableStyle). Border(lipgloss.NormalBorder()). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) } func TestTableWidths(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } table := New(). Width(30). StyleFunc(TableStyle). BorderLeft(false). BorderRight(false). Border(lipgloss.NormalBorder()). BorderColumn(false). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) } func TestFilter(t *testing.T) { data := NewStringData(). Item("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Item("French", "Bonjour", "Salut"). Item("Japanese", "こんにちは", "やあ"). Item("Russian", "Zdravstvuyte", "Privet"). Item("Spanish", "Hola", "¿Qué tal?") filter := NewFilter(data).Filter(func(row int) bool { return data.At(row, 0) != "French" }) table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Data(filter) golden.RequireEqual(t, []byte(table.String())) } func TestFilterInverse(t *testing.T) { data := NewStringData(). Item("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Item("French", "Bonjour", "Salut"). Item("Japanese", "こんにちは", "やあ"). Item("Russian", "Zdravstvuyte", "Privet"). Item("Spanish", "Hola", "¿Qué tal?") filter := NewFilter(data).Filter(func(row int) bool { return data.At(row, 0) == "French" }) table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Data(filter) golden.RequireEqual(t, []byte(table.String())) } func TestTableANSI(t *testing.T) { const code = "\x1b[31mC\x1b[0m\x1b[32mo\x1b[0m\x1b[34md\x1b[0m\x1b[33me\x1b[0m" rows := [][]string{ {"Apple", "Red", "\x1b[31m31\x1b[0m"}, {"Lime", "Green", "\x1b[32m32\x1b[0m"}, {"Banana", "Yellow", "\x1b[33m33\x1b[0m"}, {"Blueberry", "Blue", "\x1b[34m34\x1b[0m"}, } table := New(). Width(29). StyleFunc(TableStyle). Border(lipgloss.NormalBorder()). Headers("Fruit", "Color", code). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) } func TestTableHeightExact(t *testing.T) { table := New(). Height(9). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). Row("Spanish", "Hola", "¿Qué tal?") golden.RequireEqual(t, []byte(table.String())) } func TestTableHeightExtra(t *testing.T) { table := New(). Height(100). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). Row("Spanish", "Hola", "¿Qué tal?") golden.RequireEqual(t, []byte(table.String())) } func TestTableHeightShrink(t *testing.T) { headers := []string{"LANGUAGE", "FORMAL", "INFORMAL"} rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } paddingStyleFunc := func(row, col int) lipgloss.Style { return TableStyle(row, col).Padding(1) } t.Run("NoBorderRow", func(t *testing.T) { for i := 1; i <= 9; i++ { t.Run(fmt.Sprintf("HeightOf%02d", i), func(t *testing.T) { table := New(). Height(i). Border(lipgloss.NormalBorder()). BorderRow(false). StyleFunc(TableStyle). Headers(headers...). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) }) } }) t.Run("WithBorderRow", func(t *testing.T) { for i := 1; i <= 13; i++ { t.Run(fmt.Sprintf("HeightOf%02d", i), func(t *testing.T) { table := New(). Height(i). Border(lipgloss.NormalBorder()). BorderRow(true). StyleFunc(TableStyle). Headers(headers...). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) }) } }) t.Run("NoBorderRowPadding", func(t *testing.T) { for i := 1; i <= 21; i++ { t.Run(fmt.Sprintf("HeightOf%02d", i), func(t *testing.T) { table := New(). Height(i). Border(lipgloss.NormalBorder()). BorderRow(false). StyleFunc(paddingStyleFunc). Headers(headers...). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) }) } }) t.Run("WithBorderRowPadding", func(t *testing.T) { for i := 1; i <= 25; i++ { t.Run(fmt.Sprintf("HeightOf%02d", i), func(t *testing.T) { table := New(). Height(i). Border(lipgloss.NormalBorder()). BorderRow(true). StyleFunc(paddingStyleFunc). Headers(headers...). Rows(rows...) golden.RequireEqual(t, []byte(table.String())) }) } }) } func TestTableHeightWithYOffset(t *testing.T) { // This test exists to check for a bug/edge case when the table has an // offset and the height is set. table := New(). Height(8). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). Row("Spanish", "Hola", "¿Qué tal?"). YOffset(1) golden.RequireEqual(t, []byte(table.String())) } func TestStyleFunc(t *testing.T) { tests := []struct { name string style StyleFunc }{ { "RightAlignedTextWithMargins", func(row, col int) lipgloss.Style { switch { case row == HeaderRow: return lipgloss.NewStyle().Align(lipgloss.Center) default: return lipgloss.NewStyle().Margin(0, 1).Align(lipgloss.Right) } }, }, { "MarginAndPaddingSet", // this test case uses background colors to differentiate margins // and padding. func(row, col int) lipgloss.Style { switch { case row == HeaderRow: return lipgloss.NewStyle().Align(lipgloss.Center) default: return lipgloss.NewStyle(). Padding(1). Margin(1). // keeping right-aligned text as it's the most likely to // be broken when truncated. Align(lipgloss.Right). Background(lipgloss.Color("#874bfc")) } }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { table := New(). Border(lipgloss.NormalBorder()). StyleFunc(tc.style). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). Row("Spanish", "Hola", "¿Qué tal?") golden.RequireEqual(t, []byte(table.String())) }) } } func TestClearRows(t *testing.T) { defer func() { if r := recover(); r != nil { t.Fatalf("had to recover: %v", r) } }() table := New(). Border(lipgloss.NormalBorder()). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo") table.ClearRows() table.Row("French", "Bonjour", "Salut") // String() will try to get the rows from table.data table.String() } func TestContentWrapping(t *testing.T) { tests := []struct { name string headers []string data [][]string wrap bool styleFunc StyleFunc }{ { "LongRowContent", []string{"Name", "Description", "Type", "Required", "Default"}, [][]string{{"command", "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", "yes", "hello", "yep"}}, true, TableStyle, }, { "LongRowContentNoWrap", []string{"Name", "Description", "Type", "Required", "Default"}, [][]string{{"command", "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", "yes", "hello", "yep"}}, false, TableStyle, }, { "LongRowContentNoWrapNoMargins", []string{"Name", "Description", "Type", "Required", "Default"}, [][]string{{"command", "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", "yes", "hello", "yep"}}, false, func(row, col int) lipgloss.Style { switch { case row == HeaderRow: return lipgloss.NewStyle().Padding(0).Align(lipgloss.Center) default: return lipgloss.NewStyle().Padding(0) } }, }, { "LongRowContentNoWrapCustomMargins", []string{"Name", "Description", "Type", "Required", "Default"}, [][]string{{"command", "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", "yes", "hello", "yep"}}, false, func(row, col int) lipgloss.Style { switch { case row == HeaderRow: return lipgloss.NewStyle().Padding(0, 2).Align(lipgloss.Center) default: return lipgloss.NewStyle().Padding(0, 2) } }, }, { "MissingRowContent", []string{"Name", "Description", "Type", "Required", "Default"}, [][]string{{"command", "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", "yes", "", ""}}, true, TableStyle, }, { "LongHeaderContentLongAndShortRows", []string{"Destination", "Why are you going on this trip? Is it a hot or cold climate?", "Affordability"}, [][]string{ {"Mexico", "I want to go somewhere hot, dry, and affordable. Mexico has really good food, just don't drink tap water!", "$"}, {"New York", "I'm thinking about going during the Christmas season to check out Rockefeller center. Might be cold though...", "$$$"}, {"California", "", "$$$"}, }, true, TableStyle, }, { "LongTextDifferentLanguages", []string{"Hello", "你好", "مرحبًا", "안녕하세요"}, [][]string{ { "Lorem ipsum dolor sit amet, regione detracto eos an. Has ei quidam hendrerit intellegebat, id tamquam iudicabit necessitatibus ius, at errem officiis hendrerit mei. Exerci noster at has, sit id tota convenire, vel ex rebum inciderint liberavisse. Quaeque delectus corrumpit cu cum.", `耐許ヱヨカハ調出あゆ監件び理別よン國給災レホチ権輝モエフ会割もフ響3現エツ文時しだびほ経機ムイメフ敗文ヨク現義なさド請情ゆじょて憶主管州けでふく。排ゃわつげ美刊ヱミ出見ツ南者オ抜豆ハトロネ論索モネニイ任償スヲ話破リヤヨ秒止口イセソス止央のさ食周健でてつだ官送ト読聴遊容ひるべ。際ぐドらづ市居ネムヤ研校35岩6繹ごわク報拐イ革深52球ゃレスご究東スラ衝3間ラ録占たス。 禁にンご忘康ざほぎル騰般ねど事超スんいう真表何カモ自浩ヲシミ図客線るふ静王ぱーま写村月掛焼詐面ぞゃ。昇強ごントほ価保キ族85岡モテ恋困ひりこな刊並せご出来ぼぎむう点目ヲウ止環公ニレ事応タス必書タメムノ当84無信升ちひょ。価ーぐ中客テサ告覧ヨトハ極整ラ得95稿はかラせ江利ス宏丸霊ミ考整ス静将ず業巨職ノラホ収嗅ざな。`, "شيء قد للحكومة والكوري الأوروبيّون, بوابة تعديل واعتلاء ضرب بـ. إذ أسر اتّجة اعلان, ٣٠ اكتوبر العصبة استمرار ومن. أفاق للسيطرة التاريخ، مع بحث, كلّ اتّجة القوى مع. فبعد ايطاليا، تم حتى, لكل تم جسيمة الإحتفاظ وباستثناء, عل فرنسا وانتهاءً الإقتصادية عرض. ونتج دأبوا إحكام بال إذ. لغات عملية وتم مع, وصل بداية وبغطاء البرية بل, أي قررت بلاده فكانت حدى", "版応道潟部中幕爆営報門案名見壌府。博健必権次覧編仕断青場内凄新東深簿代供供。守聞書神秀同浜東波恋闘秀。未格打好作器来利阪持西焦朝三女。権幽問季負娘購合旧資健載員式活陸。未倍校朝遺続術吉迎暮広知角亡志不説空住。法省当死年勝絡聞方北投健。室分性山天態意画詳知浅方裁。変激伝阜中野品省載嗅闘額端反。中必台際造事寄民経能前作臓", "각급 선거관리위원회의 조직·직무범위 기타 필요한 사항은 법률로 정한다. 임시회의 회기는 30일을 초과할 수 없다. 국가는 여자의 복지와 권익의 향상을 위하여 노력하여야 한다. 국군의 조직과 편성은 법률로 정한다.", }, }, true, TableStyle, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { table := New(). Headers(tc.headers...). Rows(tc.data...). Width(80). StyleFunc(tc.styleFunc). Wrap(tc.wrap) golden.RequireEqual(t, []byte(table.String())) }) } } func TestContentWrapping_WithPadding(t *testing.T) { tests := []struct { name string headers []string data [][]string }{ { "LongRowContent", []string{"Name", "Description", "Type", "Required", "Default"}, [][]string{{"command", "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", "yes", "hello", "yep"}}, }, { "MissingRowContent", []string{"Name", "Description", "Type", "Required", "Default"}, [][]string{{"command", "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", "yes", "", ""}}, }, { "LongHeaderContentLongAndShortRows", []string{"Destination", "Why are you going on this trip? Is it a hot or cold climate?", "Affordability"}, [][]string{ {"Mexico", "I want to go somewhere hot, dry, and affordable. Mexico has really good food, just don't drink tap water!", "$"}, {"New York", "I'm thinking about going during the Christmas season to check out Rockefeller center. Might be cold though...", "$$$"}, {"California", "", "$$$"}, }, }, { "LongTextDifferentLanguages", []string{"Hello", "你好", "مرحبًا", "안녕하세요"}, [][]string{ { "", `耐許ヱヨカハ調出あゆ監件び理別よン國給災レホチ権輝モエフ会割もフ響3現エツ文時しだびほ経機ムイメフ敗文ヨク現義なさド請情ゆじょて憶主管州けでふく。排ゃわつげ美刊ヱミ出見ツ南者オ抜豆ハトロネ論索モネニイ任償スヲ話破リヤヨ秒止口イセソス止央のさ食周健でてつだ官送ト読聴遊容ひるべ。際ぐドらづ市居ネムヤ研校35岩6繹ごわク報拐イ革深52球ゃレスご究東スラ衝3間ラ録占たス。 禁にンご忘康ざほぎル騰般ねど事超スんいう真表何カモ自浩ヲシミ図客線るふ静王ぱーま写村月掛焼詐面ぞゃ。昇強ごントほ価保キ族85岡モテ恋困ひりこな刊並せご出来ぼぎむう点目ヲウ止環公ニレ事応タス必書タメムノ当84無信升ちひょ。価ーぐ中客テサ告覧ヨトハ極整ラ得95稿はかラせ江利ス宏丸霊ミ考整ス静将ず業巨職ノラホ収嗅ざな。`, "شيء قد للحكومة والكوري الأوروبيّون, بوابة تعديل واعتلاء ضرب بـ. إذ أسر اتّجة اعلان, ٣٠ اكتوبر العصبة استمرار ومن. أفاق للسيطرة التاريخ، مع بحث, كلّ اتّجة القوى مع. فبعد ايطاليا، تم حتى, لكل تم جسيمة الإحتفاظ وباستثناء, عل فرنسا وانتهاءً الإقتصادية عرض. ونتج دأبوا إحكام بال إذ. لغات عملية وتم مع, وصل بداية وبغطاء البرية بل, أي قررت بلاده فكانت حدى", "版応道潟部中幕爆営報門案名見壌府。博健必権次覧編仕断青場内凄新東深簿代供供。守聞書神秀同浜東波恋闘秀。未格打好作器来利阪持西焦朝三女。権幽問季負娘購合旧資健載員式活陸。未倍校朝遺続術吉迎暮広知角亡志不説空住。法省当死年勝絡聞方北投健。室分性山天態意画詳知浅方裁。変激伝阜中野品省載嗅闘額端反。中必台際造事寄民経能前作臓", "각급 선거관리위원회의 조직·직무범위 기타 필요한 사항은 법률로 정한다. 임시회의 회기는 30일을 초과할 수 없다. 국가는 여자의 복지와 권익의 향상을 위하여 노력하여야 한다. 국군의 조직과 편성은 법률로 정한다.", }, }, }, } defaultWidth := 80 for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { table := New(). Headers(tc.headers...). Rows(tc.data...). StyleFunc(func(_, col int) lipgloss.Style { return lipgloss.NewStyle().Padding(0, 1) }) table.Width(defaultWidth) // check total width. if got := lipgloss.Width(table.String()); got != defaultWidth { t.Fatalf("Table is not the correct width. got %d, want %d", got, defaultWidth) t.Log(table.String()) } golden.RequireEqual(t, []byte(table.String())) }) } } func TestContentWrapping_WithMargins(t *testing.T) { tests := []struct { name string headers []string data [][]string }{ { "LongRowContent", []string{"Name", "Description", "Type", "Required", "Default"}, [][]string{{"command", "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", "yes", "hello", "yep"}}, }, { "MissingRowContent", []string{"Name", "Description", "Type", "Required", "Default"}, [][]string{{"command", "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", "yes", "", ""}}, }, { "LongHeaderContentLongAndShortRows", []string{"Destination", "Why are you going on this trip? Is it a hot or cold climate?", "Affordability"}, [][]string{ {"Mexico", "I want to go somewhere hot, dry, and affordable. Mexico has really good food, just don't drink tap water!", "$"}, {"New York", "I'm thinking about going during the Christmas season to check out Rockefeller center. Might be cold though...", "$$$"}, {"California", "", "$$$"}, }, }, { "LongTextDifferentLanguages", []string{"Hello", "你好", "مرحبًا", "안녕하세요"}, [][]string{ { "Lorem ipsum dolor sit amet, regione detracto eos an. Has ei quidam hendrerit intellegebat, id tamquam iudicabit necessitatibus ius, at errem officiis hendrerit mei. Exerci noster at has, sit id tota convenire, vel ex rebum inciderint liberavisse. Quaeque delectus corrumpit cu cum.", `耐許ヱヨカハ調出あゆ監件び理別よン國給災レホチ権輝モエフ会割もフ響3現エツ文時しだびほ経機ムイメフ敗文ヨク現義なさド請情ゆじょて憶主管州けでふく。排ゃわつげ美刊ヱミ出見ツ南者オ抜豆ハトロネ論索モネニイ任償スヲ話破リヤヨ秒止口イセソス止央のさ食周健でてつだ官送ト読聴遊容ひるべ。際ぐドらづ市居ネムヤ研校35岩6繹ごわク報拐イ革深52球ゃレスご究東スラ衝3間ラ録占たス。 禁にンご忘康ざほぎル騰般ねど事超スんいう真表何カモ自浩ヲシミ図客線るふ静王ぱーま写村月掛焼詐面ぞゃ。昇強ごントほ価保キ族85岡モテ恋困ひりこな刊並せご出来ぼぎむう点目ヲウ止環公ニレ事応タス必書タメムノ当84無信升ちひょ。価ーぐ中客テサ告覧ヨトハ極整ラ得95稿はかラせ江利ス宏丸霊ミ考整ス静将ず業巨職ノラホ収嗅ざな。`, "شيء قد للحكومة والكوري الأوروبيّون, بوابة تعديل واعتلاء ضرب بـ. إذ أسر اتّجة اعلان, ٣٠ اكتوبر العصبة استمرار ومن. أفاق للسيطرة التاريخ، مع بحث, كلّ اتّجة القوى مع. فبعد ايطاليا، تم حتى, لكل تم جسيمة الإحتفاظ وباستثناء, عل فرنسا وانتهاءً الإقتصادية عرض. ونتج دأبوا إحكام بال إذ. لغات عملية وتم مع, وصل بداية وبغطاء البرية بل, أي قررت بلاده فكانت حدى", "版応道潟部中幕爆営報門案名見壌府。博健必権次覧編仕断青場内凄新東深簿代供供。守聞書神秀同浜東波恋闘秀。未格打好作器来利阪持西焦朝三女。権幽問季負娘購合旧資健載員式活陸。未倍校朝遺続術吉迎暮広知角亡志不説空住。法省当死年勝絡聞方北投健。室分性山天態意画詳知浅方裁。変激伝阜中野品省載嗅闘額端反。中必台際造事寄民経能前作臓", "각급 선거관리위원회의 조직·직무범위 기타 필요한 사항은 법률로 정한다. 임시회의 회기는 30일을 초과할 수 없다. 국가는 여자의 복지와 권익의 향상을 위하여 노력하여야 한다. 국군의 조직과 편성은 법률로 정한다.", }, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { table := New(). Headers(tc.headers...). Rows(tc.data...). StyleFunc(func(row, col int) lipgloss.Style { return lipgloss.NewStyle().Margin(0, 4) }) table.Width(80) golden.RequireEqual(t, []byte(table.String())) }) } } func TestContentWrapping_WithHeight(t *testing.T) { tests := []struct { name string headers []string data [][]string }{ { "LongHeaderContentLongAndShortRows", []string{"Destination", "Why are you going on this trip? Is it a hot or cold climate?", "Affordability"}, [][]string{ {"Mexico", "I want to go somewhere hot, dry, and affordable. Mexico has really good food, just don't drink tap water!", "$"}, {"New York", "I'm thinking about going during the Christmas season to check out Rockefeller center. Might be cold though...", "$$$"}, {"California", "", "$$$"}, {"Florida", "I want to go somewhere hot, humid, and affordable. Florida has really good food, just don't go during hurricane season!", "$$"}, {"Maine", "I'm thinking about going during the summer to check out Acadia National Park. Might be cold though...", "$$"}, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { for i := 5; i <= 35; i += 10 { t.Run(fmt.Sprintf("HeightOf%02d", i), func(t *testing.T) { table := New(). Height(i). Width(60). Border(lipgloss.NormalBorder()). BorderRow(false). StyleFunc(TableStyle). Headers(tc.headers...). Rows(tc.data...) golden.RequireEqual(t, []byte(table.String())) }) } }) } } func TestContentWrapping_ColumnWidth(t *testing.T) { tests := []struct { name string headers []string data [][]string }{ { "LongRowContent", []string{"Name", "Description", "Type", "Required", "Default"}, [][]string{{"command", "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", "yes", "hello", "yep"}}, }, { "MissingRowContent", []string{"Name", "Description", "Type", "Required", "Default"}, [][]string{{"command", "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", "yes", "", ""}}, }, { "LongHeaderContentLongAndShortRows", []string{"Destination", "Why are you going on this trip? Is it a hot or cold climate?", "Affordability"}, [][]string{ {"Mexico", "I want to go somewhere hot, dry, and affordable. Mexico has really good food, just don't drink tap water!", "$"}, {"New York", "I'm thinking about going during the Christmas season to check out Rockefeller center. Might be cold though...", "$$$"}, {"California", "", "$$$"}, }, }, { "LongTextDifferentLanguages", []string{"Hello", "你好", "مرحبًا", "안녕하세요"}, [][]string{ { "Lorem ipsum dolor sit amet, regione detracto eos an. Has ei quidam hendrerit intellegebat, id tamquam iudicabit necessitatibus ius, at errem officiis hendrerit mei. Exerci noster at has, sit id tota convenire, vel ex rebum inciderint liberavisse. Quaeque delectus corrumpit cu cum.", `耐許ヱヨカハ調出あゆ監件び理別よン國給災レホチ権輝モエフ会割もフ響3現エツ文時しだびほ経機ムイメフ敗文ヨク現義なさド請情ゆじょて憶主管州けでふく。排ゃわつげ美刊ヱミ出見ツ南者オ抜豆ハトロネ論索モネニイ任償スヲ話破リヤヨ秒止口イセソス止央のさ食周健でてつだ官送ト読聴遊容ひるべ。際ぐドらづ市居ネムヤ研校35岩6繹ごわク報拐イ革深52球ゃレスご究東スラ衝3間ラ録占たス。 禁にンご忘康ざほぎル騰般ねど事超スんいう真表何カモ自浩ヲシミ図客線るふ静王ぱーま写村月掛焼詐面ぞゃ。昇強ごントほ価保キ族85岡モテ恋困ひりこな刊並せご出来ぼぎむう点目ヲウ止環公ニレ事応タス必書タメムノ当84無信升ちひょ。価ーぐ中客テサ告覧ヨトハ極整ラ得95稿はかラせ江利ス宏丸霊ミ考整ス静将ず業巨職ノラホ収嗅ざな。`, "شيء قد للحكومة والكوري الأوروبيّون, بوابة تعديل واعتلاء ضرب بـ. إذ أسر اتّجة اعلان, ٣٠ اكتوبر العصبة استمرار ومن. أفاق للسيطرة التاريخ، مع بحث, كلّ اتّجة القوى مع. فبعد ايطاليا، تم حتى, لكل تم جسيمة الإحتفاظ وباستثناء, عل فرنسا وانتهاءً الإقتصادية عرض. ونتج دأبوا إحكام بال إذ. لغات عملية وتم مع, وصل بداية وبغطاء البرية بل, أي قررت بلاده فكانت حدى", "版応道潟部中幕爆営報門案名見壌府。博健必権次覧編仕断青場内凄新東深簿代供供。守聞書神秀同浜東波恋闘秀。未格打好作器来利阪持西焦朝三女。権幽問季負娘購合旧資健載員式活陸。未倍校朝遺続術吉迎暮広知角亡志不説空住。法省当死年勝絡聞方北投健。室分性山天態意画詳知浅方裁。変激伝阜中野品省載嗅闘額端反。中必台際造事寄民経能前作臓", "각급 선거관리위원회의 조직·직무범위 기타 필요한 사항은 법률로 정한다. 임시회의 회기는 30일을 초과할 수 없다. 국가는 여자의 복지와 권익의 향상을 위하여 노력하여야 한다. 국군의 조직과 편성은 법률로 정한다.", }, }, }, } defaultWidth := 80 for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { table := New(). Headers(tc.headers...). Rows(tc.data...). StyleFunc(func(row, col int) lipgloss.Style { // If we set a specific cell width, it should count for all rows // in that column. if row == 0 && col == 1 { return lipgloss.NewStyle().Width(30) } // Set a column's width directly. if col == 2 { return lipgloss.NewStyle().Width(5) } return lipgloss.NewStyle() }) table.Width(defaultWidth) // check total width. if got := lipgloss.Width(table.String()); got != defaultWidth { t.Log(table.String()) t.Fatalf("Table is not the correct width. got %d, want %d", got, defaultWidth) } // check that width is overridden with a small value. if table.widths[2] != 5 { t.Log(table.String()) t.Fatalf("Did not set correct width value at column at index %d.\ngot %d, want %d", 2, table.widths[2], 5) } // check that width is overridden with a wide value. if table.widths[1] != 30 { t.Log(table.String()) t.Fatalf("Did not set correct width value at column at index %d.\ngot %d, want %d", 1, table.widths[1], 30) } t.Log(table.widths[2]) golden.RequireEqual(t, []byte(table.String())) }) } } // Test truncation for overflow and no wrap when combined. func TestTableOverFlowNoWrap(t *testing.T) { // LongTextDifferentLanguages headers := []string{"Hello", "你好", "مرحبًا", "안녕하세요"} data := [][]string{ { "Lorem ipsum dolor sit amet, regione detracto eos an. Has ei quidam hendrerit intellegebat, id tamquam iudicabit necessitatibus ius, at errem officiis hendrerit mei. Exerci noster at has, sit id tota convenire, vel ex rebum inciderint liberavisse. Quaeque delectus corrumpit cu cum.", `耐許ヱヨカハ調出あゆ監件び理別よン國給災レホチ権輝モエフ会割もフ響3現エツ文時しだびほ経機ムイメフ敗文ヨク現義なさド請情ゆじょて憶主管州けでふく。排ゃわつげ美刊ヱミ出見ツ南者オ抜豆ハトロネ論索モネニイ任償スヲ話破リヤヨ秒止口イセソス止央のさ食周健でてつだ官送ト読聴遊容ひるべ。際ぐドらづ市居ネムヤ研校35岩6繹ごわク報拐イ革深52球ゃレスご究東スラ衝3間ラ録占たス。 禁にンご忘康ざほぎル騰般ねど事超スんいう真表何カモ自浩ヲシミ図客線るふ静王ぱーま写村月掛焼詐面ぞゃ。昇強ごントほ価保キ族85岡モテ恋困ひりこな刊並せご出来ぼぎむう点目ヲウ止環公ニレ事応タス必書タメムノ当84無信升ちひょ。価ーぐ中客テサ告覧ヨトハ極整ラ得95稿はかラせ江利ス宏丸霊ミ考整ス静将ず業巨職ノラホ収嗅ざな。`, "شيء قد للحكومة والكوري الأوروبيّون, بوابة تعديل واعتلاء ضرب بـ. إذ أسر اتّجة اعلان, ٣٠ اكتوبر العصبة استمرار ومن. أفاق للسيطرة التاريخ، مع بحث, كلّ اتّجة القوى مع. فبعد ايطاليا، تم حتى, لكل تم جسيمة الإحتفاظ وباستثناء, عل فرنسا وانتهاءً الإقتصادية عرض. ونتج دأبوا إحكام بال إذ. لغات عملية وتم مع, وصل بداية وبغطاء البرية بل, أي قررت بلاده فكانت حدى", "각급 선거관리위원회의 조직·직무범위 기타 필요한 사항은 법률로 정한다. 임시회의 회기는 30일을 초과할 수 없다. 국가는 여자의 복지와 권익의 향상을 위하여 노력하여야 한다. 국군의 조직과 편성은 법률로 정한다.", "版応道潟部中幕爆営報門案名見壌府。博健必権次覧編仕断青場内凄新東深簿代供供。守聞書神秀同浜東波恋闘秀。未格打好作器来利阪持西焦朝三女。権幽問季負娘購合旧資健載員式活陸。未倍校朝遺続術吉迎暮広知角亡志不説空住。法省当死年勝絡聞方北投健。室分性山天態意画詳知浅方裁。変激伝阜中野品省載嗅闘額端反。中必台際造事寄民経能前作臓", }, {"Welcome", "いらっしゃいませ", "مرحباً", "환영", "欢迎"}, {"Goodbye", "さようなら", "مع السلامة", "안녕히 가세요", "再见"}, } tableHeight := 6 table := New(). Headers(headers...). Rows(data...). StyleFunc(TableStyle). Height(tableHeight). Width(80). Wrap(false) if got := lipgloss.Height(table.String()); got != tableHeight { t.Fatalf("got the wrong height. got %d, want %d", got, tableHeight) } golden.RequireEqual(t, []byte(table.String())) } func TestCarriageReturn(t *testing.T) { data := [][]string{ {"a0", "b0", "c0", "d0"}, {"a1", "b1.0\r\nb1.1\r\nb1.2\r\nb1.3\r\nb1.4\r\nb1.5\r\nb1.6", "c1", "d1"}, {"a2", "b2", "c2", "d2"}, {"a3", "b3", "c3", "d3"}, } table := New().Rows(data...).Border(lipgloss.NormalBorder()) golden.RequireEqual(t, []byte(table.String())) } func TestTableShrinkWithYOffset(t *testing.T) { rows := [][]string{ {"1", "Tokyo", "Japan", "37,274,000"}, {"2", "Delhi", "India", "32,065,760"}, {"3", "Shanghai", "China", "28,516,904"}, {"4", "Dhaka", "Bangladesh", "22,478,116"}, {"5", "São Paulo", "Brazil", "22,429,800"}, {"6", "Mexico City", "Mexico", "22,085,140"}, {"7", "Cairo", "Egypt", "21,750,020"}, {"8", "Beijing", "China", "21,333,332"}, {"9", "Mumbai", "India", "20,961,472"}, {"10", "Osaka", "Japan", "19,059,856"}, {"11", "Chongqing", "China", "16,874,740"}, {"12", "Karachi", "Pakistan", "16,839,950"}, {"13", "Istanbul", "Turkey", "15,636,243"}, {"14", "Kinshasa", "DR Congo", "15,628,085"}, {"15", "Lagos", "Nigeria", "15,387,639"}, {"16", "Buenos Aires", "Argentina", "15,369,919"}, {"17", "Kolkata", "India", "15,133,888"}, {"18", "Manila", "Philippines", "14,406,059"}, {"19", "Tianjin", "China", "14,011,828"}, {"20", "Guangzhou", "China", "13,964,637"}, {"21", "Rio De Janeiro", "Brazil", "13,634,274"}, {"22", "Lahore", "Pakistan", "13,541,764"}, {"23", "Bangalore", "India", "13,193,035"}, {"24", "Shenzhen", "China", "12,831,330"}, {"25", "Moscow", "Russia", "12,640,818"}, {"26", "Chennai", "India", "11,503,293"}, {"27", "Bogota", "Colombia", "11,344,312"}, {"28", "Paris", "France", "11,142,303"}, {"29", "Jakarta", "Indonesia", "11,074,811"}, {"30", "Lima", "Peru", "11,044,607"}, {"31", "Bangkok", "Thailand", "10,899,698"}, {"32", "Hyderabad", "India", "10,534,418"}, {"33", "Seoul", "South Korea", "9,975,709"}, {"34", "Nagoya", "Japan", "9,571,596"}, {"35", "London", "United Kingdom", "9,540,576"}, {"36", "Chengdu", "China", "9,478,521"}, {"37", "Nanjing", "China", "9,429,381"}, {"38", "Tehran", "Iran", "9,381,546"}, {"39", "Ho Chi Minh City", "Vietnam", "9,077,158"}, {"40", "Luanda", "Angola", "8,952,496"}, {"41", "Wuhan", "China", "8,591,611"}, {"42", "Xi An Shaanxi", "China", "8,537,646"}, {"43", "Ahmedabad", "India", "8,450,228"}, {"44", "Kuala Lumpur", "Malaysia", "8,419,566"}, {"45", "New York City", "United States", "8,177,020"}, {"46", "Hangzhou", "China", "8,044,878"}, {"47", "Surat", "India", "7,784,276"}, {"48", "Suzhou", "China", "7,764,499"}, {"49", "Hong Kong", "Hong Kong", "7,643,256"}, {"50", "Riyadh", "Saudi Arabia", "7,538,200"}, {"51", "Shenyang", "China", "7,527,975"}, {"52", "Baghdad", "Iraq", "7,511,920"}, {"53", "Dongguan", "China", "7,511,851"}, {"54", "Foshan", "China", "7,497,263"}, {"55", "Dar Es Salaam", "Tanzania", "7,404,689"}, {"56", "Pune", "India", "6,987,077"}, {"57", "Santiago", "Chile", "6,856,939"}, {"58", "Madrid", "Spain", "6,713,557"}, {"59", "Haerbin", "China", "6,665,951"}, {"60", "Toronto", "Canada", "6,312,974"}, {"61", "Belo Horizonte", "Brazil", "6,194,292"}, {"62", "Khartoum", "Sudan", "6,160,327"}, {"63", "Johannesburg", "South Africa", "6,065,354"}, {"64", "Singapore", "Singapore", "6,039,577"}, {"65", "Dalian", "China", "5,930,140"}, {"66", "Qingdao", "China", "5,865,232"}, {"67", "Zhengzhou", "China", "5,690,312"}, {"68", "Ji Nan Shandong", "China", "5,663,015"}, {"69", "Barcelona", "Spain", "5,658,472"}, {"70", "Saint Petersburg", "Russia", "5,535,556"}, {"71", "Abidjan", "Ivory Coast", "5,515,790"}, {"72", "Yangon", "Myanmar", "5,514,454"}, {"73", "Fukuoka", "Japan", "5,502,591"}, {"74", "Alexandria", "Egypt", "5,483,605"}, {"75", "Guadalajara", "Mexico", "5,339,583"}, {"76", "Ankara", "Turkey", "5,309,690"}, {"77", "Chittagong", "Bangladesh", "5,252,842"}, {"78", "Addis Ababa", "Ethiopia", "5,227,794"}, {"79", "Melbourne", "Australia", "5,150,766"}, {"80", "Nairobi", "Kenya", "5,118,844"}, {"81", "Hanoi", "Vietnam", "5,067,352"}, {"82", "Sydney", "Australia", "5,056,571"}, {"83", "Monterrey", "Mexico", "5,036,535"}, {"84", "Changsha", "China", "4,809,887"}, {"85", "Brasilia", "Brazil", "4,803,877"}, {"86", "Cape Town", "South Africa", "4,800,954"}, {"87", "Jiddah", "Saudi Arabia", "4,780,740"}, {"88", "Urumqi", "China", "4,710,203"}, {"89", "Kunming", "China", "4,657,381"}, {"90", "Changchun", "China", "4,616,002"}, {"91", "Hefei", "China", "4,496,456"}, {"92", "Shantou", "China", "4,490,411"}, {"93", "Xinbei", "Taiwan", "4,470,672"}, {"94", "Kabul", "Afghanistan", "4,457,882"}, {"95", "Ningbo", "China", "4,405,292"}, {"96", "Tel Aviv", "Israel", "4,343,584"}, {"97", "Yaounde", "Cameroon", "4,336,670"}, {"98", "Rome", "Italy", "4,297,877"}, {"99", "Shijiazhuang", "China", "4,285,135"}, {"100", "Montreal", "Canada", "4,276,526"}, } t.Run("NoHeaders", func(t *testing.T) { table := New(). Rows(rows...). YOffset(80). Height(45) content := table.String() golden.RequireEqual(t, []byte(content)) }) t.Run("WithHeaders", func(t *testing.T) { table := New(). Headers("Rank", "City", "Country", "Population"). Rows(rows...). YOffset(80). Height(45) content := table.String() golden.RequireEqual(t, []byte(content)) }) t.Run("WithBorderRow", func(t *testing.T) { table := New(). Headers("Rank", "City", "Country", "Population"). Rows(rows...). BorderRow(true). YOffset(80). Height(43) content := table.String() golden.RequireEqual(t, []byte(content)) }) } func TestBorderStyles(t *testing.T) { rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } tests := []struct { name string borderFunc func() lipgloss.Border topBottomBorders bool }{ {"NormalBorder", lipgloss.NormalBorder, true}, {"RoundedBorder", lipgloss.RoundedBorder, true}, {"BlockBorder", lipgloss.BlockBorder, true}, {"ThickBorder", lipgloss.ThickBorder, true}, {"HiddenBorder", lipgloss.HiddenBorder, true}, {"MarkdownBorder", lipgloss.MarkdownBorder, false}, {"ASCIIBorder", lipgloss.ASCIIBorder, true}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { table := New(). StyleFunc(TableStyle). Border(test.borderFunc()). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Rows(rows...). BorderTop(test.topBottomBorders). BorderBottom(test.topBottomBorders) golden.RequireEqual(t, []byte(table.String())) }) } } func TestNoFinalEmptyRowWhenOverflow(t *testing.T) { headers := []string{"Rank", "City", "Country", "Population"} rows := [][]string{ {"1", "Tokyo", "Japan", "37,274,000"}, {"2", "Delhi", "India", "32,065,760"}, {"3", "Shanghai", "China", "28,516,904"}, {"4", "Dhaka", "Bangladesh", "22,478,116"}, {"5", "São Paulo", "Brazil", "22,429,800"}, {"6", "Mexico City", "Mexico", "22,085,140"}, {"7", "Cairo", "Egypt", "21,750,020"}, {"8", "Beijing", "China", "21,333,332"}, {"9", "Mumbai", "India", "20,961,472"}, {"10", "Osaka", "Japan", "19,059,856"}, {"11", "Chongqing", "China", "16,874,740"}, {"12", "Karachi", "Pakistan", "16,839,950"}, {"13", "Istanbul", "Turkey", "15,636,243"}, {"14", "Kinshasa", "DR Congo", "15,628,085"}, {"15", "Lagos", "Nigeria", "15,387,639"}, {"16", "Buenos Aires", "Argentina", "15,369,919"}, } table := New(). Headers(headers...). Rows(rows...). BorderRow(true). Height(16) golden.RequireEqual(t, []byte(table.String())) } func TestExtraPaddingHeading(t *testing.T) { headers := []string{"Name", "Country of Origin", "Dunk-able"} rows := [][]string{ {"Chocolate Digestives", "UK", "Yes"}, {"Tim Tams", "Australia", "No"}, {"Hobnobs", "UK", "Yes"}, } styleFunc := func(row, col int) lipgloss.Style { return lipgloss.NewStyle().Padding(2, 2) } table := New(). Headers(headers...). Rows(rows...). StyleFunc(styleFunc) golden.RequireEqual(t, []byte(table.String())) } func TestExtraPaddingHeadingLong(t *testing.T) { headers := []string{"Looong Name", "Looong Country of Origin", "Looong Dunk-able"} rows := [][]string{ {"Chocolate Digestives", "UK", "Yes"}, {"Tim Tams", "Australia", "No"}, {"Hobnobs", "UK", "Yes"}, } styleFunc := func(row, col int) lipgloss.Style { return lipgloss.NewStyle().Padding(2, 2) } table := New(). Width(46). Headers(headers...). Rows(rows...). StyleFunc(styleFunc) golden.RequireEqual(t, []byte(table.String())) } func TestBorderedCells(t *testing.T) { headers := []string{"Name", "Country of Origin", "Dunk-able"} rows := [][]string{ {"Chocolate Digestives", "UK", "Yes"}, {"Tim Tams", "Australia", "No"}, {"Hobnobs", "UK", "Yes"}, } styleFunc := func(row, col int) lipgloss.Style { return lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()) } table := New(). Headers(headers...). Rows(rows...). StyleFunc(styleFunc) golden.RequireEqual(t, []byte(table.String())) } // Examples func ExampleTable_Wrap() { // LongTextDifferentLanguages headers := []string{"Hello", "你好", "مرحبًا", "안녕하세요"} data := [][]string{ { "Lorem ipsum dolor sit amet, regione detracto eos an. Has ei quidam hendrerit intellegebat, id tamquam iudicabit necessitatibus ius, at errem officiis hendrerit mei. Exerci noster at has, sit id tota convenire, vel ex rebum inciderint liberavisse. Quaeque delectus corrumpit cu cum.", `耐許ヱヨカハ調出あゆ監件び理別よン國給災レホチ権輝モエフ会割もフ響3現エツ文時しだびほ経機ムイメフ敗文ヨク現義なさド請情ゆじょて憶主管州けでふく。排ゃわつげ美刊ヱミ出見ツ南者オ抜豆ハトロネ論索モネニイ任償スヲ話破リヤヨ秒止口イセソス止央のさ食周健でてつだ官送ト読聴遊容ひるべ。際ぐドらづ市居ネムヤ研校35岩6繹ごわク報拐イ革深52球ゃレスご究東スラ衝3間ラ録占たス。 禁にンご忘康ざほぎル騰般ねど事超スんいう真表何カモ自浩ヲシミ図客線るふ静王ぱーま写村月掛焼詐面ぞゃ。昇強ごントほ価保キ族85岡モテ恋困ひりこな刊並せご出来ぼぎむう点目ヲウ止環公ニレ事応タス必書タメムノ当84無信升ちひょ。価ーぐ中客テサ告覧ヨトハ極整ラ得95稿はかラせ江利ス宏丸霊ミ考整ス静将ず業巨職ノラホ収嗅ざな。`, "شيء قد للحكومة والكوري الأوروبيّون, بوابة تعديل واعتلاء ضرب بـ. إذ أسر اتّجة اعلان, ٣٠ اكتوبر العصبة استمرار ومن. أفاق للسيطرة التاريخ، مع بحث, كلّ اتّجة القوى مع. فبعد ايطاليا، تم حتى, لكل تم جسيمة الإحتفاظ وباستثناء, عل فرنسا وانتهاءً الإقتصادية عرض. ونتج دأبوا إحكام بال إذ. لغات عملية وتم مع, وصل بداية وبغطاء البرية بل, أي قررت بلاده فكانت حدى", "版応道潟部中幕爆営報門案名見壌府。博健必権次覧編仕断青場内凄新東深簿代供供。守聞書神秀同浜東波恋闘秀。未格打好作器来利阪持西焦朝三女。権幽問季負娘購合旧資健載員式活陸。未倍校朝遺続術吉迎暮広知角亡志不説空住。法省当死年勝絡聞方北投健。室分性山天態意画詳知浅方裁。変激伝阜中野品省載嗅闘額端反。中必台際造事寄民経能前作臓", "각급 선거관리위원회의 조직·직무범위 기타 필요한 사항은 법률로 정한다. 임시회의 회기는 30일을 초과할 수 없다. 국가는 여자의 복지와 권익의 향상을 위하여 노력하여야 한다. 국군의 조직과 편성은 법률로 정한다.", }, } table := New(). Headers(headers...). Rows(data...). StyleFunc(TableStyle). Width(80). Wrap(false) fmt.Println(table.String()) table.Wrap(true) fmt.Println(table.String()) // Output: // // ┌──────────────┬───────────────┬───────────────┬───────────────┬───────────────┐ // │ Hello │ 你好 │ مرحبًا │ 안녕하세요 │ │ // ├──────────────┼───────────────┼───────────────┼───────────────┼───────────────┤ // │ Lorem ipsum… │ 耐許ヱヨカハ… │ شيء قد للحكو… │ 版応道潟部中… │ 각급 선거관… │ // └──────────────┴───────────────┴───────────────┴───────────────┴───────────────┘ // ┌──────────────┬───────────────┬───────────────┬───────────────┬───────────────┐ // │ Hello │ 你好 │ مرحبًا │ 안녕하세요 │ │ // ├──────────────┼───────────────┼───────────────┼───────────────┼───────────────┤ // │ Lorem ipsum │ 耐許ヱヨカハ │ شيء قد │ 版応道潟部中 │ 각급 │ // │ dolor sit │ 調出あゆ監件 │ للحكومة │ 幕爆営報門案 │ 선거관리위원 │ // │ amet, │ び理別よン國 │ والكوري │ 名見壌府。博 │ 회의 │ // │ regione │ 給災レホチ権 │ الأوروبيّون, │ 健必権次覧編 │ 조직·직무범위 │ // │ detracto eos │ 輝モエフ会割 │ بوابة تعديل │ 仕断青場内凄 │ 기타 필요한 │ // │ an. Has ei │ もフ響3現エツ │ واعتلاء ضرب │ 新東深簿代供 │ 사항은 법률로 │ // │ quidam │ 文時しだびほ │ بـ. إذ أسر │ 供。守聞書神 │ 정한다. │ // │ hendrerit │ 経機ムイメフ │ اتّجة اعلان, │ 秀同浜東波恋 │ 임시회의 │ // │ intellegebat │ 敗文ヨク現義 │ ٣٠ اكتوبر │ 闘秀。未格打 │ 회기는 30일을 │ // │ , id tamquam │ なさド請情ゆ │ العصبة │ 好作器来利阪 │ 초과할 수 │ // │ iudicabit │ じょて憶主管 │ استمرار ومن. │ 持西焦朝三女 │ 없다. 국가는 │ // │ necessitatib │ 州けでふく。 │ أفاق للسيطرة │ 。権幽問季負 │ 여자의 복지와 │ // │ us ius, at │ 排ゃわつげ美 │ التاريخ، مع │ 娘購合旧資健 │ 권익의 향상을 │ // │ errem │ 刊ヱミ出見ツ │ بحث, كلّ اتّجة │ 載員式活陸。 │ 위하여 │ // │ officiis │ 南者オ抜豆ハ │ القوى مع. │ 未倍校朝遺続 │ 노력하여야 │ // │ hendrerit │ トロネ論索モ │ فبعد ايطاليا، │ 術吉迎暮広知 │ 한다. 국군의 │ // │ mei. Exerci │ ネニイ任償ス │ تم حتى, لكل │ 角亡志不説空 │ 조직과 편성은 │ // │ noster at │ ヲ話破リヤヨ │ تم جسيمة │ 住。法省当死 │ 법률로 │ // │ has, sit id │ 秒止口イセソ │ الإحتفاظ │ 年勝絡聞方北 │ 정한다. │ // │ tota │ ス止央のさ食 │ وباستثناء, عل │ 投健。室分性 │ │ // │ convenire, │ 周健でてつだ │ فرنسا وانتهاءً │ 山天態意画詳 │ │ // │ vel ex rebum │ 官送ト読聴遊 │ الإقتصادية │ 知浅方裁。変 │ │ // │ inciderint │ 容ひるべ。際 │ عرض. ونتج │ 激伝阜中野品 │ │ // │ liberavisse. │ ぐドらづ市居 │ دأبوا إحكام │ 省載嗅闘額端 │ │ // │ Quaeque │ ネムヤ研校35 │ بال إذ. لغات │ 反。中必台際 │ │ // │ delectus │ 岩6繹ごわク報 │ عملية وتم مع, │ 造事寄民経能 │ │ // │ corrumpit cu │ 拐イ革深52球 │ وصل بداية │ 前作臓 │ │ // │ cum. │ ゃレスご究東 │ وبغطاء البرية │ │ │ // │ │ スラ衝3間ラ録 │ بل, أي قررت │ │ │ // │ │ 占たス。 │ بلاده فكانت │ │ │ // │ │ 禁にンご忘康 │ حدى │ │ │ // │ │ ざほぎル騰般 │ │ │ │ // │ │ ねど事超スん │ │ │ │ // │ │ いう真表何カ │ │ │ │ // │ │ モ自浩ヲシミ │ │ │ │ // │ │ 図客線るふ静 │ │ │ │ // │ │ 王ぱーま写村 │ │ │ │ // │ │ 月掛焼詐面ぞ │ │ │ │ // │ │ ゃ。昇強ごン │ │ │ │ // │ │ トほ価保キ族8 │ │ │ │ // │ │ 5岡モテ恋困ひ │ │ │ │ // │ │ りこな刊並せ │ │ │ │ // │ │ ご出来ぼぎむ │ │ │ │ // │ │ う点目ヲウ止 │ │ │ │ // │ │ 環公ニレ事応 │ │ │ │ // │ │ タス必書タメ │ │ │ │ // │ │ ムノ当84無信 │ │ │ │ // │ │ 升ちひょ。価 │ │ │ │ // │ │ ーぐ中客テサ │ │ │ │ // │ │ 告覧ヨトハ極 │ │ │ │ // │ │ 整ラ得95稿は │ │ │ │ // │ │ かラせ江利ス │ │ │ │ // │ │ 宏丸霊ミ考整 │ │ │ │ // │ │ ス静将ず業巨 │ │ │ │ // │ │ 職ノラホ収嗅 │ │ │ │ // │ │ ざな。 │ │ │ │ // └──────────────┴───────────────┴───────────────┴───────────────┴───────────────┘ } // Check that stylized wrapped content does not go beyond its cell. func TestWrapPreStyledContent(t *testing.T) { headers := []string{"Package", "Version", "Link"} data := [][]string{ {"sourcegit", "0.19", lipgloss.JoinHorizontal(lipgloss.Left, lipgloss.NewStyle().Foreground(lipgloss.Color("#31BB71")).Render("https://aur.archlinux.org/packages/sourcegit-bin"))}, {}, {"Welcome", "いらっしゃいませ", "مرحباً", "환영", "欢迎"}, {"Goodbye", "さようなら", "مع السلامة", "안녕히 가세요", "再见"}, } table := New(). Headers(headers...). Rows(data...). Width(80). Wrap(true) golden.RequireEqual(t, []byte(table.String())) } // Check that stylized wrapped content does not go beyond its cell. func TestWrapStyleFuncContent(t *testing.T) { headers := []string{"Package", "Version", "Link"} data := [][]string{ {"sourcegit", "0.19", "https://aur.archlinux.org/packages/sourcegit-bin"}, {"Welcome", "いらっしゃいませ", "مرحباً"}, {"Goodbye", "さようなら", "مع السلامة"}, } table := New(). Headers(headers...). Rows(data...). StyleFunc(func(row, col int) lipgloss.Style { if row == HeaderRow { return lipgloss.NewStyle() } if strings.Contains(data[row][col], "https://") { return lipgloss.NewStyle().Foreground(lipgloss.Color("#31BB71")) } return lipgloss.NewStyle() }). Width(60). Wrap(true) golden.RequireEqual(t, []byte(table.String())) } ================================================ FILE: table/testdata/TestBorderColumnsWithExtraRows.golden ================================================ ┌───────────────────────────────────────────────────┐ │ LANGUAGE FORMAL │ ├───────────────────────────────────────────────────┤ │ Chinese Nǐn hǎo Nǐ hǎo │ │ French Bonjour Salut Salut │ │ Japanese こんにちは やあ │ │ Russian Zdravstvuyte Privet Privet Privet │ │ Spanish Hola ¿Qué tal? │ └───────────────────────────────────────────────────┘ ================================================ FILE: table/testdata/TestBorderStyles/ASCIIBorder.golden ================================================ +----------+--------------+-----------+ | LANGUAGE | FORMAL | INFORMAL | +----------+--------------+-----------+ | Chinese | Nǐn hǎo | Nǐ hǎo | | French | Bonjour | Salut | | Japanese | こんにちは | やあ | | Russian | Zdravstvuyte | Privet | | Spanish | Hola | ¿Qué tal? | +----------+--------------+-----------+ ================================================ FILE: table/testdata/TestBorderStyles/BlockBorder.golden ================================================ ███████████████████████████████████████ █ LANGUAGE █ FORMAL █ INFORMAL █ ███████████████████████████████████████ █ Chinese █ Nǐn hǎo █ Nǐ hǎo █ █ French █ Bonjour █ Salut █ █ Japanese █ こんにちは █ やあ █ █ Russian █ Zdravstvuyte █ Privet █ █ Spanish █ Hola █ ¿Qué tal? █ ███████████████████████████████████████ ================================================ FILE: table/testdata/TestBorderStyles/HiddenBorder.golden ================================================ LANGUAGE FORMAL INFORMAL Chinese Nǐn hǎo Nǐ hǎo French Bonjour Salut Japanese こんにちは やあ Russian Zdravstvuyte Privet Spanish Hola ¿Qué tal? ================================================ FILE: table/testdata/TestBorderStyles/MarkdownBorder.golden ================================================ | LANGUAGE | FORMAL | INFORMAL | |----------|--------------|-----------| | Chinese | Nǐn hǎo | Nǐ hǎo | | French | Bonjour | Salut | | Japanese | こんにちは | やあ | | Russian | Zdravstvuyte | Privet | | Spanish | Hola | ¿Qué tal? | ================================================ FILE: table/testdata/TestBorderStyles/NormalBorder.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestBorderStyles/RoundedBorder.golden ================================================ ╭──────────┬──────────────┬───────────╮ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ ╰──────────┴──────────────┴───────────╯ ================================================ FILE: table/testdata/TestBorderStyles/ThickBorder.golden ================================================ ┏━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━┓ ┃ LANGUAGE ┃ FORMAL ┃ INFORMAL ┃ ┣━━━━━━━━━━╋━━━━━━━━━━━━━━╋━━━━━━━━━━━┫ ┃ Chinese ┃ Nǐn hǎo ┃ Nǐ hǎo ┃ ┃ French ┃ Bonjour ┃ Salut ┃ ┃ Japanese ┃ こんにちは ┃ やあ ┃ ┃ Russian ┃ Zdravstvuyte ┃ Privet ┃ ┃ Spanish ┃ Hola ┃ ¿Qué tal? ┃ ┗━━━━━━━━━━┻━━━━━━━━━━━━━━┻━━━━━━━━━━━┛ ================================================ FILE: table/testdata/TestBorderedCells.golden ================================================ ┌──────────────────────┬───────────────────┬───────────┐ │┌────────────────────┐│┌─────────────────┐│┌─────────┐│ ││Name │││Country of Origin│││Dunk-able││ │└────────────────────┘│└─────────────────┘│└─────────┘│ ├──────────────────────┼───────────────────┼───────────┤ │┌────────────────────┐│┌─────────────────┐│┌─────────┐│ ││Chocolate Digestives│││UK │││Yes ││ │└────────────────────┘│└─────────────────┘│└─────────┘│ │┌────────────────────┐│┌─────────────────┐│┌─────────┐│ ││Tim Tams │││Australia │││No ││ │└────────────────────┘│└─────────────────┘│└─────────┘│ │┌────────────────────┐│┌─────────────────┐│┌─────────┐│ ││Hobnobs │││UK │││Yes ││ │└────────────────────┘│└─────────────────┘│└─────────┘│ └──────────────────────┴───────────────────┴───────────┘ ================================================ FILE: table/testdata/TestCarriageReturn.golden ================================================ ┌──┬────┬──┬──┐ │a0│b0 │c0│d0│ │a1│b1.0│c1│d1│ │ │b1.1│ │ │ │ │b1.2│ │ │ │ │b1.3│ │ │ │ │b1.4│ │ │ │ │b1.5│ │ │ │ │b1.6│ │ │ │a2│b2 │c2│d2│ │a3│b3 │c3│d3│ └──┴────┴──┴──┘ ================================================ FILE: table/testdata/TestContentWrapping/LongHeaderContentLongAndShortRows.golden ================================================ ┌─────────────┬────────────────────────────────────────────────┬───────────────┐ │ Destination │ Why are you going on this trip? Is it a hot o… │ Affordability │ ├─────────────┼────────────────────────────────────────────────┼───────────────┤ │ Mexico │ I want to go somewhere hot, dry, and │ $ │ │ │ affordable. Mexico has really good food, just │ │ │ │ don't drink tap water! │ │ │ New York │ I'm thinking about going during the Christmas │ $$$ │ │ │ season to check out Rockefeller center. Might │ │ │ │ be cold though... │ │ │ California │ │ $$$ │ └─────────────┴────────────────────────────────────────────────┴───────────────┘ ================================================ FILE: table/testdata/TestContentWrapping/LongRowContent.golden ================================================ ┌─────────┬────────────────────────────────────────┬──────┬──────────┬─────────┐ │ Name │ Description │ Type │ Required │ Default │ ├─────────┼────────────────────────────────────────┼──────┼──────────┼─────────┤ │ command │ A command to be executed inside the │ yes │ hello │ yep │ │ │ container to assess its health. Each │ │ │ │ │ │ space delimited token of the command │ │ │ │ │ │ is a separate array element. Commands │ │ │ │ │ │ exiting 0 are considered to be │ │ │ │ │ │ successful probes, whilst all other │ │ │ │ │ │ exit codes are considered failures. │ │ │ │ └─────────┴────────────────────────────────────────┴──────┴──────────┴─────────┘ ================================================ FILE: table/testdata/TestContentWrapping/LongRowContentNoWrap.golden ================================================ ┌─────────┬────────────────────────────────────────┬──────┬──────────┬─────────┐ │ Name │ Description │ Type │ Required │ Default │ ├─────────┼────────────────────────────────────────┼──────┼──────────┼─────────┤ │ command │ A command to be executed inside the c… │ yes │ hello │ yep │ └─────────┴────────────────────────────────────────┴──────┴──────────┴─────────┘ ================================================ FILE: table/testdata/TestContentWrapping/LongRowContentNoWrapCustomMargins.golden ================================================ ┌───────────┬───────────────────────────────────────┬───────┬─────────┬────────┐ │ Name │ Description │ Ty… │ Requ… │ Def… │ ├───────────┼───────────────────────────────────────┼───────┼─────────┼────────┤ │ command │ A command to be executed inside th… │ yes │ hello │ yep │ └───────────┴───────────────────────────────────────┴───────┴─────────┴────────┘ ================================================ FILE: table/testdata/TestContentWrapping/LongRowContentNoWrapNoMargins.golden ================================================ ┌───────┬────────────────────────────────────────────────┬────┬────────┬───────┐ │ Name │ Description │Type│Required│Default│ ├───────┼────────────────────────────────────────────────┼────┼────────┼───────┤ │command│A command to be executed inside the container t…│yes │hello │yep │ └───────┴────────────────────────────────────────────────┴────┴────────┴───────┘ ================================================ FILE: table/testdata/TestContentWrapping/LongTextDifferentLanguages.golden ================================================ ┌──────────────┬───────────────┬───────────────┬───────────────┬───────────────┐ │ Hello │ 你好 │ مرحبًا │ 안녕하세요 │ │ ├──────────────┼───────────────┼───────────────┼───────────────┼───────────────┤ │ Lorem ipsum │ 耐許ヱヨカハ │ شيء قد │ 版応道潟部中 │ 각급 │ │ dolor sit │ 調出あゆ監件 │ للحكومة │ 幕爆営報門案 │ 선거관리위원 │ │ amet, │ び理別よン國 │ والكوري │ 名見壌府。博 │ 회의 │ │ regione │ 給災レホチ権 │ الأوروبيّون, │ 健必権次覧編 │ 조직·직무범위 │ │ detracto eos │ 輝モエフ会割 │ بوابة تعديل │ 仕断青場内凄 │ 기타 필요한 │ │ an. Has ei │ もフ響3現エツ │ واعتلاء ضرب │ 新東深簿代供 │ 사항은 법률로 │ │ quidam │ 文時しだびほ │ بـ. إذ أسر │ 供。守聞書神 │ 정한다. │ │ hendrerit │ 経機ムイメフ │ اتّجة اعلان, │ 秀同浜東波恋 │ 임시회의 │ │ intellegebat │ 敗文ヨク現義 │ ٣٠ اكتوبر │ 闘秀。未格打 │ 회기는 30일을 │ │ , id tamquam │ なさド請情ゆ │ العصبة │ 好作器来利阪 │ 초과할 수 │ │ iudicabit │ じょて憶主管 │ استمرار ومن. │ 持西焦朝三女 │ 없다. 국가는 │ │ necessitatib │ 州けでふく。 │ أفاق للسيطرة │ 。権幽問季負 │ 여자의 복지와 │ │ us ius, at │ 排ゃわつげ美 │ التاريخ، مع │ 娘購合旧資健 │ 권익의 향상을 │ │ errem │ 刊ヱミ出見ツ │ بحث, كلّ اتّجة │ 載員式活陸。 │ 위하여 │ │ officiis │ 南者オ抜豆ハ │ القوى مع. │ 未倍校朝遺続 │ 노력하여야 │ │ hendrerit │ トロネ論索モ │ فبعد ايطاليا، │ 術吉迎暮広知 │ 한다. 국군의 │ │ mei. Exerci │ ネニイ任償ス │ تم حتى, لكل │ 角亡志不説空 │ 조직과 편성은 │ │ noster at │ ヲ話破リヤヨ │ تم جسيمة │ 住。法省当死 │ 법률로 │ │ has, sit id │ 秒止口イセソ │ الإحتفاظ │ 年勝絡聞方北 │ 정한다. │ │ tota │ ス止央のさ食 │ وباستثناء, عل │ 投健。室分性 │ │ │ convenire, │ 周健でてつだ │ فرنسا وانتهاءً │ 山天態意画詳 │ │ │ vel ex rebum │ 官送ト読聴遊 │ الإقتصادية │ 知浅方裁。変 │ │ │ inciderint │ 容ひるべ。際 │ عرض. ونتج │ 激伝阜中野品 │ │ │ liberavisse. │ ぐドらづ市居 │ دأبوا إحكام │ 省載嗅闘額端 │ │ │ Quaeque │ ネムヤ研校35 │ بال إذ. لغات │ 反。中必台際 │ │ │ delectus │ 岩6繹ごわク報 │ عملية وتم مع, │ 造事寄民経能 │ │ │ corrumpit cu │ 拐イ革深52球 │ وصل بداية │ 前作臓 │ │ │ cum. │ ゃレスご究東 │ وبغطاء البرية │ │ │ │ │ スラ衝3間ラ録 │ بل, أي قررت │ │ │ │ │ 占たス。 │ بلاده فكانت │ │ │ │ │ 禁にンご忘康 │ حدى │ │ │ │ │ ざほぎル騰般 │ │ │ │ │ │ ねど事超スん │ │ │ │ │ │ いう真表何カ │ │ │ │ │ │ モ自浩ヲシミ │ │ │ │ │ │ 図客線るふ静 │ │ │ │ │ │ 王ぱーま写村 │ │ │ │ │ │ 月掛焼詐面ぞ │ │ │ │ │ │ ゃ。昇強ごン │ │ │ │ │ │ トほ価保キ族8 │ │ │ │ │ │ 5岡モテ恋困ひ │ │ │ │ │ │ りこな刊並せ │ │ │ │ │ │ ご出来ぼぎむ │ │ │ │ │ │ う点目ヲウ止 │ │ │ │ │ │ 環公ニレ事応 │ │ │ │ │ │ タス必書タメ │ │ │ │ │ │ ムノ当84無信 │ │ │ │ │ │ 升ちひょ。価 │ │ │ │ │ │ ーぐ中客テサ │ │ │ │ │ │ 告覧ヨトハ極 │ │ │ │ │ │ 整ラ得95稿は │ │ │ │ │ │ かラせ江利ス │ │ │ │ │ │ 宏丸霊ミ考整 │ │ │ │ │ │ ス静将ず業巨 │ │ │ │ │ │ 職ノラホ収嗅 │ │ │ │ │ │ ざな。 │ │ │ │ └──────────────┴───────────────┴───────────────┴───────────────┴───────────────┘ ================================================ FILE: table/testdata/TestContentWrapping/MissingRowContent.golden ================================================ ┌─────────┬────────────────────────────────────────┬──────┬──────────┬─────────┐ │ Name │ Description │ Type │ Required │ Default │ ├─────────┼────────────────────────────────────────┼──────┼──────────┼─────────┤ │ command │ A command to be executed inside the │ yes │ │ │ │ │ container to assess its health. Each │ │ │ │ │ │ space delimited token of the command │ │ │ │ │ │ is a separate array element. Commands │ │ │ │ │ │ exiting 0 are considered to be │ │ │ │ │ │ successful probes, whilst all other │ │ │ │ │ │ exit codes are considered failures. │ │ │ │ └─────────┴────────────────────────────────────────┴──────┴──────────┴─────────┘ ================================================ FILE: table/testdata/TestContentWrapping_ColumnWidth/LongHeaderContentLongAndShortRows.golden ================================================ ┌─────────────────────────────────────────┬──────────────────────────────┬─────┐ │Destination │Why are you going on this tri…│Affo…│ ├─────────────────────────────────────────┼──────────────────────────────┼─────┤ │Mexico │I want to go somewhere hot, │$ │ │ │dry, and affordable. Mexico │ │ │ │has really good food, just │ │ │ │don't drink tap water! │ │ │New York │I'm thinking about going │$$$ │ │ │during the Christmas season to│ │ │ │check out Rockefeller center. │ │ │ │Might be cold though... │ │ │California │ │$$$ │ └─────────────────────────────────────────┴──────────────────────────────┴─────┘ ================================================ FILE: table/testdata/TestContentWrapping_ColumnWidth/LongRowContent.golden ================================================ ┌─────────────┬──────────────────────────────┬─────┬─────────────┬─────────────┐ │Name │Description │Type │Required │Default │ ├─────────────┼──────────────────────────────┼─────┼─────────────┼─────────────┤ │command │A command to be executed │yes │hello │yep │ │ │inside the container to assess│ │ │ │ │ │its health. Each space │ │ │ │ │ │delimited token of the command│ │ │ │ │ │is a separate array element. │ │ │ │ │ │Commands exiting 0 are │ │ │ │ │ │considered to be successful │ │ │ │ │ │probes, whilst all other exit │ │ │ │ │ │codes are considered failures.│ │ │ │ └─────────────┴──────────────────────────────┴─────┴─────────────┴─────────────┘ ================================================ FILE: table/testdata/TestContentWrapping_ColumnWidth/LongTextDifferentLanguages.golden ================================================ ┌─────────────┬──────────────────────────────┬─────┬─────────────┬─────────────┐ │Hello │你好 │مرحبًا│안녕하세요 │ │ ├─────────────┼──────────────────────────────┼─────┼─────────────┼─────────────┤ │Lorem ipsum │耐許ヱヨカハ調出あゆ監件び理別│شيء │版応道潟部中 │각급 │ │dolor sit │よン國給災レホチ権輝モエフ会割│قد │幕爆営報門案 │선거관리위원 │ │amet, regione│もフ響3現エツ文時しだびほ経機 │للحكو│名見壌府。博 │회의 │ │detracto eos │ムイメフ敗文ヨク現義なさド請情│مة │健必権次覧編 │조직·직무범위│ │an. Has ei │ゆじょて憶主管州けでふく。排ゃ│والكو│仕断青場内凄 │기타 필요한 │ │quidam │わつげ美刊ヱミ出見ツ南者オ抜豆│ري │新東深簿代供 │사항은 법률로│ │hendrerit │ハトロネ論索モネニイ任償スヲ話│الأور│供。守聞書神 │정한다. │ │intellegebat,│破リヤヨ秒止口イセソス止央のさ│وبيّون│秀同浜東波恋 │임시회의 │ │id tamquam │食周健でてつだ官送ト読聴遊容ひ│, │闘秀。未格打 │회기는 30일을│ │iudicabit │るべ。際ぐドらづ市居ネムヤ研校│بوابة│好作器来利阪 │초과할 수 │ │necessitatibu│35岩6繹ごわク報拐イ革深52球ゃ │تعديل│持西焦朝三女 │없다. 국가는 │ │s ius, at │レスご究東スラ衝3間ラ録占たス │واعتل│。権幽問季負 │여자의 복지와│ │errem │。 │اء │娘購合旧資健 │권익의 향상을│ │officiis │禁にンご忘康ざほぎル騰般ねど事│ضرب │載員式活陸。 │위하여 │ │hendrerit │超スんいう真表何カモ自浩ヲシミ│بـ. │未倍校朝遺続 │노력하여야 │ │mei. Exerci │図客線るふ静王ぱーま写村月掛焼│إذ │術吉迎暮広知 │한다. 국군의 │ │noster at │詐面ぞゃ。昇強ごントほ価保キ族│أسر │角亡志不説空 │조직과 편성은│ │has, sit id │85岡モテ恋困ひりこな刊並せご出│اتّجة │住。法省当死 │법률로 │ │tota │来ぼぎむう点目ヲウ止環公ニレ事│اعلان│年勝絡聞方北 │정한다. │ │convenire, │応タス必書タメムノ当84無信升ち│, ٣٠ │投健。室分性 │ │ │vel ex rebum │ひょ。価ーぐ中客テサ告覧ヨトハ│اكتوب│山天態意画詳 │ │ │inciderint │極整ラ得95稿はかラせ江利ス宏丸│ر │知浅方裁。変 │ │ │liberavisse. │霊ミ考整ス静将ず業巨職ノラホ収│العصب│激伝阜中野品 │ │ │Quaeque │嗅ざな。 │ة │省載嗅闘額端 │ │ │delectus │ │استمر│反。中必台際 │ │ │corrumpit cu │ │ار │造事寄民経能 │ │ │cum. │ │ومن. │前作臓 │ │ │ │ │أفاق │ │ │ │ │ │للسيط│ │ │ │ │ │رة │ │ │ │ │ │التار│ │ │ │ │ │يخ، │ │ │ │ │ │مع │ │ │ │ │ │بحث, │ │ │ │ │ │كلّ │ │ │ │ │ │اتّجة │ │ │ │ │ │القوى│ │ │ │ │ │مع. │ │ │ │ │ │فبعد │ │ │ │ │ │ايطال│ │ │ │ │ │يا، │ │ │ │ │ │تم │ │ │ │ │ │حتى, │ │ │ │ │ │لكل │ │ │ │ │ │تم │ │ │ │ │ │جسيمة│ │ │ │ │ │الإحت│ │ │ │ │ │فاظ │ │ │ │ │ │وباست│ │ │ │ │ │ثناء,│ │ │ │ │ │عل │ │ │ │ │ │فرنسا│ │ │ │ │ │وانته│ │ │ │ │ │اءً │ │ │ │ │ │الإقت│ │ │ │ │ │صادية│ │ │ │ │ │عرض. │ │ │ │ │ │ونتج │ │ │ │ │ │دأبوا│ │ │ │ │ │إحكام│ │ │ │ │ │بال │ │ │ │ │ │إذ. │ │ │ │ │ │لغات │ │ │ │ │ │عملية│ │ │ │ │ │وتم │ │ │ │ │ │مع, │ │ │ │ │ │وصل │ │ │ │ │ │بداية│ │ │ │ │ │وبغطا│ │ │ │ │ │ء │ │ │ │ │ │البري│ │ │ │ │ │ة بل,│ │ │ │ │ │أي │ │ │ │ │ │قررت │ │ │ │ │ │بلاده│ │ │ │ │ │فكانت│ │ │ │ │ │حدى │ │ │ └─────────────┴──────────────────────────────┴─────┴─────────────┴─────────────┘ ================================================ FILE: table/testdata/TestContentWrapping_ColumnWidth/MissingRowContent.golden ================================================ ┌─────────────┬──────────────────────────────┬─────┬─────────────┬─────────────┐ │Name │Description │Type │Required │Default │ ├─────────────┼──────────────────────────────┼─────┼─────────────┼─────────────┤ │command │A command to be executed │yes │ │ │ │ │inside the container to assess│ │ │ │ │ │its health. Each space │ │ │ │ │ │delimited token of the command│ │ │ │ │ │is a separate array element. │ │ │ │ │ │Commands exiting 0 are │ │ │ │ │ │considered to be successful │ │ │ │ │ │probes, whilst all other exit │ │ │ │ │ │codes are considered failures.│ │ │ │ └─────────────┴──────────────────────────────┴─────┴─────────────┴─────────────┘ ================================================ FILE: table/testdata/TestContentWrapping_WithHeight/LongHeaderContentLongAndShortRows/HeightOf05.golden ================================================ ┌─────────────┬─────────────────────────────┬──────────────┐ │ Destination │ Why are you going on this … │ Affordabili… │ ├─────────────┼─────────────────────────────┼──────────────┤ │ … │ … │ … │ └─────────────┴─────────────────────────────┴──────────────┘ ================================================ FILE: table/testdata/TestContentWrapping_WithHeight/LongHeaderContentLongAndShortRows/HeightOf15.golden ================================================ ┌─────────────┬─────────────────────────────┬──────────────┐ │ Destination │ Why are you going on this … │ Affordabili… │ ├─────────────┼─────────────────────────────┼──────────────┤ │ Mexico │ I want to go somewhere hot, │ $ │ │ │ dry, and affordable. Mexico │ │ │ │ has really good food, just │ │ │ │ don't drink tap water! │ │ │ New York │ I'm thinking about going │ $$$ │ │ │ during the Christmas season │ │ │ │ to check out Rockefeller │ │ │ │ center. Might be cold │ │ │ │ though... │ │ │ California │ │ $$$ │ │ … │ … │ … │ └─────────────┴─────────────────────────────┴──────────────┘ ================================================ FILE: table/testdata/TestContentWrapping_WithHeight/LongHeaderContentLongAndShortRows/HeightOf25.golden ================================================ ┌─────────────┬─────────────────────────────┬──────────────┐ │ Destination │ Why are you going on this … │ Affordabili… │ ├─────────────┼─────────────────────────────┼──────────────┤ │ Mexico │ I want to go somewhere hot, │ $ │ │ │ dry, and affordable. Mexico │ │ │ │ has really good food, just │ │ │ │ don't drink tap water! │ │ │ New York │ I'm thinking about going │ $$$ │ │ │ during the Christmas season │ │ │ │ to check out Rockefeller │ │ │ │ center. Might be cold │ │ │ │ though... │ │ │ California │ │ $$$ │ │ Florida │ I want to go somewhere hot, │ $$ │ │ │ humid, and affordable. │ │ │ │ Florida has really good │ │ │ │ food, just don't go during │ │ │ │ hurricane season! │ │ │ Maine │ I'm thinking about going │ $$ │ │ │ during the summer to check │ │ │ │ out Acadia National Park. │ │ │ │ Might be cold though... │ │ └─────────────┴─────────────────────────────┴──────────────┘ ================================================ FILE: table/testdata/TestContentWrapping_WithHeight/LongHeaderContentLongAndShortRows/HeightOf35.golden ================================================ ┌─────────────┬─────────────────────────────┬──────────────┐ │ Destination │ Why are you going on this … │ Affordabili… │ ├─────────────┼─────────────────────────────┼──────────────┤ │ Mexico │ I want to go somewhere hot, │ $ │ │ │ dry, and affordable. Mexico │ │ │ │ has really good food, just │ │ │ │ don't drink tap water! │ │ │ New York │ I'm thinking about going │ $$$ │ │ │ during the Christmas season │ │ │ │ to check out Rockefeller │ │ │ │ center. Might be cold │ │ │ │ though... │ │ │ California │ │ $$$ │ │ Florida │ I want to go somewhere hot, │ $$ │ │ │ humid, and affordable. │ │ │ │ Florida has really good │ │ │ │ food, just don't go during │ │ │ │ hurricane season! │ │ │ Maine │ I'm thinking about going │ $$ │ │ │ during the summer to check │ │ │ │ out Acadia National Park. │ │ │ │ Might be cold though... │ │ └─────────────┴─────────────────────────────┴──────────────┘ ================================================ FILE: table/testdata/TestContentWrapping_WithMargins/LongHeaderContentLongAndShortRows.golden ================================================ ┌───────────────────┬───────────────────────────────────────┬──────────────────┐ │ Destination │ Why are you going on this trip… │ Affordabi… │ ├───────────────────┼───────────────────────────────────────┼──────────────────┤ │ Mexico │ I want to go somewhere hot, │ $ │ │ │ dry, and affordable. Mexico has │ │ │ │ really good food, just don't │ │ │ │ drink tap water! │ │ │ New York │ I'm thinking about going during │ $$$ │ │ │ the Christmas season to check │ │ │ │ out Rockefeller center. Might │ │ │ │ be cold though... │ │ │ California │ │ $$$ │ └───────────────────┴───────────────────────────────────────┴──────────────────┘ ================================================ FILE: table/testdata/TestContentWrapping_WithMargins/LongRowContent.golden ================================================ ┌───────────────┬────────────────────────┬───────────┬─────────────┬───────────┐ │ Name │ Description │ Ty… │ Requ… │ De… │ ├───────────────┼────────────────────────┼───────────┼─────────────┼───────────┤ │ command │ A command to be │ yes │ hello │ yep │ │ │ executed inside │ │ │ │ │ │ the container to │ │ │ │ │ │ assess its │ │ │ │ │ │ health. Each │ │ │ │ │ │ space delimited │ │ │ │ │ │ token of the │ │ │ │ │ │ command is a │ │ │ │ │ │ separate array │ │ │ │ │ │ element. │ │ │ │ │ │ Commands exiting │ │ │ │ │ │ 0 are considered │ │ │ │ │ │ to be successful │ │ │ │ │ │ probes, whilst │ │ │ │ │ │ all other exit │ │ │ │ │ │ codes are │ │ │ │ │ │ considered │ │ │ │ │ │ failures. │ │ │ │ └───────────────┴────────────────────────┴───────────┴─────────────┴───────────┘ ================================================ FILE: table/testdata/TestContentWrapping_WithMargins/LongTextDifferentLanguages.golden ================================================ ┌──────────────┬───────────────┬───────────────┬───────────────┬───────────────┐ │ Hello │ 你好 │ مرحبًا │ 안녕하… │ │ ├──────────────┼───────────────┼───────────────┼───────────────┼───────────────┤ │ Lorem │ 耐許ヱ │ شيء قد │ 版応道 │ 각급 │ │ ipsum │ ヨカハ │ للحكومة │ 潟部中 │ 선거관 │ │ dolor │ 調出あ │ والكوري │ 幕爆営 │ 리위원 │ │ sit │ ゆ監件 │ الأوروب │ 報門案 │ 회의 │ │ amet, │ び理別 │ يّون, │ 名見壌 │ 조직·직 │ │ region │ よン國 │ بوابة │ 府。博 │ 무범위 │ │ e │ 給災レ │ تعديل │ 健必権 │ 기타 │ │ detrac │ ホチ権 │ واعتلاء │ 次覧編 │ 필요한 │ │ to eos │ 輝モエ │ ضرب بـ. │ 仕断青 │ 사항은 │ │ an. │ フ会割 │ إذ أسر │ 場内凄 │ 법률로 │ │ Has ei │ もフ響3 │ اتّجة │ 新東深 │ 정한다. │ │ quidam │ 現エツ │ اعلان, │ 簿代供 │ 임시회 │ │ hendre │ 文時し │ ٣٠ │ 供。守 │ 의 │ │ rit │ だびほ │ اكتوبر │ 聞書神 │ 회기는 │ │ intell │ 経機ム │ العصبة │ 秀同浜 │ 30일을 │ │ egebat │ イメフ │ استمرار │ 東波恋 │ 초과할 │ │ , id │ 敗文ヨ │ ومن. │ 闘秀。 │ 수 │ │ tamqua │ ク現義 │ أفاق │ 未格打 │ 없다. │ │ m │ なさド │ للسيطرة │ 好作器 │ 국가는 │ │ iudica │ 請情ゆ │ التاريخ │ 来利阪 │ 여자의 │ │ bit │ じょて │ ، مع │ 持西焦 │ 복지와 │ │ necess │ 憶主管 │ بحث, كلّ │ 朝三女 │ 권익의 │ │ itatib │ 州けで │ اتّجة │ 。権幽 │ 향상을 │ │ us │ ふく。 │ القوى │ 問季負 │ 위하여 │ │ ius, │ 排ゃわ │ مع. │ 娘購合 │ 노력하 │ │ at │ つげ美 │ فبعد │ 旧資健 │ 여야 │ │ errem │ 刊ヱミ │ ايطاليا │ 載員式 │ 한다. │ │ offici │ 出見ツ │ ، تم │ 活陸。 │ 국군의 │ │ is │ 南者オ │ حتى, │ 未倍校 │ 조직과 │ │ hendre │ 抜豆ハ │ لكل تم │ 朝遺続 │ 편성은 │ │ rit │ トロネ │ جسيمة │ 術吉迎 │ 법률로 │ │ mei. │ 論索モ │ الإحتفا │ 暮広知 │ 정한다. │ │ Exerci │ ネニイ │ ظ │ 角亡志 │ │ │ noster │ 任償ス │ وباستثن │ 不説空 │ │ │ at │ ヲ話破 │ اء, عل │ 住。法 │ │ │ has, │ リヤヨ │ فرنسا │ 省当死 │ │ │ sit id │ 秒止口 │ وانتهاءً │ 年勝絡 │ │ │ tota │ イセソ │ الإقتصا │ 聞方北 │ │ │ conven │ ス止央 │ دية │ 投健。 │ │ │ ire, │ のさ食 │ عرض. │ 室分性 │ │ │ vel ex │ 周健で │ ونتج │ 山天態 │ │ │ rebum │ てつだ │ دأبوا │ 意画詳 │ │ │ incide │ 官送ト │ إحكام │ 知浅方 │ │ │ rint │ 読聴遊 │ بال إذ. │ 裁。変 │ │ │ libera │ 容ひる │ لغات │ 激伝阜 │ │ │ visse. │ べ。際 │ عملية │ 中野品 │ │ │ Quaequ │ ぐドら │ وتم مع, │ 省載嗅 │ │ │ e │ づ市居 │ وصل │ 闘額端 │ │ │ delect │ ネムヤ │ بداية │ 反。中 │ │ │ us │ 研校35 │ وبغطاء │ 必台際 │ │ │ corrum │ 岩6繹ご │ البرية │ 造事寄 │ │ │ pit cu │ わク報 │ بل, أي │ 民経能 │ │ │ cum. │ 拐イ革 │ قررت │ 前作臓 │ │ │ │ 深52球 │ بلاده │ │ │ │ │ ゃレス │ فكانت │ │ │ │ │ ご究東 │ حدى │ │ │ │ │ スラ衝3 │ │ │ │ │ │ 間ラ録 │ │ │ │ │ │ 占たス │ │ │ │ │ │ 。 │ │ │ │ │ │ 禁にン │ │ │ │ │ │ ご忘康 │ │ │ │ │ │ ざほぎ │ │ │ │ │ │ ル騰般 │ │ │ │ │ │ ねど事 │ │ │ │ │ │ 超スん │ │ │ │ │ │ いう真 │ │ │ │ │ │ 表何カ │ │ │ │ │ │ モ自浩 │ │ │ │ │ │ ヲシミ │ │ │ │ │ │ 図客線 │ │ │ │ │ │ るふ静 │ │ │ │ │ │ 王ぱー │ │ │ │ │ │ ま写村 │ │ │ │ │ │ 月掛焼 │ │ │ │ │ │ 詐面ぞ │ │ │ │ │ │ ゃ。昇 │ │ │ │ │ │ 強ごン │ │ │ │ │ │ トほ価 │ │ │ │ │ │ 保キ族8 │ │ │ │ │ │ 5岡モテ │ │ │ │ │ │ 恋困ひ │ │ │ │ │ │ りこな │ │ │ │ │ │ 刊並せ │ │ │ │ │ │ ご出来 │ │ │ │ │ │ ぼぎむ │ │ │ │ │ │ う点目 │ │ │ │ │ │ ヲウ止 │ │ │ │ │ │ 環公ニ │ │ │ │ │ │ レ事応 │ │ │ │ │ │ タス必 │ │ │ │ │ │ 書タメ │ │ │ │ │ │ ムノ当8 │ │ │ │ │ │ 4無信升 │ │ │ │ │ │ ちひょ │ │ │ │ │ │ 。価ー │ │ │ │ │ │ ぐ中客 │ │ │ │ │ │ テサ告 │ │ │ │ │ │ 覧ヨト │ │ │ │ │ │ ハ極整 │ │ │ │ │ │ ラ得95 │ │ │ │ │ │ 稿はか │ │ │ │ │ │ ラせ江 │ │ │ │ │ │ 利ス宏 │ │ │ │ │ │ 丸霊ミ │ │ │ │ │ │ 考整ス │ │ │ │ │ │ 静将ず │ │ │ │ │ │ 業巨職 │ │ │ │ │ │ ノラホ │ │ │ │ │ │ 収嗅ざ │ │ │ │ │ │ な。 │ │ │ │ └──────────────┴───────────────┴───────────────┴───────────────┴───────────────┘ ================================================ FILE: table/testdata/TestContentWrapping_WithMargins/MissingRowContent.golden ================================================ ┌───────────────┬────────────────────────────────┬───────────┬────────┬────────┐ │ Name │ Description │ Ty… │ │ │ ├───────────────┼────────────────────────────────┼───────────┼────────┼────────┤ │ command │ A command to be executed │ yes │ │ │ │ │ inside the container to │ │ │ │ │ │ assess its health. Each │ │ │ │ │ │ space delimited token of │ │ │ │ │ │ the command is a │ │ │ │ │ │ separate array element. │ │ │ │ │ │ Commands exiting 0 are │ │ │ │ │ │ considered to be │ │ │ │ │ │ successful probes, │ │ │ │ │ │ whilst all other exit │ │ │ │ │ │ codes are considered │ │ │ │ │ │ failures. │ │ │ │ └───────────────┴────────────────────────────────┴───────────┴────────┴────────┘ ================================================ FILE: table/testdata/TestContentWrapping_WithPadding/LongHeaderContentLongAndShortRows.golden ================================================ ┌─────────────┬────────────────────────────────────────────────┬───────────────┐ │ Destination │ Why are you going on this trip? Is it a hot o… │ Affordability │ ├─────────────┼────────────────────────────────────────────────┼───────────────┤ │ Mexico │ I want to go somewhere hot, dry, and │ $ │ │ │ affordable. Mexico has really good food, just │ │ │ │ don't drink tap water! │ │ │ New York │ I'm thinking about going during the Christmas │ $$$ │ │ │ season to check out Rockefeller center. Might │ │ │ │ be cold though... │ │ │ California │ │ $$$ │ └─────────────┴────────────────────────────────────────────────┴───────────────┘ ================================================ FILE: table/testdata/TestContentWrapping_WithPadding/LongRowContent.golden ================================================ ┌─────────┬────────────────────────────────────────┬──────┬──────────┬─────────┐ │ Name │ Description │ Type │ Required │ Default │ ├─────────┼────────────────────────────────────────┼──────┼──────────┼─────────┤ │ command │ A command to be executed inside the │ yes │ hello │ yep │ │ │ container to assess its health. Each │ │ │ │ │ │ space delimited token of the command │ │ │ │ │ │ is a separate array element. Commands │ │ │ │ │ │ exiting 0 are considered to be │ │ │ │ │ │ successful probes, whilst all other │ │ │ │ │ │ exit codes are considered failures. │ │ │ │ └─────────┴────────────────────────────────────────┴──────┴──────────┴─────────┘ ================================================ FILE: table/testdata/TestContentWrapping_WithPadding/LongTextDifferentLanguages.golden ================================================ ┌───────┬────────────────┬─────────────────┬─────────────────┬─────────────────┐ │ Hello │ 你好 │ مرحبًا │ 안녕하세요 │ │ ├───────┼────────────────┼─────────────────┼─────────────────┼─────────────────┤ │ │ 耐許ヱヨカハ調 │ شيء قد للحكومة │ 版応道潟部中幕 │ 각급 │ │ │ 出あゆ監件び理 │ والكوري │ 爆営報門案名見 │ 선거관리위원회 │ │ │ 別よン國給災レ │ الأوروبيّون, │ 壌府。博健必権 │ 의 │ │ │ ホチ権輝モエフ │ بوابة تعديل │ 次覧編仕断青場 │ 조직·직무범위 │ │ │ 会割もフ響3現 │ واعتلاء ضرب بـ. │ 内凄新東深簿代 │ 기타 필요한 │ │ │ エツ文時しだび │ إذ أسر اتّجة │ 供供。守聞書神 │ 사항은 법률로 │ │ │ ほ経機ムイメフ │ اعلان, ٣٠ │ 秀同浜東波恋闘 │ 정한다. │ │ │ 敗文ヨク現義な │ اكتوبر العصبة │ 秀。未格打好作 │ 임시회의 회기는 │ │ │ さド請情ゆじょ │ استمرار ومن. │ 器来利阪持西焦 │ 30일을 초과할 │ │ │ て憶主管州けで │ أفاق للسيطرة │ 朝三女。権幽問 │ 수 없다. 국가는 │ │ │ ふく。排ゃわつ │ التاريخ، مع │ 季負娘購合旧資 │ 여자의 복지와 │ │ │ げ美刊ヱミ出見 │ بحث, كلّ اتّجة │ 健載員式活陸。 │ 권익의 향상을 │ │ │ ツ南者オ抜豆ハ │ القوى مع. فبعد │ 未倍校朝遺続術 │ 위하여 │ │ │ トロネ論索モネ │ ايطاليا، تم │ 吉迎暮広知角亡 │ 노력하여야 │ │ │ ニイ任償スヲ話 │ حتى, لكل تم │ 志不説空住。法 │ 한다. 국군의 │ │ │ 破リヤヨ秒止口 │ جسيمة الإحتفاظ │ 省当死年勝絡聞 │ 조직과 편성은 │ │ │ イセソス止央の │ وباستثناء, عل │ 方北投健。室分 │ 법률로 정한다. │ │ │ さ食周健でてつ │ فرنسا وانتهاءً │ 性山天態意画詳 │ │ │ │ だ官送ト読聴遊 │ الإقتصادية عرض. │ 知浅方裁。変激 │ │ │ │ 容ひるべ。際ぐ │ ونتج دأبوا │ 伝阜中野品省載 │ │ │ │ ドらづ市居ネム │ إحكام بال إذ. │ 嗅闘額端反。中 │ │ │ │ ヤ研校35岩6繹 │ لغات عملية وتم │ 必台際造事寄民 │ │ │ │ ごわク報拐イ革 │ مع, وصل بداية │ 経能前作臓 │ │ │ │ 深52球ゃレスご │ وبغطاء البرية │ │ │ │ │ 究東スラ衝3間 │ بل, أي قررت │ │ │ │ │ ラ録占たス。 │ بلاده فكانت حدى │ │ │ │ │ 禁にンご忘康ざ │ │ │ │ │ │ ほぎル騰般ねど │ │ │ │ │ │ 事超スんいう真 │ │ │ │ │ │ 表何カモ自浩ヲ │ │ │ │ │ │ シミ図客線るふ │ │ │ │ │ │ 静王ぱーま写村 │ │ │ │ │ │ 月掛焼詐面ぞゃ │ │ │ │ │ │ 。昇強ごントほ │ │ │ │ │ │ 価保キ族85岡モ │ │ │ │ │ │ テ恋困ひりこな │ │ │ │ │ │ 刊並せご出来ぼ │ │ │ │ │ │ ぎむう点目ヲウ │ │ │ │ │ │ 止環公ニレ事応 │ │ │ │ │ │ タス必書タメム │ │ │ │ │ │ ノ当84無信升ち │ │ │ │ │ │ ひょ。価ーぐ中 │ │ │ │ │ │ 客テサ告覧ヨト │ │ │ │ │ │ ハ極整ラ得95稿 │ │ │ │ │ │ はかラせ江利ス │ │ │ │ │ │ 宏丸霊ミ考整ス │ │ │ │ │ │ 静将ず業巨職ノ │ │ │ │ │ │ ラホ収嗅ざな。 │ │ │ │ └───────┴────────────────┴─────────────────┴─────────────────┴─────────────────┘ ================================================ FILE: table/testdata/TestContentWrapping_WithPadding/MissingRowContent.golden ================================================ ┌─────────┬────────────────────────────────────────┬──────┬──────────┬─────────┐ │ Name │ Description │ Type │ Required │ Default │ ├─────────┼────────────────────────────────────────┼──────┼──────────┼─────────┤ │ command │ A command to be executed inside the │ yes │ │ │ │ │ container to assess its health. Each │ │ │ │ │ │ space delimited token of the command │ │ │ │ │ │ is a separate array element. Commands │ │ │ │ │ │ exiting 0 are considered to be │ │ │ │ │ │ successful probes, whilst all other │ │ │ │ │ │ exit codes are considered failures. │ │ │ │ └─────────┴────────────────────────────────────────┴──────┴──────────┴─────────┘ ================================================ FILE: table/testdata/TestExtraPaddingHeading.golden ================================================ ┌────────────────────────┬─────────────────────┬─────────────┐ │ │ │ │ │ │ │ │ │ Name │ Country of Origin │ Dunk-able │ │ │ │ │ │ │ │ │ ├────────────────────────┼─────────────────────┼─────────────┤ │ │ │ │ │ │ │ │ │ Chocolate Digestives │ UK │ Yes │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ Tim Tams │ Australia │ No │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ Hobnobs │ UK │ Yes │ │ │ │ │ │ │ │ │ └────────────────────────┴─────────────────────┴─────────────┘ ================================================ FILE: table/testdata/TestExtraPaddingHeadingLong.golden ================================================ ┌──────────────┬──────────────┬──────────────┐ │ │ │ │ │ │ │ │ │ Looong Na… │ Looong Co… │ Looong Du… │ │ │ │ │ │ │ │ │ ├──────────────┼──────────────┼──────────────┤ │ │ │ │ │ │ │ │ │ Chocolate │ UK │ Yes │ │ Digestives │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ Tim Tams │ Australia │ No │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ Hobnobs │ UK │ Yes │ │ │ │ │ │ │ │ │ └──────────────┴──────────────┴──────────────┘ ================================================ FILE: table/testdata/TestFilter.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestFilterInverse.golden ================================================ ┌──────────┬─────────┬──────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼─────────┼──────────┤ │ French │ Bonjour │ Salut │ └──────────┴─────────┴──────────┘ ================================================ FILE: table/testdata/TestInnerBordersOnly.golden ================================================ LANGUAGE │ FORMAL │ INFORMAL ──────────┼──────────────┼─────────── Chinese │ Nǐn hǎo │ Nǐ hǎo ──────────┼──────────────┼─────────── French │ Bonjour │ Salut ──────────┼──────────────┼─────────── Japanese │ こんにちは │ やあ ──────────┼──────────────┼─────────── Russian │ Zdravstvuyte │ Privet ──────────┼──────────────┼─────────── Spanish │ Hola │ ¿Qué tal? ================================================ FILE: table/testdata/TestMoreCellsThanHeaders.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestMoreCellsThanHeadersExtra.golden ================================================ ┌──────────┬──────────────┬───────────┬────────┬────────┐ │ LANGUAGE │ FORMAL │ │ │ │ ├──────────┼──────────────┼───────────┼────────┼────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ French │ Bonjour │ Salut │ Salut │ │ │ Japanese │ こんにちは │ やあ │ │ │ │ Russian │ Zdravstvuyte │ Privet │ Privet │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ │ │ └──────────┴──────────────┴───────────┴────────┴────────┘ ================================================ FILE: table/testdata/TestNoFinalEmptyRowWhenOverflow.golden ================================================ ┌────┬────────────┬──────────┬──────────┐ │Rank│City │Country │Population│ ├────┼────────────┼──────────┼──────────┤ │1 │Tokyo │Japan │37,274,000│ ├────┼────────────┼──────────┼──────────┤ │2 │Delhi │India │32,065,760│ ├────┼────────────┼──────────┼──────────┤ │3 │Shanghai │China │28,516,904│ ├────┼────────────┼──────────┼──────────┤ │4 │Dhaka │Bangladesh│22,478,116│ ├────┼────────────┼──────────┼──────────┤ │5 │São Paulo │Brazil │22,429,800│ ├────┼────────────┼──────────┼──────────┤ │… │… │… │… │ └────┴────────────┴──────────┴──────────┘ ================================================ FILE: table/testdata/TestStyleFunc/MarginAndPaddingSet.golden ================================================ ┌────────────┬────────────────┬─────────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├────────────┼────────────────┼─────────────┤ │ │ │ │ │   │   │   │ │   Chinese  │   Nǐn hǎo  │   Nǐ hǎo  │ │   │   │   │ │ │ │ │ │ │ │ │ │   │   │   │ │   French  │   Bonjour  │   Salut  │ │   │   │   │ │ │ │ │ │ │ │ │ │   │   │   │ │  Japanese  │   こんにちは  │   やあ  │ │   │   │   │ │ │ │ │ │ │ │ │ │   │   │   │ │   Russian  │  Zdravstvuyte  │   Privet  │ │   │   │   │ │ │ │ │ │ │ │ │ │   │   │   │ │   Spanish  │   Hola  │  ¿Qué tal?  │ │   │   │   │ │ │ │ │ └────────────┴────────────────┴─────────────┘ ================================================ FILE: table/testdata/TestStyleFunc/RightAlignedTextWithMargins.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTable.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableANSI.golden ================================================ ┌───────────┬────────┬──────┐ │ Fruit │ Color │ Code │ ├───────────┼────────┼──────┤ │ Apple │ Red │ 31 │ │ Lime │ Green │ 32 │ │ Banana │ Yellow │ 33 │ │ Blueberry │ Blue │ 34 │ └───────────┴────────┴──────┘ ================================================ FILE: table/testdata/TestTableBorder.golden ================================================ ╔══════════╦══════════════╦═══════════╗ ║ LANGUAGE ║ FORMAL ║ INFORMAL ║ ╠══════════╬══════════════╬═══════════╣ ║ Chinese ║ Nǐn hǎo ║ Nǐ hǎo ║ ║ French ║ Bonjour ║ Salut ║ ║ Japanese ║ こんにちは ║ やあ ║ ║ Russian ║ Zdravstvuyte ║ Privet ║ ║ Spanish ║ Hola ║ ¿Qué tal? ║ ╚══════════╩══════════════╩═══════════╝ ================================================ FILE: table/testdata/TestTableEmpty.golden ================================================ ┌──────────┬────────┬──────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼────────┼──────────┤ └──────────┴────────┴──────────┘ ================================================ FILE: table/testdata/TestTableExample.golden ================================================ ┌──────────┬───────────────────────────────┬─────────────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼───────────────────────────────┼─────────────────┤ │ Chinese │ 您好 │ 你好 │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Здравствуйте │ Привет │ │ Spanish │ Hola │ ¿Qué tal? │ │ English │ You look absolutely fabulous. │ How's it going? │ └──────────┴───────────────────────────────┴─────────────────┘ ================================================ FILE: table/testdata/TestTableHeightExact.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightExtra.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRow/HeightOf01.golden ================================================ ┌──────────┬──────────────┬───────────┐ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRow/HeightOf02.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRow/HeightOf03.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRow/HeightOf04.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ … │ … │ … │ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRow/HeightOf05.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRow/HeightOf06.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRow/HeightOf07.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRow/HeightOf08.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRow/HeightOf09.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf01.golden ================================================ ┌──────────┬──────────────┬───────────┐ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf02.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf03.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf04.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf05.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf06.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf07.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf08.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf09.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf10.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf11.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf12.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf13.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf14.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf15.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf16.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf17.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf18.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ │ │ │ │ │ Japanese │ こんにちは │ やあ │ │ │ │ │ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf19.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ │ │ │ │ │ Japanese │ こんにちは │ やあ │ │ │ │ │ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf20.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ │ │ │ │ │ Japanese │ こんにちは │ やあ │ │ │ │ │ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/NoBorderRowPadding/HeightOf21.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ │ │ │ │ │ Japanese │ こんにちは │ やあ │ │ │ │ │ │ │ │ │ │ Russian │ Zdravstvuyte │ Privet │ │ │ │ │ │ │ │ │ │ Spanish │ Hola │ ¿Qué tal? │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf01.golden ================================================ ┌──────────┬──────────────┬───────────┐ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf02.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf03.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf04.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ … │ … │ … │ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf05.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf06.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf07.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ ├──────────┼──────────────┼───────────┤ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf08.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ ├──────────┼──────────────┼───────────┤ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf09.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ ├──────────┼──────────────┼───────────┤ │ French │ Bonjour │ Salut │ ├──────────┼──────────────┼───────────┤ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf10.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ ├──────────┼──────────────┼───────────┤ │ French │ Bonjour │ Salut │ ├──────────┼──────────────┼───────────┤ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf11.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ ├──────────┼──────────────┼───────────┤ │ French │ Bonjour │ Salut │ ├──────────┼──────────────┼───────────┤ │ Japanese │ こんにちは │ やあ │ ├──────────┼──────────────┼───────────┤ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf12.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ ├──────────┼──────────────┼───────────┤ │ French │ Bonjour │ Salut │ ├──────────┼──────────────┼───────────┤ │ Japanese │ こんにちは │ やあ │ ├──────────┼──────────────┼───────────┤ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRow/HeightOf13.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ ├──────────┼──────────────┼───────────┤ │ French │ Bonjour │ Salut │ ├──────────┼──────────────┼───────────┤ │ Japanese │ こんにちは │ やあ │ ├──────────┼──────────────┼───────────┤ │ Russian │ Zdravstvuyte │ Privet │ ├──────────┼──────────────┼───────────┤ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf01.golden ================================================ ┌──────────┬──────────────┬───────────┐ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf02.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf03.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf04.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf05.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf06.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf07.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf08.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf09.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf10.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf11.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf12.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf13.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf14.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf15.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf16.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf17.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf18.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf19.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf20.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf21.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Japanese │ こんにちは │ やあ │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf22.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Japanese │ こんにちは │ やあ │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf23.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Japanese │ こんにちは │ やあ │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf24.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Japanese │ こんにちは │ やあ │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ … │ … │ … │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightShrink/WithBorderRowPadding/HeightOf25.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ │ │ │ │ LANGUAGE │ FORMAL │ INFORMAL │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ French │ Bonjour │ Salut │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Japanese │ こんにちは │ やあ │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Russian │ Zdravstvuyte │ Privet │ │ │ │ │ ├──────────┼──────────────┼───────────┤ │ │ │ │ │ Spanish │ Hola │ ¿Qué tal? │ │ │ │ │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeightWithYOffset.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableHeights.golden ================================================ ┌──────────────────┬─────────────────────────┐ │ EXPRESSION │ MEANING │ ├──────────────────┼─────────────────────────┤ │ │ │ │ Chutar o balde │ Literally translates │ │ │ to "kick the bucket." │ │ │ It's used when │ │ │ someone gives up or │ │ │ loses patience. │ │ │ │ │ │ │ │ Engolir sapos │ Literally means "to │ │ │ swallow frogs." It's │ │ │ used to describe │ │ │ someone who has to │ │ │ tolerate or endure │ │ │ unpleasant │ │ │ situations. │ │ │ │ │ │ │ │ Arroz de festa │ Literally means │ │ │ "party rice." It´s │ │ │ used to refer to │ │ │ someone who shows up │ │ │ everywhere. │ │ │ │ └──────────────────┴─────────────────────────┘ ================================================ FILE: table/testdata/TestTableMarginAndRightAlignment.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Arabic │ أهلين │ أهلا │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableMultiLineRowSeparator.golden ================================================ ┌──────────────────┬─────────────────────────┐ │ EXPRESSION │ MEANING │ ├──────────────────┼─────────────────────────┤ │ │ │ │ Chutar o balde │ Literally translates │ │ │ to "kick the bucket." │ │ │ It's used when │ │ │ someone gives up or │ │ │ loses patience. │ │ │ │ ├──────────────────┼─────────────────────────┤ │ │ │ │ Engolir sapos │ Literally means "to │ │ │ swallow frogs." It's │ │ │ used to describe │ │ │ someone who has to │ │ │ tolerate or endure │ │ │ unpleasant │ │ │ situations. │ │ │ │ ├──────────────────┼─────────────────────────┤ │ │ │ │ Arroz de festa │ Literally means │ │ │ "party rice." It´s │ │ │ used to refer to │ │ │ someone who shows up │ │ │ everywhere. │ │ │ │ └──────────────────┴─────────────────────────┘ ================================================ FILE: table/testdata/TestTableNoColumnSeparators.golden ================================================ ┌───────────────────────────────────┐ │ Chinese Nǐn hǎo Nǐ hǎo │ │ French Bonjour Salut │ │ Japanese こんにちは やあ │ │ Russian Zdravstvuyte Privet │ │ Spanish Hola ¿Qué tal? │ └───────────────────────────────────┘ ================================================ FILE: table/testdata/TestTableNoColumnSeparatorsWithHeaders.golden ================================================ ┌───────────────────────────────────┐ │ LANGUAGE FORMAL INFORMAL │ ├───────────────────────────────────┤ │ Chinese Nǐn hǎo Nǐ hǎo │ │ French Bonjour Salut │ │ Japanese こんにちは やあ │ │ Russian Zdravstvuyte Privet │ │ Spanish Hola ¿Qué tal? │ └───────────────────────────────────┘ ================================================ FILE: table/testdata/TestTableNoHeaders.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableNoStyleFunc.golden ================================================ ┌────────┬────────────┬─────────┐ │LANGUAGE│FORMAL │INFORMAL │ ├────────┼────────────┼─────────┤ │Chinese │Nǐn hǎo │Nǐ hǎo │ │French │Bonjour │Salut │ │Japanese│こんにちは │やあ │ │Russian │Zdravstvuyte│Privet │ │Spanish │Hola │¿Qué tal?│ └────────┴────────────┴─────────┘ ================================================ FILE: table/testdata/TestTableOverFlowNoWrap.golden ================================================ ┌──────────────┬───────────────┬───────────────┬───────────────┬───────────────┐ │ Hello │ 你好 │ مرحبًا │ 안녕하세요 │ │ ├──────────────┼───────────────┼───────────────┼───────────────┼───────────────┤ │ Lorem ipsum… │ 耐許ヱヨカハ… │ شيء قد للحكو… │ 각급 선거관… │ 版応道潟部中… │ │ … │ … │ … │ … │ … │ └──────────────┴───────────────┴───────────────┴───────────────┴───────────────┘ ================================================ FILE: table/testdata/TestTableRowSeparators/no_overflow.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ ├──────────┼──────────────┼───────────┤ │ French │ Bonjour │ Salut │ ├──────────┼──────────────┼───────────┤ │ Japanese │ こんにちは │ やあ │ ├──────────┼──────────────┼───────────┤ │ Russian │ Zdravstvuyte │ Privet │ ├──────────┼──────────────┼───────────┤ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableRowSeparators/with_overflow.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ ├──────────┼──────────────┼───────────┤ │ French │ Bonjour │ Salut │ ├──────────┼──────────────┼───────────┤ │ Japanese │ こんにちは │ やあ │ ├──────────┼──────────────┼───────────┤ │ … │ … │ … │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableRowSeparators.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ ├──────────┼──────────────┼───────────┤ │ French │ Bonjour │ Salut │ ├──────────┼──────────────┼───────────┤ │ Japanese │ こんにちは │ やあ │ ├──────────┼──────────────┼───────────┤ │ Russian │ Zdravstvuyte │ Privet │ ├──────────┼──────────────┼───────────┤ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableSetRows.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableShrinkWithYOffset/NoHeaders.golden ================================================ ┌───┬────────────────┬──────────────┬──────────┐ │58 │Madrid │Spain │6,713,557 │ │59 │Haerbin │China │6,665,951 │ │60 │Toronto │Canada │6,312,974 │ │61 │Belo Horizonte │Brazil │6,194,292 │ │62 │Khartoum │Sudan │6,160,327 │ │63 │Johannesburg │South Africa │6,065,354 │ │64 │Singapore │Singapore │6,039,577 │ │65 │Dalian │China │5,930,140 │ │66 │Qingdao │China │5,865,232 │ │67 │Zhengzhou │China │5,690,312 │ │68 │Ji Nan Shandong │China │5,663,015 │ │69 │Barcelona │Spain │5,658,472 │ │70 │Saint Petersburg│Russia │5,535,556 │ │71 │Abidjan │Ivory Coast │5,515,790 │ │72 │Yangon │Myanmar │5,514,454 │ │73 │Fukuoka │Japan │5,502,591 │ │74 │Alexandria │Egypt │5,483,605 │ │75 │Guadalajara │Mexico │5,339,583 │ │76 │Ankara │Turkey │5,309,690 │ │77 │Chittagong │Bangladesh │5,252,842 │ │78 │Addis Ababa │Ethiopia │5,227,794 │ │79 │Melbourne │Australia │5,150,766 │ │80 │Nairobi │Kenya │5,118,844 │ │81 │Hanoi │Vietnam │5,067,352 │ │82 │Sydney │Australia │5,056,571 │ │83 │Monterrey │Mexico │5,036,535 │ │84 │Changsha │China │4,809,887 │ │85 │Brasilia │Brazil │4,803,877 │ │86 │Cape Town │South Africa │4,800,954 │ │87 │Jiddah │Saudi Arabia │4,780,740 │ │88 │Urumqi │China │4,710,203 │ │89 │Kunming │China │4,657,381 │ │90 │Changchun │China │4,616,002 │ │91 │Hefei │China │4,496,456 │ │92 │Shantou │China │4,490,411 │ │93 │Xinbei │Taiwan │4,470,672 │ │94 │Kabul │Afghanistan │4,457,882 │ │95 │Ningbo │China │4,405,292 │ │96 │Tel Aviv │Israel │4,343,584 │ │97 │Yaounde │Cameroon │4,336,670 │ │98 │Rome │Italy │4,297,877 │ │99 │Shijiazhuang │China │4,285,135 │ │100│Montreal │Canada │4,276,526 │ └───┴────────────────┴──────────────┴──────────┘ ================================================ FILE: table/testdata/TestTableShrinkWithYOffset/WithBorderRow.golden ================================================ ┌────┬────────────────┬──────────────┬──────────┐ │Rank│City │Country │Population│ ├────┼────────────────┼──────────────┼──────────┤ │81 │Hanoi │Vietnam │5,067,352 │ ├────┼────────────────┼──────────────┼──────────┤ │82 │Sydney │Australia │5,056,571 │ ├────┼────────────────┼──────────────┼──────────┤ │83 │Monterrey │Mexico │5,036,535 │ ├────┼────────────────┼──────────────┼──────────┤ │84 │Changsha │China │4,809,887 │ ├────┼────────────────┼──────────────┼──────────┤ │85 │Brasilia │Brazil │4,803,877 │ ├────┼────────────────┼──────────────┼──────────┤ │86 │Cape Town │South Africa │4,800,954 │ ├────┼────────────────┼──────────────┼──────────┤ │87 │Jiddah │Saudi Arabia │4,780,740 │ ├────┼────────────────┼──────────────┼──────────┤ │88 │Urumqi │China │4,710,203 │ ├────┼────────────────┼──────────────┼──────────┤ │89 │Kunming │China │4,657,381 │ ├────┼────────────────┼──────────────┼──────────┤ │90 │Changchun │China │4,616,002 │ ├────┼────────────────┼──────────────┼──────────┤ │91 │Hefei │China │4,496,456 │ ├────┼────────────────┼──────────────┼──────────┤ │92 │Shantou │China │4,490,411 │ ├────┼────────────────┼──────────────┼──────────┤ │93 │Xinbei │Taiwan │4,470,672 │ ├────┼────────────────┼──────────────┼──────────┤ │94 │Kabul │Afghanistan │4,457,882 │ ├────┼────────────────┼──────────────┼──────────┤ │95 │Ningbo │China │4,405,292 │ ├────┼────────────────┼──────────────┼──────────┤ │96 │Tel Aviv │Israel │4,343,584 │ ├────┼────────────────┼──────────────┼──────────┤ │97 │Yaounde │Cameroon │4,336,670 │ ├────┼────────────────┼──────────────┼──────────┤ │98 │Rome │Italy │4,297,877 │ ├────┼────────────────┼──────────────┼──────────┤ │99 │Shijiazhuang │China │4,285,135 │ ├────┼────────────────┼──────────────┼──────────┤ │100 │Montreal │Canada │4,276,526 │ └────┴────────────────┴──────────────┴──────────┘ ================================================ FILE: table/testdata/TestTableShrinkWithYOffset/WithHeaders.golden ================================================ ┌────┬────────────────┬──────────────┬──────────┐ │Rank│City │Country │Population│ ├────┼────────────────┼──────────────┼──────────┤ │60 │Toronto │Canada │6,312,974 │ │61 │Belo Horizonte │Brazil │6,194,292 │ │62 │Khartoum │Sudan │6,160,327 │ │63 │Johannesburg │South Africa │6,065,354 │ │64 │Singapore │Singapore │6,039,577 │ │65 │Dalian │China │5,930,140 │ │66 │Qingdao │China │5,865,232 │ │67 │Zhengzhou │China │5,690,312 │ │68 │Ji Nan Shandong │China │5,663,015 │ │69 │Barcelona │Spain │5,658,472 │ │70 │Saint Petersburg│Russia │5,535,556 │ │71 │Abidjan │Ivory Coast │5,515,790 │ │72 │Yangon │Myanmar │5,514,454 │ │73 │Fukuoka │Japan │5,502,591 │ │74 │Alexandria │Egypt │5,483,605 │ │75 │Guadalajara │Mexico │5,339,583 │ │76 │Ankara │Turkey │5,309,690 │ │77 │Chittagong │Bangladesh │5,252,842 │ │78 │Addis Ababa │Ethiopia │5,227,794 │ │79 │Melbourne │Australia │5,150,766 │ │80 │Nairobi │Kenya │5,118,844 │ │81 │Hanoi │Vietnam │5,067,352 │ │82 │Sydney │Australia │5,056,571 │ │83 │Monterrey │Mexico │5,036,535 │ │84 │Changsha │China │4,809,887 │ │85 │Brasilia │Brazil │4,803,877 │ │86 │Cape Town │South Africa │4,800,954 │ │87 │Jiddah │Saudi Arabia │4,780,740 │ │88 │Urumqi │China │4,710,203 │ │89 │Kunming │China │4,657,381 │ │90 │Changchun │China │4,616,002 │ │91 │Hefei │China │4,496,456 │ │92 │Shantou │China │4,490,411 │ │93 │Xinbei │Taiwan │4,470,672 │ │94 │Kabul │Afghanistan │4,457,882 │ │95 │Ningbo │China │4,405,292 │ │96 │Tel Aviv │Israel │4,343,584 │ │97 │Yaounde │Cameroon │4,336,670 │ │98 │Rome │Italy │4,297,877 │ │99 │Shijiazhuang │China │4,285,135 │ │100 │Montreal │Canada │4,276,526 │ └────┴────────────────┴──────────────┴──────────┘ ================================================ FILE: table/testdata/TestTableUnsetBorders.golden ================================================ LANGUAGE │ FORMAL │ INFORMAL ──────────┼──────────────┼─────────── Chinese │ Nǐn hǎo │ Nǐ hǎo French │ Bonjour │ Salut Japanese │ こんにちは │ やあ Russian │ Zdravstvuyte │ Privet Spanish │ Hola │ ¿Qué tal? ================================================ FILE: table/testdata/TestTableUnsetHeaderSeparator.golden ================================================ LANGUAGE │ FORMAL │ INFORMAL Chinese │ Nǐn hǎo │ Nǐ hǎo French │ Bonjour │ Salut Japanese │ こんにちは │ やあ Russian │ Zdravstvuyte │ Privet Spanish │ Hola │ ¿Qué tal? ================================================ FILE: table/testdata/TestTableUnsetHeaderSeparatorWithBorder.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestTableWidthExpand.golden ================================================ ┌──────────────────────────┬─────────────────────────┬─────────────────────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────────────────────┼─────────────────────────┼─────────────────────────┤ │ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────────────────────┴─────────────────────────┴─────────────────────────┘ ================================================ FILE: table/testdata/TestTableWidthShrink/DefaultBorders.golden ================================================ ┌────────┬─────────┬─────────┐ │ LANGU… │ FORMAL │ INFORM… │ ├────────┼─────────┼─────────┤ │ Chines │ Nǐn hǎo │ Nǐ hǎo │ │ e │ │ │ │ French │ Bonjour │ Salut │ │ Japane │ こんに │ やあ │ │ se │ ちは │ │ │ Russia │ Zdravst │ Privet │ │ n │ vuyte │ │ │ Spanis │ Hola │ ¿Qué │ │ h │ │ tal? │ └────────┴─────────┴─────────┘ ================================================ FILE: table/testdata/TestTableWidthShrink/NoBorders.golden ================================================ ────────────────────────────── LANGUAGE FORMAL INFORMAL ────────────────────────────── Chinese Nǐn hǎo Nǐ hǎo French Bonjour Salut Japanese こんにち やあ は Russian Zdravstv Privet uyte Spanish Hola ¿Qué tal? ────────────────────────────── ================================================ FILE: table/testdata/TestTableWidthShrink/OutlineBordersOnly.golden ================================================ ┌────────────────────────────┐ │ LANGUA… FORMAL INFORM… │ ├────────────────────────────┤ │ Chinese Nǐn hǎo Nǐ hǎo │ │ French Bonjour Salut │ │ Japanes こんにち やあ │ │ e は │ │ Russian Zdravstv Privet │ │ uyte │ │ Spanish Hola ¿Qué │ │ tal? │ └────────────────────────────┘ ================================================ FILE: table/testdata/TestTableWidthSmartCrop.golden ================================================ ┌──────┬─────┬──────────┐ │ Name │ Ag… │ Location │ ├──────┼─────┼──────────┤ │ Kini │ 40 │ New York │ │ Eli │ 30 │ London │ │ Iris │ 20 │ Paris │ └──────┴─────┴──────────┘ ================================================ FILE: table/testdata/TestTableWidthSmartCropExtensive.golden ================================================ ┏━━━━┳━━━━━┳━━━━━┓ ┃ L… ┃ FO… ┃ IN… ┃ ┣━━━━╋━━━━━╋━━━━━┫ ┃ C… ┃ 您… ┃ 你… ┃ ┃ J… ┃ こ… ┃ や… ┃ ┃ A… ┃ أه… ┃ أه… ┃ ┃ R… ┃ Зд… ┃ Пр… ┃ ┃ S… ┃ Ho… ┃ ¿Q… ┃ ┃ E… ┃ Yo… ┃ Ho… ┃ ┗━━━━┻━━━━━┻━━━━━┛ ================================================ FILE: table/testdata/TestTableWidthSmartCropTiny.golden ================================================ ┌ │ ├ │ │ │ │ │ └ ================================================ FILE: table/testdata/TestTableWidths.golden ================================================ ────────────────────────────── LANGUAGE FORMAL INFORMAL ────────────────────────────── Chinese Nǐn hǎo Nǐ hǎo French Bonjour Salut Japanese こんにち やあ は Russian Zdravstv Privet uyte Spanish Hola ¿Qué tal? ────────────────────────────── ================================================ FILE: table/testdata/TestTableWithBackground.golden ================================================ ┌────────┬────────────┬─────────┐ │LANGUAGE│FORMAL │INFORMAL │ ├────────┼────────────┼─────────┤ │Chinese │Nǐn hǎo │Nǐ hǎo │ │French │Bonjour │Salut │ │Japanese│こんにちは │やあ │ │Russian │Zdravstvuyte│Privet │ │Spanish │Hola │¿Qué tal?│ └────────┴────────────┴─────────┘ ================================================ FILE: table/testdata/TestTableYOffset.golden ================================================ ┌──────────┬──────────────┬───────────┐ │ LANGUAGE │ FORMAL │ INFORMAL │ ├──────────┼──────────────┼───────────┤ │ French │ Bonjour │ Salut │ │ Japanese │ こんにちは │ やあ │ │ Russian │ Zdravstvuyte │ Privet │ │ Spanish │ Hola │ ¿Qué tal? │ └──────────┴──────────────┴───────────┘ ================================================ FILE: table/testdata/TestWrapPreStyledContent.golden ================================================ ┌─────────┬────────────────┬────────────────────────────────┬─────────────┬────┐ │Package │Version │Link │ │ │ ├─────────┼────────────────┼────────────────────────────────┼─────────────┼────┤ │sourcegit│0.19 │https://aur.archlinux.org/packag│ │ │ │ │ │es/sourcegit-bin │ │ │ │ │ │ │ │ │ │Welcome │いらっしゃいませ│مرحباً │환영 │欢迎│ │Goodbye │さようなら │مع السلامة │안녕히 가세요│再见│ └─────────┴────────────────┴────────────────────────────────┴─────────────┴────┘ ================================================ FILE: table/testdata/TestWrapStyleFuncContent.golden ================================================ ┌─────────┬────────────────┬───────────────────────────────┐ │Package │Version │Link │ ├─────────┼────────────────┼───────────────────────────────┤ │sourcegit│0.19 │https://aur.archlinux.org/packa│ │ │ │ges/sourcegit-bin │ │Welcome │いらっしゃいませ│مرحباً │ │Goodbye │さようなら │مع السلامة │ └─────────┴────────────────┴───────────────────────────────┘ ================================================ FILE: table/util.go ================================================ package table import ( "sort" ) // btoi converts a boolean to an integer, 1 if true, 0 if false. func btoi(b bool) int { if b { return 1 } return 0 } // bton converts a boolean to a specific integer, n if true, 0 if false. func bton(b bool, n int) int { if b { return n } return 0 } // sum returns the sum of all integers in a slice. func sum(n []int) int { var sum int for _, i := range n { sum += i } return sum } // median returns the median of a slice of integers. func median(n []int) int { sort.Ints(n) if len(n) <= 0 { return 0 } if len(n)%2 == 0 { h := len(n) / 2 //nolint:mnd return (n[h-1] + n[h]) / 2 //nolint:mnd } return n[len(n)/2] } ================================================ FILE: terminal.go ================================================ package lipgloss import ( "fmt" "image/color" "io" "strings" "time" uv "github.com/charmbracelet/ultraviolet" "github.com/charmbracelet/x/ansi" ) // queryBackgroundColor queries the terminal for the background color. // If the terminal does not support querying the background color, nil is // returned. // // Note: you will need to set the input to raw mode before calling this // function. // // state, _ := term.MakeRaw(in.Fd()) // defer term.Restore(in.Fd(), state) // // copied from x/term@v0.1.3. func queryBackgroundColor(in io.Reader, out io.Writer) (c color.Color, err error) { err = queryTerminal(in, out, defaultQueryTimeout, func(seq string, pa *ansi.Parser) bool { switch { case ansi.HasOscPrefix(seq): switch pa.Command() { case 11: // OSC 11 parts := strings.Split(string(pa.Data()), ";") if len(parts) != 2 { break // invalid, but we still need to parse the next sequence } c = ansi.XParseColor(parts[1]) } case ansi.HasCsiPrefix(seq): switch pa.Command() { case ansi.Command('?', 0, 'c'): // DA1 return false } } return true }, ansi.RequestBackgroundColor+ansi.RequestPrimaryDeviceAttributes) return } const defaultQueryTimeout = time.Second * 2 // queryTerminalFilter is a function that filters input events using a type // switch. If false is returned, the QueryTerminal function will stop reading // input. type queryTerminalFilter func(seq string, pa *ansi.Parser) bool // queryTerminal queries the terminal for support of various features and // returns a list of response events. // Most of the time, you will need to set stdin to raw mode before calling this // function. // Note: This function will block until the terminal responds or the timeout // is reached. // copied from x/term@v0.1.3. func queryTerminal( in io.Reader, out io.Writer, timeout time.Duration, filter queryTerminalFilter, query string, ) error { // We use [uv.NewCancelReader] because it uses a different Windows // implementation than the on in the [cancelreader] library, which uses // the Cancel IO API to cancel reads instead of using Overlapped IO. rd, err := uv.NewCancelReader(in) if err != nil { return fmt.Errorf("could not create cancel reader: %w", err) } defer rd.Close() //nolint: errcheck done := make(chan struct{}, 1) defer close(done) go func() { select { case <-done: case <-time.After(timeout): rd.Cancel() } }() if _, err := io.WriteString(out, query); err != nil { return fmt.Errorf("could not write query: %w", err) } pa := ansi.GetParser() defer ansi.PutParser(pa) var acc []byte // Accumulate partial responses before filtering var buf [256]byte // 256 bytes should be enough for most responses var state byte for { n, err := rd.Read(buf[:]) if err != nil { return fmt.Errorf("could not read from input: %w", err) } p := buf[:] for n > 0 { seq, _, read, newState := ansi.DecodeSequence(p[:n], state, pa) acc = append(acc, seq...) if newState == ansi.NormalState { if !filter(string(acc), pa) { return nil } acc = acc[:0] } state = newState n -= read p = p[read:] } } } ================================================ FILE: tree/children.go ================================================ package tree import "slices" // Children is the interface that wraps the basic methods of a tree model. type Children interface { // At returns the content item of the given index. At(index int) Node // Length returns the number of children in the tree. Length() int } // NodeChildren is the implementation of the Children interface with tree Nodes. type NodeChildren []Node // Append appends a child to the list of children. func (n NodeChildren) Append(child Node) NodeChildren { n = append(n, child) return n } // Remove removes a child from the list at the given index. func (n NodeChildren) Remove(index int) NodeChildren { if index < 0 || len(n) < index+1 { return n } n = slices.Delete(n, index, index+1) return n } // Length returns the number of children in the list. func (n NodeChildren) Length() int { return len(n) } // At returns the child at the given index. func (n NodeChildren) At(i int) Node { if i >= 0 && i < len(n) { return n[i] } return nil } // NewStringData returns a Data of strings. func NewStringData(data ...string) Children { result := make([]Node, 0, len(data)) for _, d := range data { s := Leaf{value: d} result = append(result, &s) } return NodeChildren(result) } var _ Children = NewFilter(nil) // Filter applies a filter on some data. You could use this to create a new // tree whose values all satisfy the condition provided in the Filter() function. type Filter struct { data Children filter func(index int) bool } // NewFilter initializes a new Filter. func NewFilter(data Children) *Filter { return &Filter{data: data} } // At returns the item at the given index. // The index is relative to the filtered results. func (m *Filter) At(index int) Node { j := 0 for i := range m.data.Length() { if m.filter(i) { if j == index { return m.data.At(i) } j++ } } return nil } // Filter uses a filter function to set a condition that all the data must satisfy to be in the Tree. func (m *Filter) Filter(f func(index int) bool) *Filter { m.filter = f return m } // Length returns the number of children in the tree. func (m *Filter) Length() int { j := 0 for i := range m.data.Length() { if m.filter(i) { j++ } } return j } ================================================ FILE: tree/enumerator.go ================================================ package tree // Enumerator enumerates a tree. Typically, this is used to draw the branches // for the tree nodes and is different for the last child. // // For example, the default enumerator would be: // // func TreeEnumerator(children Children, index int) string { // if children.Length()-1 == index { // return "└──" // } // // return "├──" // } type Enumerator func(children Children, index int) string // DefaultEnumerator enumerates a tree. // // ├── Foo // ├── Bar // ├── Baz // └── Qux. func DefaultEnumerator(children Children, index int) string { if children.Length()-1 == index { return "└──" } return "├──" } // RoundedEnumerator enumerates a tree with rounded edges. // // ├── Foo // ├── Bar // ├── Baz // ╰── Qux. func RoundedEnumerator(children Children, index int) string { if children.Length()-1 == index { return "╰──" } return "├──" } // Indenter indents the children of a tree. // // Indenters allow for displaying nested tree items with connecting borders // to sibling nodes. // // For example, the default indenter would be: // // func TreeIndenter(children Children, index int) string { // if children.Length()-1 == index { // return "│ " // } // // return " " // } type Indenter func(children Children, index int) string // DefaultIndenter indents a tree for nested trees and multiline content. // // ├── Foo // ├── Bar // │ ├── Qux // │ ├── Quux // │ │ ├── Foo // │ │ └── Bar // │ └── Quuux // └── Baz. func DefaultIndenter(children Children, index int) string { if children.Length()-1 == index { return " " } return "│ " } ================================================ FILE: tree/example_test.go ================================================ package tree_test import ( "fmt" "charm.land/lipgloss/v2/tree" "github.com/charmbracelet/x/ansi" ) // Leaf Examples func ExampleLeaf_SetHidden() { tr := tree.New(). Child( "Foo", tree.Root("Bar"). Child( "Qux", tree.Root("Quux"). Child("Hello!"), "Quuux", ), "Baz", ) tr.Children().At(1).Children().At(2).SetHidden(true) fmt.Println(tr.String()) // Output: // // ├── Foo // ├── Bar // │ ├── Qux // │ └── Quux // │ └── Hello! // └── Baz } func ExampleNewLeaf() { tr := tree.New(). Child( "Foo", tree.Root("Bar"). Child( "Qux", tree.Root("Quux"). Child( tree.NewLeaf("This should be hidden", true), tree.NewLeaf( tree.Root("I am groot").Child("leaves"), false), ), "Quuux", ), "Baz", ) fmt.Println(tr.String()) // Output: // ├── Foo // ├── Bar // │ ├── Qux // │ ├── Quux // │ │ └── I am groot // │ │ └── leaves // │ └── Quuux // └── Baz } func ExampleLeaf_SetValue() { t := tree. Root("⁜ Makeup"). Child( "Glossier", "Fenty Beauty", tree.New().Child( "Gloss Bomb Universal Lip Luminizer", "Hot Cheeks Velour Blushlighter", ), "Nyx", "Mac", "Milk", ). Enumerator(tree.RoundedEnumerator) glossier := t.Children().At(0) glossier.SetValue("Il Makiage") fmt.Println(ansi.Strip(t.String())) // Output: // ⁜ Makeup // ├── Il Makiage // ├── Fenty Beauty // │ ├── Gloss Bomb Universal Lip Luminizer // │ ╰── Hot Cheeks Velour Blushlighter // ├── Nyx // ├── Mac // ╰── Milk } // Tree Examples func ExampleTree_Hide() { tr := tree.New(). Child( "Foo", tree.Root("Bar"). Child( "Qux", tree.Root("Quux"). Child("Foo", "Bar"). Hide(true), "Quuux", ), "Baz", ) fmt.Println(tr.String()) // Output: // ├── Foo // ├── Bar // │ ├── Qux // │ └── Quuux // └── Baz } func ExampleTree_SetHidden() { tr := tree.New(). Child( "Foo", tree.Root("Bar"). Child( "Qux", tree.Root("Quux"). Child("Foo", "Bar"), "Quuux", ), "Baz", ) // Hide a tree after its creation. We'll hide Quux. tr.Children().At(1).Children().At(1).SetHidden(true) // Output: // ├── Foo // ├── Bar // │ ├── Qux // │ └── Quuux // └── Baz fmt.Println(tr.String()) } ================================================ FILE: tree/renderer.go ================================================ package tree import ( "strings" "charm.land/lipgloss/v2" ) // StyleFunc allows the tree to be styled per item. type StyleFunc func(children Children, i int) lipgloss.Style // Style is the styling applied to the tree. type Style struct { enumeratorFunc StyleFunc indenterFunc StyleFunc itemFunc StyleFunc root lipgloss.Style } // newRenderer returns the renderer used to render a tree. func newRenderer() *renderer { return &renderer{ style: Style{ enumeratorFunc: func(Children, int) lipgloss.Style { return lipgloss.NewStyle().PaddingRight(1) }, indenterFunc: func(Children, int) lipgloss.Style { return lipgloss.NewStyle().PaddingRight(1) }, itemFunc: func(Children, int) lipgloss.Style { return lipgloss.NewStyle() }, }, enumerator: DefaultEnumerator, indenter: DefaultIndenter, } } type renderer struct { style Style enumerator Enumerator indenter Indenter width int } // render is responsible for actually rendering the tree. func (r *renderer) render(node Node, root bool, prefix string) string { if node.Hidden() { return "" } var maxLen int children := node.Children() enumerator := r.enumerator indenter := r.indenter strs := make([]string, 0, children.Length()) // print the root node name if its not empty. if name := node.Value(); name != "" && root { line := r.style.root.Render(name) // If the line is shorter than the desired width, we pad it with spaces. if pad := r.width - lipgloss.Width(line); pad > 0 { line = name + r.style.root.Render(strings.Repeat(" ", pad)) } strs = append(strs, r.style.root.Render(line)) } for i := range children.Length() { if i < children.Length()-1 { if child := children.At(i + 1); child.Hidden() { // Don't count the last child if its hidden. This renders the // last visible element with the right prefix // // The only type of Children is NodeChildren. children = children.(NodeChildren).Remove(i + 1) } } prefix := enumerator(children, i) prefix = r.style.enumeratorFunc(children, i).Render(prefix) maxLen = max(lipgloss.Width(prefix), maxLen) } for i := range children.Length() { child := children.At(i) if child.Hidden() { continue } indentStyle := r.style.indenterFunc(children, i) enumStyle := r.style.enumeratorFunc(children, i) itemStyle := r.style.itemFunc(children, i) indent := indentStyle.Render(indenter(children, i)) nodePrefix := enumStyle.Render(enumerator(children, i)) // Preserve the background color of the enumerator when adding the padding enumBgStyle := lipgloss.NewStyle().Background(enumStyle.GetBackground()) // Add padding to the left of the node to align it with the longest prefix of its siblings if l := maxLen - lipgloss.Width(nodePrefix); l > 0 { nodePrefix = enumBgStyle.Render(strings.Repeat(" ", l)) + nodePrefix } item := itemStyle.Render(child.Value()) multineLinePrefix := enumBgStyle.Render(prefix) // This dance below is to account for multiline prefixes, e.g. "|\n|". // In that case, we need to make sure that both the parent prefix and // the current node's prefix have the same height. for lipgloss.Height(item) > lipgloss.Height(nodePrefix) { nodePrefix = lipgloss.JoinVertical( lipgloss.Left, nodePrefix, indent, ) } for lipgloss.Height(nodePrefix) > lipgloss.Height(multineLinePrefix) { multineLinePrefix = lipgloss.JoinVertical( lipgloss.Left, multineLinePrefix, prefix, ) } line := lipgloss.JoinHorizontal( lipgloss.Top, multineLinePrefix, nodePrefix, item, ) // If the line is shorter than the desired width, we pad it with spaces. if pad := r.width - lipgloss.Width(line); pad > 0 { line = line + itemStyle.Render(strings.Repeat(" ", pad)) } strs = append( strs, line, ) if children.Length() > 0 { // Here we see if the child has a custom renderer, which means the // user set a custom enumerator/indenter/item style, etc. // If it has one, we'll use it to render itself. // otherwise, we keep using the current renderer. // Note that the renderer doesn't inherit its parent's styles. renderer := r switch child := child.(type) { case *Tree: if child.r != nil { renderer = child.r } } if s := renderer.render( child, false, prefix+indent, ); s != "" { strs = append(strs, s) } } } return strings.Join(strs, "\n") } ================================================ FILE: tree/testdata/TestAddItemWithAndWithoutRoot/with_root.golden ================================================ ├── Foo ├── Bar │ └── Baz └── Qux ================================================ FILE: tree/testdata/TestAddItemWithAndWithoutRoot/without_root.golden ================================================ ├── Foo ├── Bar │ └── Baz └── Qux ================================================ FILE: tree/testdata/TestEmbedListWithinTree.golden ================================================ ├── 1. A │ 2. B │ 3. C └── A. 1 B. 2 C. 3 ================================================ FILE: tree/testdata/TestFilter.golden ================================================ Root ├── Foo ├── Bar └── Baz ================================================ FILE: tree/testdata/TestMultilinePrefix.golden ================================================ Foo Document The Foo Files │ Bar Document │ The Bar Files Baz Document The Baz Files ================================================ FILE: tree/testdata/TestMultilinePrefixInception.golden ================================================ Foo Document The Foo Files │ Bar Document │ The Bar Files Qux Document The Qux Files │ Quux Document │ The Quux Files Quuux Document The Quuux Files Baz Document The Baz Files ================================================ FILE: tree/testdata/TestMultilinePrefixSubtree.golden ================================================ ├── Foo ├── Bar ├── Baz │ Foo Document │ The Foo Files │ │ │ Bar Document │ │ The Bar Files │ │ Baz Document │ The Baz Files │ └── Qux ================================================ FILE: tree/testdata/TestRootStyle.golden ================================================ Root ├── Foo └── Baz ================================================ FILE: tree/testdata/TestTree/after.golden ================================================ ├── Foo ├── Bar │ ├── Qux │ ├── Quux │ │ ├── Foo │ │ ╰── Bar │ ╰── Quuux ╰── Baz ================================================ FILE: tree/testdata/TestTree/before.golden ================================================ ├── Foo ├── Bar │ ├── Qux │ ├── Quux │ │ ├── Foo │ │ └── Bar │ └── Quuux └── Baz ================================================ FILE: tree/testdata/TestTreeAddTwoSubTreesWithoutName.golden ================================================ ├── Bar ├── Foo │ ├── Qux │ ├── Qux │ ├── Qux │ ├── Qux │ ├── Qux │ ├── Quux │ ├── Quux │ ├── Quux │ ├── Quux │ └── Quux └── Baz ================================================ FILE: tree/testdata/TestTreeAllHidden.golden ================================================ ================================================ FILE: tree/testdata/TestTreeCustom.golden ================================================ -> Foo -> Bar -> -> Qux -> -> Quux -> -> -> Foo -> -> -> Bar -> -> Quuux -> Baz ================================================ FILE: tree/testdata/TestTreeHidden.golden ================================================ ├── Foo ├── Bar │ ├── Qux │ └── Quuux └── Baz ================================================ FILE: tree/testdata/TestTreeLastNodeIsSubTree.golden ================================================ ├── Foo └── Bar ├── Qux ├── Quux │ ├── Foo │ └── Bar └── Quuux ================================================ FILE: tree/testdata/TestTreeMixedEnumeratorSize.golden ================================================ The Root Node™ I Foo II Foo III Foo IV Foo V Foo ================================================ FILE: tree/testdata/TestTreeMultilineNode.golden ================================================ Big Root Node ├── Foo ├── Bar │ ├── Line 1 │ │ Line 2 │ │ Line 3 │ │ Line 4 │ ├── Quux │ │ ├── Foo │ │ └── Bar │ └── Quuux └── Baz Line 2 ================================================ FILE: tree/testdata/TestTreeNil.golden ================================================ ├── Bar │ ├── Qux │ ├── Quux │ │ └── Bar │ └── Quuux └── Baz ================================================ FILE: tree/testdata/TestTreeRoot.golden ================================================ Root ├── Foo ├── Bar │ ├── Qux │ └── Quuux └── Baz ================================================ FILE: tree/testdata/TestTreeStartsWithSubtree.golden ================================================ ├── Bar │ ├── Qux │ └── Quuux └── Baz ================================================ FILE: tree/testdata/TestTreeStyleAt.golden ================================================ Root > Foo - Baz ================================================ FILE: tree/testdata/TestTreeStyleNilFuncs.golden ================================================ Silly ├──Willy └──Nilly ================================================ FILE: tree/testdata/TestTreeSubTreeWithCustomEnumerator.golden ================================================ The Root Node™ ├── Parent │ + ├── * child 1 │ + └── * child 2 └── Baz ================================================ FILE: tree/testdata/TestTreeTable.golden ================================================ ├── Foo ├── Bar │ ├── Baz │ ├── Baz │ ├── ┌─────────┬────────┐ │ │ │ Foo │ Bar │ │ │ ├─────────┼────────┤ │ │ │ Qux │ Baz │ │ │ │ Qux │ Baz │ │ │ └─────────┴────────┘ │ └── Baz └── Qux ================================================ FILE: tree/testdata/TestTypes.golden ================================================ ├── 0 ├── true ├── Foo ├── Bar ├── Qux ├── Quux └── Quuux ================================================ FILE: tree/tree.go ================================================ // Package tree allows you to build trees, as simple or complicated as you need. // // Define a tree with a root node, and children, set rendering properties (such // as style, enumerators, etc...), and print it. // // t := tree.New(). // Child( // ".git", // tree.Root("examples/"). // Child( // tree.Root("list/"). // Child("main.go"). // tree.Root("table/"). // Child("main.go"). // ). // tree.Root("list/"). // Child("list.go", "list_test.go"). // tree.New(). // Root("table/"). // Child("table.go", "table_test.go"). // "align.go", // "align_test.go", // "join.go", // "join_test.go", // ) package tree import ( "fmt" "sync" "charm.land/lipgloss/v2" ) // Node defines a node in a tree. type Node interface { fmt.Stringer Value() string Children() Children Hidden() bool SetHidden(bool) SetValue(any) } // Leaf is a node without children. type Leaf struct { value string hidden bool } // NewLeaf returns a new Leaf. func NewLeaf(value any, hidden bool) *Leaf { s := Leaf{} s.SetValue(value) s.SetHidden(hidden) return &s } // Children of a Leaf node are always empty. func (Leaf) Children() Children { return NodeChildren(nil) } // Value returns the value of a Leaf node. func (s Leaf) Value() string { return s.value } // SetValue sets the value of a Leaf node. func (s *Leaf) SetValue(value any) { switch item := value.(type) { case Node, fmt.Stringer: s.value = item.(fmt.Stringer).String() case string, nil: s.value = item.(string) default: s.value = fmt.Sprintf("%v", item) } } // Hidden returns whether a Leaf node is hidden. func (s Leaf) Hidden() bool { return s.hidden } // SetHidden hides a Leaf node. func (s *Leaf) SetHidden(hidden bool) { s.hidden = hidden } // String returns the string representation of a Leaf node. // For leaf nodes, this is the same as Value. func (s Leaf) String() string { return s.Value() } // Tree implements a Node. type Tree struct { value string hidden bool offset [2]int children Children r *renderer ronce sync.Once } // Hidden returns whether a Tree node is hidden. func (t *Tree) Hidden() bool { return t.hidden } // Hide sets whether to hide the Tree node. Use this when creating a new // hidden Tree. func (t *Tree) Hide(hide bool) *Tree { t.hidden = hide return t } // SetHidden hides a Tree node. func (t *Tree) SetHidden(hidden bool) { t.Hide(hidden) } // Offset sets the Tree children offsets. func (t *Tree) Offset(start, end int) *Tree { if start > end { _start := start start = end end = _start } if start < 0 { start = 0 } if end < 0 || end > t.children.Length() { end = t.children.Length() } t.offset[0] = start t.offset[1] = end return t } // Value returns the root name of this node. // If the root implements fmt.Stringer, it will return the value returned by it. func (t *Tree) Value() string { return t.value } // SetValue sets the value of a Tree node. func (t *Tree) SetValue(value any) { t.Root(value) } // String returns the string representation of the Tree node. func (t *Tree) String() string { return t.ensureRenderer().render(t, true, "") } // Child adds a child to this Tree. // // If a Child Tree is passed without a root, it will be parented to its sibling // child (auto-nesting). // // tree.Root("Foo").Child("Bar", tree.New().Child("Baz"), "Qux") // tree.Root("Foo").Child(tree.Root("Bar").Child("Baz"), "Qux") // // ├── Foo // ├── Bar // │ └── Baz // └── Qux func (t *Tree) Child(children ...any) *Tree { for _, child := range children { switch item := child.(type) { case *Tree: newItem, rm := ensureParent(t.children, item) if rm >= 0 { t.children = t.children.(NodeChildren).Remove(rm) } t.children = t.children.(NodeChildren).Append(newItem) case Children: for i := range item.Length() { t.children = t.children.(NodeChildren).Append(item.At(i)) } case Node: t.children = t.children.(NodeChildren).Append(item) case fmt.Stringer: s := Leaf{value: item.String()} t.children = t.children.(NodeChildren).Append(&s) case string: s := Leaf{value: item} t.children = t.children.(NodeChildren).Append(&s) case []any: return t.Child(item...) case []string: ss := make([]any, 0, len(item)) for _, s := range item { ss = append(ss, s) } return t.Child(ss...) case nil: continue default: return t.Child(fmt.Sprintf("%v", item)) } } return t } func ensureParent(nodes Children, item *Tree) (*Tree, int) { if item.Value() != "" || nodes.Length() == 0 { return item, -1 } j := nodes.Length() - 1 parent := nodes.At(j) switch parent := parent.(type) { case *Tree: for i := range item.Children().Length() { parent.Child(item.children.At(i)) } return parent, j case *Leaf: item.value = parent.Value() return item, j } return item, -1 } func (t *Tree) ensureRenderer() *renderer { t.ronce.Do(func() { t.r = newRenderer() }) return t.r } // EnumeratorStyle sets a static style for all enumerators. // // Use EnumeratorStyleFunc to conditionally set styles based on the tree node. func (t *Tree) EnumeratorStyle(style lipgloss.Style) *Tree { t.ensureRenderer().style.enumeratorFunc = func(Children, int) lipgloss.Style { return style } return t } // EnumeratorStyleFunc sets the enumeration style function. Use this function // for conditional styling. // // t := tree.New(). // EnumeratorStyleFunc(func(_ tree.Children, i int) lipgloss.Style { // if selected == i { // return lipgloss.NewStyle().Foreground(hightlightColor) // } // return lipgloss.NewStyle().Foreground(dimColor) // }) func (t *Tree) EnumeratorStyleFunc(fn StyleFunc) *Tree { if fn == nil { fn = func(Children, int) lipgloss.Style { return lipgloss.NewStyle() } } t.ensureRenderer().style.enumeratorFunc = fn return t } // IndenterStyle sets a static style for all indenters. // // Use IndenterStyleFunc to conditionally set styles based on the tree node. func (t *Tree) IndenterStyle(style lipgloss.Style) *Tree { t.ensureRenderer().style.indenterFunc = func(Children, int) lipgloss.Style { return style } return t } // IndenterStyleFunc sets the indentation style function. Use this function // for conditional styling. // // t := tree.New(). // IndenterStyleFunc(func(_ tree.Children, i int) lipgloss.Style { // if selected == i { // return lipgloss.NewStyle().Foreground(hightlightColor) // } // return lipgloss.NewStyle().Foreground(dimColor) // }) func (t *Tree) IndenterStyleFunc(fn StyleFunc) *Tree { if fn == nil { fn = func(Children, int) lipgloss.Style { return lipgloss.NewStyle() } } t.ensureRenderer().style.indenterFunc = fn return t } // RootStyle sets a style for the root element. func (t *Tree) RootStyle(style lipgloss.Style) *Tree { t.ensureRenderer().style.root = style return t } // ItemStyle sets a static style for all items. // // Use ItemStyleFunc to conditionally set styles based on the tree node. func (t *Tree) ItemStyle(style lipgloss.Style) *Tree { t.ensureRenderer().style.itemFunc = func(Children, int) lipgloss.Style { return style } return t } // ItemStyleFunc sets the item style function. Use this for conditional styling. // For example: // // t := tree.New(). // ItemStyleFunc(func(_ tree.Data, i int) lipgloss.Style { // if selected == i { // return lipgloss.NewStyle().Foreground(hightlightColor) // } // return lipgloss.NewStyle().Foreground(dimColor) // }) func (t *Tree) ItemStyleFunc(fn StyleFunc) *Tree { if fn == nil { fn = func(Children, int) lipgloss.Style { return lipgloss.NewStyle() } } t.ensureRenderer().style.itemFunc = fn return t } // Enumerator sets the enumerator implementation. This can be used to change the // way the branches indicators look. Lipgloss includes predefined enumerators // for a classic or rounded tree. For example, you can have a rounded tree: // // tree.New(). // Enumerator(RoundedEnumerator) func (t *Tree) Enumerator(enum Enumerator) *Tree { t.ensureRenderer().enumerator = enum return t } // Indenter sets the indenter implementation. This is used to change the way // the tree is indented. The default indentor places a border connecting sibling // elements and no border for the last child. // // └── Foo // └── Bar // └── Baz // └── Qux // └── Quux // // You can define your own indenter. // // func ArrowIndenter(children tree.Children, index int) string { // return "→ " // } // // → Foo // → → Bar // → → → Baz // → → → → Qux // → → → → → Quux func (t *Tree) Indenter(indenter Indenter) *Tree { t.ensureRenderer().indenter = indenter return t } // Width sets the tree width. // // Items will be padded to account for the entire width of the tree. func (t *Tree) Width(width int) *Tree { t.ensureRenderer().width = width return t } // Children returns the children of a node. func (t *Tree) Children() Children { var data []Node for i := t.offset[0]; i < t.children.Length()-t.offset[1]; i++ { data = append(data, t.children.At(i)) } return NodeChildren(data) } // Root returns a new tree with the root set. // // tree.Root(root) // // It is a shorthand for: // // tree.New().Root(root) func Root(root any) *Tree { t := New() return t.Root(root) } // Root sets the root value of this tree. func (t *Tree) Root(root any) *Tree { // root is a tree or string switch item := root.(type) { case *Tree: t.value = item.value t = t.Child(item.children) case Node, fmt.Stringer: t.value = item.(fmt.Stringer).String() case string, nil: t.value = item.(string) default: t.value = fmt.Sprintf("%v", item) } return t } // New returns a new tree. func New() *Tree { return &Tree{ children: NodeChildren(nil), } } ================================================ FILE: tree/tree_test.go ================================================ package tree_test import ( "testing" "charm.land/lipgloss/v2" "charm.land/lipgloss/v2/list" "charm.land/lipgloss/v2/table" "charm.land/lipgloss/v2/tree" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/exp/golden" ) func TestTree(t *testing.T) { tr := tree.New(). Child( "Foo", tree.Root("Bar"). Child( "Qux", tree.Root("Quux"). Child( "Foo", "Bar", ), "Quuux", ), "Baz", ) t.Run("before", func(t *testing.T) { golden.RequireEqual(t, []byte(tr.String())) }) tr.Enumerator(tree.RoundedEnumerator) t.Run("after", func(t *testing.T) { golden.RequireEqual(t, []byte(tr.String())) }) } func TestTreeHidden(t *testing.T) { tree := tree.New(). Child( "Foo", tree.Root("Bar"). Child( "Qux", tree.Root("Quux"). Child("Foo", "Bar"). Hide(true), "Quuux", ), "Baz", ) golden.RequireEqual(t, []byte(tree.String())) } func TestTreeAllHidden(t *testing.T) { tree := tree.New(). Child( "Foo", tree.Root("Bar"). Child( "Qux", tree.Root("Quux"). Child( "Foo", "Bar", ), "Quuux", ), "Baz", ).Hide(true) golden.RequireEqual(t, []byte(tree.String())) } func TestTreeRoot(t *testing.T) { tree := tree.New(). Root("Root"). Child( "Foo", tree.Root("Bar"). Child("Qux", "Quuux"), "Baz", ) golden.RequireEqual(t, []byte(tree.String())) } func TestTreeStartsWithSubtree(t *testing.T) { tree := tree.New(). Child( tree.New(). Root("Bar"). Child("Qux", "Quuux"), "Baz", ) golden.RequireEqual(t, []byte(tree.String())) } func TestTreeAddTwoSubTreesWithoutName(t *testing.T) { tree := tree.New(). Child( "Bar", "Foo", tree.New(). Child( "Qux", "Qux", "Qux", "Qux", "Qux", ), tree.New(). Child( "Quux", "Quux", "Quux", "Quux", "Quux", ), "Baz", ) golden.RequireEqual(t, []byte(tree.String())) } func TestTreeLastNodeIsSubTree(t *testing.T) { tree := tree.New(). Child( "Foo", tree.Root("Bar"). Child("Qux", tree.Root("Quux").Child("Foo", "Bar"), "Quuux", ), ) golden.RequireEqual(t, []byte(tree.String())) } func TestTreeNil(t *testing.T) { tree := tree.New(). Child( nil, tree.Root("Bar"). Child( "Qux", tree.Root("Quux"). Child("Bar"), "Quuux", ), "Baz", ) golden.RequireEqual(t, []byte(tree.String())) } func TestTreeCustom(t *testing.T) { tree := tree.New(). Child( "Foo", tree.New(). Root("Bar"). Child( "Qux", tree.New(). Root("Quux"). Child("Foo", "Bar", ), "Quuux", ), "Baz", ). ItemStyle(lipgloss.NewStyle(). Foreground(lipgloss.Color("9"))). EnumeratorStyle(lipgloss.NewStyle(). Foreground(lipgloss.Color("12")). PaddingRight(1)). IndenterStyle(lipgloss.NewStyle(). Foreground(lipgloss.Color("12")). PaddingRight(1)). Enumerator(func(tree.Children, int) string { return "->" }). Indenter(func(tree.Children, int) string { return "->" }) golden.RequireEqual(t, []byte(tree.String())) } func TestTreeMultilineNode(t *testing.T) { tree := tree.New(). Root("Big\nRoot\nNode"). Child( "Foo", tree.New(). Root("Bar"). Child( "Line 1\nLine 2\nLine 3\nLine 4", tree.New(). Root("Quux"). Child( "Foo", "Bar", ), "Quuux", ), "Baz\nLine 2", ) golden.RequireEqual(t, []byte(tree.String())) } func TestTreeSubTreeWithCustomEnumerator(t *testing.T) { tree := tree.New(). Root("The Root Node™"). Child( tree.New(). Root("Parent"). Child("child 1", "child 2"). ItemStyleFunc(func(tree.Children, int) lipgloss.Style { return lipgloss.NewStyle(). SetString("*") }). EnumeratorStyleFunc(func(_ tree.Children, i int) lipgloss.Style { return lipgloss.NewStyle(). SetString("+"). PaddingRight(1) }), "Baz", ) golden.RequireEqual(t, []byte(tree.String())) } func TestTreeMixedEnumeratorSize(t *testing.T) { tree := tree.New(). Root("The Root Node™"). Child( "Foo", "Foo", "Foo", "Foo", "Foo", ).Enumerator(func(_ tree.Children, i int) string { romans := map[int]string{ 1: "I", 2: "II", 3: "III", 4: "IV", 5: "V", 6: "VI", } return romans[i+1] }) golden.RequireEqual(t, []byte(tree.String())) } func TestTreeStyleNilFuncs(t *testing.T) { tree := tree.New(). Root("Silly"). Child("Willy ", "Nilly"). ItemStyleFunc(nil). EnumeratorStyleFunc(nil) golden.RequireEqual(t, []byte(tree.String())) } func TestTreeStyleAt(t *testing.T) { tree := tree.New(). Root("Root"). Child( "Foo", "Baz", ).Enumerator(func(data tree.Children, i int) string { if data.At(i).Value() == "Foo" { return ">" } return "-" }) golden.RequireEqual(t, []byte(tree.String())) } func TestRootStyle(t *testing.T) { tree := tree.New(). Root("Root"). Child( "Foo", "Baz", ). RootStyle(lipgloss.NewStyle().Background(lipgloss.Color("#5A56E0"))). ItemStyle(lipgloss.NewStyle().Background(lipgloss.Color("#04B575"))) golden.RequireEqual(t, []byte(ansi.Strip(tree.String()))) } func TestAt(t *testing.T) { data := tree.NewStringData("Foo", "Bar") if s := data.At(0).String(); s != "Foo" { t.Errorf("want 'Foo', got '%s'", s) } if n := data.At(10); n != nil { t.Errorf("want nil, got '%s'", n) } if n := data.At(-1); n != nil { t.Errorf("want nil, got '%s'", n) } } func TestFilter(t *testing.T) { data := tree.NewFilter(tree.NewStringData( "Foo", "Bar", "Baz", "Nope", )). Filter(func(index int) bool { return index != 3 }) tree := tree.New(). Root("Root"). Child(data) golden.RequireEqual(t, []byte(tree.String())) if got := data.At(1); got.Value() != "Bar" { t.Errorf("want Bar, got %v", got) } if got := data.At(10); got != nil { t.Errorf("want nil, got %v", got) } } func TestNodeDataRemoveOutOfBounds(t *testing.T) { data := tree.NewStringData("a") if l := data.Length(); l != 1 { t.Errorf("want data to contain 1 items, has %d", l) } } func TestTreeTable(t *testing.T) { tree := tree.New(). Child( "Foo", tree.New(). Root("Bar"). Child( "Baz", "Baz", table.New(). Width(20). StyleFunc(func(row, col int) lipgloss.Style { return lipgloss.NewStyle().Padding(0, 1) }). Headers("Foo", "Bar"). Row("Qux", "Baz"). Row("Qux", "Baz"), "Baz", ), "Qux", ) golden.RequireEqual(t, []byte(tree.String())) } func TestAddItemWithAndWithoutRoot(t *testing.T) { t.Run("with root", func(t *testing.T) { t1 := tree.New(). Child( "Foo", "Bar", tree.New(). Child("Baz"), "Qux", ) golden.RequireEqual(t, []byte(t1.String())) }) t.Run("without root", func(t *testing.T) { t2 := tree.New(). Child( "Foo", tree.New(). Root("Bar"). Child("Baz"), "Qux", ) golden.RequireEqual(t, []byte(t2.String())) }) } func TestEmbedListWithinTree(t *testing.T) { t1 := tree.New(). Child(list.New("A", "B", "C"). Enumerator(list.Arabic)). Child(list.New("1", "2", "3"). Enumerator(list.Alphabet)) golden.RequireEqual(t, []byte(t1.String())) } func TestMultilinePrefix(t *testing.T) { paddingsStyle := lipgloss.NewStyle().PaddingLeft(1).PaddingBottom(1) tree := tree.New(). Enumerator(func(_ tree.Children, i int) string { if i == 1 { return "│\n│" } return " " }). Indenter(func(_ tree.Children, i int) string { return " " }). ItemStyle(paddingsStyle). Child("Foo Document\nThe Foo Files"). Child("Bar Document\nThe Bar Files"). Child("Baz Document\nThe Baz Files") golden.RequireEqual(t, []byte(tree.String())) } func TestMultilinePrefixSubtree(t *testing.T) { paddingsStyle := lipgloss.NewStyle(). Padding(0, 0, 1, 1) tree := tree.New(). Child("Foo"). Child("Bar"). Child( tree.New(). Root("Baz"). Enumerator(func(_ tree.Children, i int) string { if i == 1 { return "│\n│" } return " " }). Indenter(func(tree.Children, int) string { return " " }). ItemStyle(paddingsStyle). Child("Foo Document\nThe Foo Files"). Child("Bar Document\nThe Bar Files"). Child("Baz Document\nThe Baz Files"), ). Child("Qux") golden.RequireEqual(t, []byte(tree.String())) } func TestMultilinePrefixInception(t *testing.T) { glowEnum := func(_ tree.Children, i int) string { if i == 1 { return "│\n│" } return " " } glowIndenter := func(_ tree.Children, i int) string { return " " } paddingsStyle := lipgloss.NewStyle().PaddingLeft(1).PaddingBottom(1) tree := tree.New(). Enumerator(glowEnum). Indenter(glowIndenter). ItemStyle(paddingsStyle). Child("Foo Document\nThe Foo Files"). Child("Bar Document\nThe Bar Files"). Child( tree.New(). Enumerator(glowEnum). Indenter(glowIndenter). ItemStyle(paddingsStyle). Child("Qux Document\nThe Qux Files"). Child("Quux Document\nThe Quux Files"). Child("Quuux Document\nThe Quuux Files"), ). Child("Baz Document\nThe Baz Files") golden.RequireEqual(t, []byte(tree.String())) } func TestTypes(t *testing.T) { tree := tree.New(). Child(0). Child(true). Child([]any{"Foo", "Bar"}). Child([]string{"Qux", "Quux", "Quuux"}) golden.RequireEqual(t, []byte(tree.String())) } ================================================ FILE: unset.go ================================================ package lipgloss // unset unsets a property from a style. func (s *Style) unset(key propKey) { s.props = s.props.unset(key) } // UnsetBold removes the bold style rule, if set. func (s Style) UnsetBold() Style { s.unset(boldKey) return s } // UnsetItalic removes the italic style rule, if set. func (s Style) UnsetItalic() Style { s.unset(italicKey) return s } // UnsetUnderline removes the underline style rule, if set. func (s Style) UnsetUnderline() Style { return s.Underline(false) } // UnsetStrikethrough removes the strikethrough style rule, if set. func (s Style) UnsetStrikethrough() Style { s.unset(strikethroughKey) return s } // UnsetReverse removes the reverse style rule, if set. func (s Style) UnsetReverse() Style { s.unset(reverseKey) return s } // UnsetBlink removes the blink style rule, if set. func (s Style) UnsetBlink() Style { s.unset(blinkKey) return s } // UnsetFaint removes the faint style rule, if set. func (s Style) UnsetFaint() Style { s.unset(faintKey) return s } // UnsetForeground removes the foreground style rule, if set. func (s Style) UnsetForeground() Style { s.unset(foregroundKey) return s } // UnsetBackground removes the background style rule, if set. func (s Style) UnsetBackground() Style { s.unset(backgroundKey) return s } // UnsetWidth removes the width style rule, if set. func (s Style) UnsetWidth() Style { s.unset(widthKey) return s } // UnsetHeight removes the height style rule, if set. func (s Style) UnsetHeight() Style { s.unset(heightKey) return s } // UnsetAlign removes the horizontal and vertical text alignment style rule, if set. func (s Style) UnsetAlign() Style { s.unset(alignHorizontalKey) s.unset(alignVerticalKey) return s } // UnsetAlignHorizontal removes the horizontal text alignment style rule, if set. func (s Style) UnsetAlignHorizontal() Style { s.unset(alignHorizontalKey) return s } // UnsetAlignVertical removes the vertical text alignment style rule, if set. func (s Style) UnsetAlignVertical() Style { s.unset(alignVerticalKey) return s } // UnsetPadding removes all padding style rules. func (s Style) UnsetPadding() Style { s.unset(paddingLeftKey) s.unset(paddingRightKey) s.unset(paddingTopKey) s.unset(paddingBottomKey) s.unset(paddingCharKey) return s } // UnsetPaddingChar removes the padding character style rule, if set. func (s Style) UnsetPaddingChar() Style { s.unset(paddingCharKey) return s } // UnsetPaddingLeft removes the left padding style rule, if set. func (s Style) UnsetPaddingLeft() Style { s.unset(paddingLeftKey) return s } // UnsetPaddingRight removes the right padding style rule, if set. func (s Style) UnsetPaddingRight() Style { s.unset(paddingRightKey) return s } // UnsetPaddingTop removes the top padding style rule, if set. func (s Style) UnsetPaddingTop() Style { s.unset(paddingTopKey) return s } // UnsetPaddingBottom removes the bottom padding style rule, if set. func (s Style) UnsetPaddingBottom() Style { s.unset(paddingBottomKey) return s } // UnsetColorWhitespace removes the rule for coloring padding, if set. func (s Style) UnsetColorWhitespace() Style { s.unset(colorWhitespaceKey) return s } // UnsetMargins removes all margin style rules. func (s Style) UnsetMargins() Style { s.unset(marginLeftKey) s.unset(marginRightKey) s.unset(marginTopKey) s.unset(marginBottomKey) return s } // UnsetMarginLeft removes the left margin style rule, if set. func (s Style) UnsetMarginLeft() Style { s.unset(marginLeftKey) return s } // UnsetMarginRight removes the right margin style rule, if set. func (s Style) UnsetMarginRight() Style { s.unset(marginRightKey) return s } // UnsetMarginTop removes the top margin style rule, if set. func (s Style) UnsetMarginTop() Style { s.unset(marginTopKey) return s } // UnsetMarginBottom removes the bottom margin style rule, if set. func (s Style) UnsetMarginBottom() Style { s.unset(marginBottomKey) return s } // UnsetMarginBackground removes the margin's background color. Note that the // margin's background color can be set from the background color of another // style during inheritance. func (s Style) UnsetMarginBackground() Style { s.unset(marginBackgroundKey) return s } // UnsetBorderStyle removes the border style rule, if set. func (s Style) UnsetBorderStyle() Style { s.unset(borderStyleKey) return s } // UnsetBorderTop removes the border top style rule, if set. func (s Style) UnsetBorderTop() Style { s.unset(borderTopKey) return s } // UnsetBorderRight removes the border right style rule, if set. func (s Style) UnsetBorderRight() Style { s.unset(borderRightKey) return s } // UnsetBorderBottom removes the border bottom style rule, if set. func (s Style) UnsetBorderBottom() Style { s.unset(borderBottomKey) return s } // UnsetBorderLeft removes the border left style rule, if set. func (s Style) UnsetBorderLeft() Style { s.unset(borderLeftKey) return s } // UnsetBorderForeground removes all border foreground color styles, if set. func (s Style) UnsetBorderForeground() Style { s.unset(borderTopForegroundKey) s.unset(borderRightForegroundKey) s.unset(borderBottomForegroundKey) s.unset(borderLeftForegroundKey) return s } // UnsetBorderTopForeground removes the top border foreground color rule, // if set. func (s Style) UnsetBorderTopForeground() Style { s.unset(borderTopForegroundKey) return s } // UnsetBorderRightForeground removes the right border foreground color rule, // if set. func (s Style) UnsetBorderRightForeground() Style { s.unset(borderRightForegroundKey) return s } // UnsetBorderBottomForeground removes the bottom border foreground color // rule, if set. func (s Style) UnsetBorderBottomForeground() Style { s.unset(borderBottomForegroundKey) return s } // UnsetBorderLeftForeground removes the left border foreground color rule, // if set. func (s Style) UnsetBorderLeftForeground() Style { s.unset(borderLeftForegroundKey) return s } // UnsetBorderForegroundBlend removes the border blend foreground color rules, // if set. func (s Style) UnsetBorderForegroundBlend() Style { s.unset(borderForegroundBlendKey) return s } // UnsetBorderForegroundBlendOffset removes the border blend offset style rule, // if set. func (s Style) UnsetBorderForegroundBlendOffset() Style { s.unset(borderForegroundBlendOffsetKey) return s } // UnsetBorderBackground removes all border background color styles, if // set. func (s Style) UnsetBorderBackground() Style { s.unset(borderTopBackgroundKey) s.unset(borderRightBackgroundKey) s.unset(borderBottomBackgroundKey) s.unset(borderLeftBackgroundKey) return s } // UnsetBorderTopBackgroundColor removes the top border background color rule, // if set. // // Deprecated: This function simply calls Style.UnsetBorderTopBackground. func (s Style) UnsetBorderTopBackgroundColor() Style { return s.UnsetBorderTopBackground() } // UnsetBorderTopBackground removes the top border background color rule, // if set. func (s Style) UnsetBorderTopBackground() Style { s.unset(borderTopBackgroundKey) return s } // UnsetBorderRightBackground removes the right border background color // rule, if set. func (s Style) UnsetBorderRightBackground() Style { s.unset(borderRightBackgroundKey) return s } // UnsetBorderBottomBackground removes the bottom border background color // rule, if set. func (s Style) UnsetBorderBottomBackground() Style { s.unset(borderBottomBackgroundKey) return s } // UnsetBorderLeftBackground removes the left border color rule, if set. func (s Style) UnsetBorderLeftBackground() Style { s.unset(borderLeftBackgroundKey) return s } // UnsetInline removes the inline style rule, if set. func (s Style) UnsetInline() Style { s.unset(inlineKey) return s } // UnsetMaxWidth removes the max width style rule, if set. func (s Style) UnsetMaxWidth() Style { s.unset(maxWidthKey) return s } // UnsetMaxHeight removes the max height style rule, if set. func (s Style) UnsetMaxHeight() Style { s.unset(maxHeightKey) return s } // UnsetTabWidth removes the tab width style rule, if set. func (s Style) UnsetTabWidth() Style { s.unset(tabWidthKey) return s } // UnsetUnderlineSpaces removes the value set by UnderlineSpaces. func (s Style) UnsetUnderlineSpaces() Style { s.unset(underlineSpacesKey) return s } // UnsetStrikethroughSpaces removes the value set by StrikethroughSpaces. func (s Style) UnsetStrikethroughSpaces() Style { s.unset(strikethroughSpacesKey) return s } // UnsetTransform removes the value set by Transform. func (s Style) UnsetTransform() Style { s.unset(transformKey) return s } // UnsetHyperlink removes the value set by Hyperlink. func (s Style) UnsetHyperlink() Style { s.unset(linkKey) s.unset(linkParamsKey) s.link, s.linkParams = "", "" // save memory return s } // UnsetString sets the underlying string value to the empty string. func (s Style) UnsetString() Style { s.value = "" return s } ================================================ FILE: whitespace.go ================================================ package lipgloss import ( "strings" "github.com/charmbracelet/x/ansi" ) // whitespace is a whitespace renderer. type whitespace struct { chars string style Style } // newWhitespace creates a new whitespace renderer. func newWhitespace(opts ...WhitespaceOption) *whitespace { w := &whitespace{} for _, opt := range opts { opt(w) } return w } // Render whitespaces. func (w whitespace) render(width int) string { if w.chars == "" { w.chars = " " } r := []rune(w.chars) j := 0 b := strings.Builder{} // Cycle through runes and print them into the whitespace. for i := 0; i < width; { b.WriteRune(r[j]) // Measure the width of the rune we just wrote, ensuring we always // make progress to avoid infinite loops with zero-width characters // like tabs. runeWidth := ansi.StringWidth(string(r[j])) if runeWidth < 1 { runeWidth = 1 } i += runeWidth j++ if j >= len(r) { j = 0 } } // Fill any extra gaps white spaces. This might be necessary if any runes // are more than one cell wide, which could leave a one-rune gap. short := width - ansi.StringWidth(b.String()) if short > 0 { b.WriteString(strings.Repeat(" ", short)) } return w.style.Render(b.String()) } // WhitespaceOption sets a styling rule for rendering whitespace. type WhitespaceOption func(*whitespace) // WithWhitespaceStyle sets the style for the whitespace. func WithWhitespaceStyle(s Style) WhitespaceOption { return func(w *whitespace) { w.style = s } } // WithWhitespaceChars sets the characters to be rendered in the whitespace. func WithWhitespaceChars(s string) WhitespaceOption { return func(w *whitespace) { w.chars = s } } ================================================ FILE: whitespace_test.go ================================================ package lipgloss import ( "testing" "time" ) func TestWhitespaceRenderWithTab(t *testing.T) { // This test verifies that rendering whitespace with tab characters // doesn't cause an infinite loop (issue #108) done := make(chan bool, 1) go func() { ws := newWhitespace(WithWhitespaceChars("\t")) _ = ws.render(10) done <- true }() select { case <-done: // Success - render completed case <-time.After(2 * time.Second): t.Fatal("whitespace.render() with tab character caused infinite loop") } } func TestWhitespaceRenderWithZeroWidthChar(t *testing.T) { // Test with zero-width joiner (another zero-width character) done := make(chan bool, 1) go func() { ws := newWhitespace(WithWhitespaceChars("\u200d")) // zero-width joiner _ = ws.render(5) done <- true }() select { case <-done: // Success case <-time.After(2 * time.Second): t.Fatal("whitespace.render() with zero-width character caused infinite loop") } } func TestWhitespaceRenderNormal(t *testing.T) { // Verify normal behavior still works ws := newWhitespace(WithWhitespaceChars("*")) result := ws.render(5) if len(result) != 5 { t.Errorf("expected 5 characters, got %d", len(result)) } } ================================================ FILE: wrap.go ================================================ package lipgloss import ( "bytes" "io" uv "github.com/charmbracelet/ultraviolet" "github.com/charmbracelet/x/ansi" ) // Wrap wraps the given string to the given width, preserving ANSI styles and links. func Wrap(s string, width int, breakpoints string) string { var buf bytes.Buffer s = ansi.Wrap(s, width, breakpoints) w := NewWrapWriter(&buf) defer w.Close() //nolint:errcheck _, _ = io.WriteString(w, s) return buf.String() } // WrapWriter is a writer that writes to a buffer and keeps track of the // current pen style and link state for the purpose of wrapping with newlines. // // When it encounters a newline, it resets the style and link, writes the // newline, and then reapplies the style and link to the next line. type WrapWriter struct { w io.Writer p *ansi.Parser style uv.Style link uv.Link } // NewWrapWriter returns a new [WrapWriter]. func NewWrapWriter(w io.Writer) *WrapWriter { pw := &WrapWriter{w: w} pw.p = ansi.GetParser() handleCsi := func(cmd ansi.Cmd, params ansi.Params) { if cmd == 'm' { uv.ReadStyle(params, &pw.style) } } handleOsc := func(cmd int, data []byte) { if cmd == 8 { uv.ReadLink(data, &pw.link) } } pw.p.SetHandler(ansi.Handler{ HandleCsi: handleCsi, HandleOsc: handleOsc, }) return pw } // Style returns the current pen style. func (w *WrapWriter) Style() uv.Style { return w.style } // Link returns the current pen link. func (w *WrapWriter) Link() uv.Link { return w.link } // Write writes to the buffer. func (w *WrapWriter) Write(p []byte) (int, error) { for i := range p { b := p[i] w.p.Advance(b) if b == '\n' { if !w.style.IsZero() { _, _ = w.w.Write([]byte(ansi.ResetStyle)) } if !w.link.IsZero() { _, _ = w.w.Write([]byte(ansi.ResetHyperlink())) } } _, _ = w.w.Write([]byte{b}) if b == '\n' { if !w.link.IsZero() { _, _ = w.w.Write([]byte(ansi.SetHyperlink(w.link.URL, w.link.Params))) } if !w.style.IsZero() { _, _ = w.w.Write([]byte(w.style.String())) } } } return len(p), nil } // Close closes the writer, resets the style and link if necessary, and releases // its parser. Calling it is performance critical, but forgetting it does not // cause safety issues or leaks. func (w *WrapWriter) Close() error { if !w.style.IsZero() { _, _ = w.w.Write([]byte(ansi.ResetStyle)) } if !w.link.IsZero() { _, _ = w.w.Write([]byte(ansi.ResetHyperlink())) } if w.p != nil { ansi.PutParser(w.p) w.p = nil } return nil } ================================================ FILE: writer.go ================================================ package lipgloss import ( "bytes" "fmt" "io" "os" "github.com/charmbracelet/colorprofile" ) // Writer is the default writer that prints to stdout, automatically // downsampling colors when necessary. var Writer = colorprofile.NewWriter(os.Stdout, os.Environ()) // Println to stdout, automatically downsampling colors when necessary, ending // with a trailing newline. // // Example: // // str := NewStyle(). // Foreground(lipgloss.Color("#6a00ff")). // Render("breakfast") // // Println("Time for a", str, "sandwich!") func Println(v ...any) (int, error) { return fmt.Fprintln(Writer, v...) //nolint:wrapcheck } // Printf prints formatted text to stdout, automatically downsampling colors // when necessary. // // Example: // // str := NewStyle(). // Foreground(lipgloss.Color("#6a00ff")). // Render("knuckle") // // Printf("Time for a %s sandwich!\n", str) func Printf(format string, v ...any) (int, error) { return fmt.Fprintf(Writer, format, v...) //nolint:wrapcheck } // Print to stdout, automatically downsampling colors when necessary. // // Example: // // str := NewStyle(). // Foreground(lipgloss.Color("#6a00ff")). // Render("Who wants marmalade?\n") // // Print(str) func Print(v ...any) (int, error) { return fmt.Fprint(Writer, v...) //nolint:wrapcheck } // Fprint pritnts to the given writer, automatically downsampling colors when // necessary. // // Example: // // str := NewStyle(). // Foreground(lipgloss.Color("#6a00ff")). // Render("guzzle") // // Fprint(os.Stderr, "I %s horchata pretty much all the time.\n", str) func Fprint(w io.Writer, v ...any) (int, error) { return fmt.Fprint(colorprofile.NewWriter(w, os.Environ()), v...) //nolint:wrapcheck } // Fprintln prints to the given writer, automatically downsampling colors when // necessary, and ending with a trailing newline. // // Example: // // str := NewStyle(). // Foreground(lipgloss.Color("#6a00ff")). // Render("Sandwich time!") // // Fprintln(os.Stderr, str) func Fprintln(w io.Writer, v ...any) (int, error) { return fmt.Fprintln(colorprofile.NewWriter(w, os.Environ()), v...) //nolint:wrapcheck } // Fprintf prints text to a writer, against the given format, automatically // downsampling colors when necessary. // // Example: // // str := NewStyle(). // Foreground(lipgloss.Color("#6a00ff")). // Render("artichokes") // // Fprintf(os.Stderr, "I really love %s!\n", food) func Fprintf(w io.Writer, format string, v ...any) (int, error) { return fmt.Fprintf(colorprofile.NewWriter(w, os.Environ()), format, v...) //nolint:wrapcheck } // Sprint returns a string for stdout, automatically downsampling colors when // necessary. // // Example: // // str := NewStyle(). // Faint(true). // Foreground(lipgloss.Color("#6a00ff")). // Render("I love to eat") // // str = Sprint(str) func Sprint(v ...any) string { var buf bytes.Buffer w := colorprofile.Writer{ Forward: &buf, Profile: Writer.Profile, } fmt.Fprint(&w, v...) //nolint:errcheck return buf.String() } // Sprintln returns a string for stdout, automatically downsampling colors when // necessary, and ending with a trailing newline. // // Example: // // str := NewStyle(). // Bold(true). // Foreground(lipgloss.Color("#6a00ff")). // Render("Yummy!") // // str = Sprintln(str) func Sprintln(v ...any) string { var buf bytes.Buffer w := colorprofile.Writer{ Forward: &buf, Profile: Writer.Profile, } fmt.Fprintln(&w, v...) //nolint:errcheck return buf.String() } // Sprintf returns a formatted string for stdout, automatically downsampling // colors when necessary. // // Example: // // str := NewStyle(). // Bold(true). // Foreground(lipgloss.Color("#fccaee")). // Render("Cantaloupe") // // str = Sprintf("I really love %s!", str) func Sprintf(format string, v ...any) string { var buf bytes.Buffer w := colorprofile.Writer{ Forward: &buf, Profile: Writer.Profile, } fmt.Fprintf(&w, format, v...) //nolint:errcheck return buf.String() }