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
Style definitions for nice terminal layouts. Built with TUIs in mind.
 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
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).
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
[38;5;99mI.[m aaa
[38;5;99mII.[m bbb
• Deep
[38;5;212mA.[m foo
[38;5;212mB.[m Deeper
[38;5;212m [m [38;5;99m1.[m a
[38;5;212m [m [38;5;99m2.[m b
[38;5;212m [m [38;5;99m3.[m Even Deeper, inherit parent renderer
[38;5;212m [m [38;5;99m [m [38;5;212m*[m sus
[38;5;212m [m [38;5;99m [m [38;5;212m*[m d minor
[38;5;212m [m [38;5;99m [m [38;5;212m*[m f#
[38;5;212m [m [38;5;99m [m [38;5;212m*[m One ore level, with another renderer
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m-[m a
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m multine
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m string
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m-[m hoccus poccus
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m-[m abra kadabra
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m-[m And finally, a tree within all this
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m [38;5;212m├──[m another
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m [38;5;212m│ [m multine
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m [38;5;212m│ [m string
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m [38;5;212m├──[m something
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m [38;5;212m├──[m a subtree
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m [38;5;212m│ [m [38;5;212m├──[m yup
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m [38;5;212m│ [m [38;5;212m├──[m many itens
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m [38;5;212m│ [m [38;5;212m└──[m another
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m [38;5;212m├──[m hallo
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m [38;5;212m└──[m wunderbar!
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m-[m this is a tree
[38;5;212m [m [38;5;99m [m [38;5;212m [m [38;5;99m [m and other obvious statements
[38;5;212mC.[m 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 │
├────────────┼────────────────┼─────────────┤
│ │ │ │
│ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │
│ [48;2;135;75;252m [m[48;2;135;75;252m [m[48;2;135;75;252mChinese[m[48;2;135;75;252m [m │ [48;2;135;75;252m [m[48;2;135;75;252m [m[48;2;135;75;252mNǐn hǎo[m[48;2;135;75;252m [m │ [48;2;135;75;252m [m[48;2;135;75;252m [m[48;2;135;75;252mNǐ hǎo[m[48;2;135;75;252m [m │
│ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │
│ │ │ │
│ │ │ │
│ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │
│ [48;2;135;75;252m [m[48;2;135;75;252m [m[48;2;135;75;252mFrench[m[48;2;135;75;252m [m │ [48;2;135;75;252m [m[48;2;135;75;252m [m[48;2;135;75;252mBonjour[m[48;2;135;75;252m [m │ [48;2;135;75;252m [m[48;2;135;75;252m [m[48;2;135;75;252mSalut[m[48;2;135;75;252m [m │
│ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │
│ │ │ │
│ │ │ │
│ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │
│ [48;2;135;75;252m [m[48;2;135;75;252mJapanese[m[48;2;135;75;252m [m │ [48;2;135;75;252m [m[48;2;135;75;252m [m[48;2;135;75;252mこんにちは[m[48;2;135;75;252m [m │ [48;2;135;75;252m [m[48;2;135;75;252m [m[48;2;135;75;252mやあ[m[48;2;135;75;252m [m │
│ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │
│ │ │ │
│ │ │ │
│ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │
│ [48;2;135;75;252m [m[48;2;135;75;252m [m[48;2;135;75;252mRussian[m[48;2;135;75;252m [m │ [48;2;135;75;252m [m[48;2;135;75;252mZdravstvuyte[m[48;2;135;75;252m [m │ [48;2;135;75;252m [m[48;2;135;75;252m [m[48;2;135;75;252mPrivet[m[48;2;135;75;252m [m │
│ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │
│ │ │ │
│ │ │ │
│ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │
│ [48;2;135;75;252m [m[48;2;135;75;252m [m[48;2;135;75;252mSpanish[m[48;2;135;75;252m [m │ [48;2;135;75;252m [m[48;2;135;75;252m [m[48;2;135;75;252mHola[m[48;2;135;75;252m [m │ [48;2;135;75;252m [m[48;2;135;75;252m¿Qué tal?[m[48;2;135;75;252m [m │
│ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │ [48;2;135;75;252m [m │
│ │ │ │
└────────────┴────────────────┴─────────────┘
================================================
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 │ [31mC[0m[32mo[0m[34md[0m[33me[0m │
├───────────┼────────┼──────┤
│ Apple │ Red │ [31m31[0m │
│ Lime │ Green │ [32m32[0m │
│ Banana │ Yellow │ [33m33[0m │
│ Blueberry │ Blue │ [34m34[0m │
└───────────┴────────┴──────┘
================================================
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
================================================
[38;5;99m┌[m[38;5;99m──────────[m[38;5;99m┬[m[38;5;99m───────────────────────────────[m[38;5;99m┬[m[38;5;99m─────────────────[m[38;5;99m┐[m
[38;5;99m│[m LANGUAGE [38;5;99m│[m FORMAL [38;5;99m│[m INFORMAL [38;5;99m│[m
[38;5;99m├[m[38;5;99m──────────[m[38;5;99m┼[m[38;5;99m───────────────────────────────[m[38;5;99m┼[m[38;5;99m─────────────────[m[38;5;99m┤[m
[38;5;99m│[m Chinese [38;5;99m│[m 您好 [38;5;99m│[m 你好 [38;5;99m│[m
[38;5;99m│[m Japanese [38;5;99m│[m こんにちは [38;5;99m│[m やあ [38;5;99m│[m
[38;5;99m│[m Russian [38;5;99m│[m Здравствуйте [38;5;99m│[m Привет [38;5;99m│[m
[38;5;99m│[m Spanish [38;5;99m│[m Hola [38;5;99m│[m ¿Qué tal? [38;5;99m│[m
[38;5;99m│[m English [38;5;99m│[m You look absolutely fabulous. [38;5;99m│[m How's it going? [38;5;99m│[m
[38;5;99m└[m[38;5;99m──────────[m[38;5;99m┴[m[38;5;99m───────────────────────────────[m[38;5;99m┴[m[38;5;99m─────────────────[m[38;5;99m┘[m
================================================
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
================================================
[97;48;5;18m┌[m[97;48;5;18m────────[m[97;48;5;18m┬[m[97;48;5;18m────────────[m[97;48;5;18m┬[m[97;48;5;18m─────────[m[97;48;5;18m┐[m
[97;48;5;18m│[m[97;48;5;18mLANGUAGE[m[97;48;5;18m│[m[97;48;5;18mFORMAL[m[48;5;18m [m[97;48;5;18m│[m[97;48;5;18mINFORMAL[m[48;5;18m [m[97;48;5;18m│[m
[97;48;5;18m├[m[97;48;5;18m────────[m[97;48;5;18m┼[m[97;48;5;18m────────────[m[97;48;5;18m┼[m[97;48;5;18m─────────[m[97;48;5;18m┤[m
[97;48;5;18m│[m[97;48;5;18mChinese[m[48;5;18m [m[97;48;5;18m│[m[97;48;5;18mNǐn hǎo[m[48;5;18m [m[97;48;5;18m│[m[97;48;5;18mNǐ hǎo[m[48;5;18m [m[97;48;5;18m│[m
[97;48;5;18m│[m[97;48;5;18mFrench[m[48;5;18m [m[97;48;5;18m│[m[97;48;5;18mBonjour[m[48;5;18m [m[97;48;5;18m│[m[97;48;5;18mSalut[m[48;5;18m [m[97;48;5;18m│[m
[97;48;5;18m│[m[97;48;5;18mJapanese[m[97;48;5;18m│[m[97;48;5;18mこんにちは[m[48;5;18m [m[97;48;5;18m│[m[97;48;5;18mやあ[m[48;5;18m [m[97;48;5;18m│[m
[97;48;5;18m│[m[97;48;5;18mRussian[m[48;5;18m [m[97;48;5;18m│[m[97;48;5;18mZdravstvuyte[m[97;48;5;18m│[m[97;48;5;18mPrivet[m[48;5;18m [m[97;48;5;18m│[m
[97;48;5;18m│[m[97;48;5;18mSpanish[m[48;5;18m [m[97;48;5;18m│[m[97;48;5;18mHola[m[48;5;18m [m[97;48;5;18m│[m[97;48;5;18m¿Qué tal?[m[97;48;5;18m│[m
[97;48;5;18m└[m[97;48;5;18m────────[m[97;48;5;18m┴[m[97;48;5;18m────────────[m[97;48;5;18m┴[m[97;48;5;18m─────────[m[97;48;5;18m┘[m
================================================
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 │[38;2;49;187;113mhttps://aur.archlinux.org/packag[m│ │ │
│ │ │[38;2;49;187;113mes/sourcegit-bin[m │ │ │
│ │ │ │ │ │
│Welcome │いらっしゃいませ│مرحباً │환영 │欢迎│
│Goodbye │さようなら │مع السلامة │안녕히 가세요│再见│
└─────────┴────────────────┴────────────────────────────────┴─────────────┴────┘
================================================
FILE: table/testdata/TestWrapStyleFuncContent.golden
================================================
┌─────────┬────────────────┬───────────────────────────────┐
│Package │Version │Link │
├─────────┼────────────────┼───────────────────────────────┤
│sourcegit│0.19 │[38;2;49;187;113mhttps://aur.archlinux.org/packa[m│
│ │ │[38;2;49;187;113mges/sourcegit-bin[m │
│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
================================================
[94m->[m [91mFoo[m
[94m->[m [91mBar[m
[94m->[m [94m->[m [91mQux[m
[94m->[m [94m->[m [91mQuux[m
[94m->[m [94m->[m [94m->[m [91mFoo[m
[94m->[m [94m->[m [94m->[m [91mBar[m
[94m->[m [94m->[m [91mQuuux[m
[94m->[m [91mBaz[m
================================================
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()
}