pax_global_header 0000666 0000000 0000000 00000000064 14764354362 0014530 g ustar 00root root 0000000 0000000 52 comment=f0e45475a64ee60d712b81145172d3739db36a93
charmbracelet-lipgloss-500b193/ 0000775 0000000 0000000 00000000000 14764354362 0016423 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/.github/ 0000775 0000000 0000000 00000000000 14764354362 0017763 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/.github/CODEOWNERS 0000664 0000000 0000000 00000000036 14764354362 0021355 0 ustar 00root root 0000000 0000000 * @meowgorithm @aymanbagabas
charmbracelet-lipgloss-500b193/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14764354362 0022146 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000001554 14764354362 0024645 0 ustar 00root root 0000000 0000000 ---
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.
charmbracelet-lipgloss-500b193/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000000170 14764354362 0024134 0 ustar 00root root 0000000 0000000 blank_issues_enabled: true
contact_links:
- name: Discord
url: https://charm.sh/discord
about: Chat on our Discord.
charmbracelet-lipgloss-500b193/.github/ISSUE_TEMPLATE/feature_request.md 0000664 0000000 0000000 00000001134 14764354362 0025672 0 ustar 00root root 0000000 0000000 ---
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.
charmbracelet-lipgloss-500b193/.github/dependabot.yml 0000664 0000000 0000000 00000002110 14764354362 0022605 0 ustar 00root root 0000000 0000000 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"
- 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"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "feat"
include: "scope"
- 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"
charmbracelet-lipgloss-500b193/.github/workflows/ 0000775 0000000 0000000 00000000000 14764354362 0022020 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/.github/workflows/build.yml 0000664 0000000 0000000 00000000326 14764354362 0023643 0 ustar 00root root 0000000 0000000 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 }}
charmbracelet-lipgloss-500b193/.github/workflows/coverage.yml 0000664 0000000 0000000 00000001753 14764354362 0024344 0 ustar 00root root 0000000 0000000 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@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v4
- 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
charmbracelet-lipgloss-500b193/.github/workflows/dependabot-sync.yml 0000664 0000000 0000000 00000000643 14764354362 0025625 0 ustar 00root root 0000000 0000000 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 }}
charmbracelet-lipgloss-500b193/.github/workflows/lint-sync.yml 0000664 0000000 0000000 00000000417 14764354362 0024465 0 ustar 00root root 0000000 0000000 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
charmbracelet-lipgloss-500b193/.github/workflows/lint.yml 0000664 0000000 0000000 00000000163 14764354362 0023511 0 ustar 00root root 0000000 0000000 name: lint
on:
push:
pull_request:
jobs:
lint:
uses: charmbracelet/meta/.github/workflows/lint.yml@main
charmbracelet-lipgloss-500b193/.github/workflows/release.yml 0000664 0000000 0000000 00000002122 14764354362 0024160 0 ustar 00root root 0000000 0000000 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
charmbracelet-lipgloss-500b193/.gitignore 0000664 0000000 0000000 00000000033 14764354362 0020407 0 ustar 00root root 0000000 0000000 ssh_example_ed25519*
dist/
charmbracelet-lipgloss-500b193/.golangci.yml 0000664 0000000 0000000 00000001070 14764354362 0021005 0 ustar 00root root 0000000 0000000 run:
tests: false
issues:
include:
- EXC0001
- EXC0005
- EXC0011
- EXC0012
- EXC0013
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- bodyclose
- exhaustive
- goconst
- godot
- godox
- gofumpt
- goimports
- gomoddirectives
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
- tparallel
- unconvert
- unparam
- whitespace
- wrapcheck
charmbracelet-lipgloss-500b193/.goreleaser.yml 0000664 0000000 0000000 00000000250 14764354362 0021351 0 ustar 00root root 0000000 0000000 # yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
version: 2
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-lib.yaml
charmbracelet-lipgloss-500b193/.vscode/ 0000775 0000000 0000000 00000000000 14764354362 0017764 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/.vscode/settings.json 0000664 0000000 0000000 00000000121 14764354362 0022511 0 ustar 00root root 0000000 0000000 {
"files.trimTrailingWhitespace": false,
"files.insertFinalNewline": false
}
charmbracelet-lipgloss-500b193/LICENSE 0000664 0000000 0000000 00000002070 14764354362 0017427 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2021-2023 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.
charmbracelet-lipgloss-500b193/README.md 0000664 0000000 0000000 00000052426 14764354362 0017713 0 ustar 00root root 0000000 0000000 # 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 "github.com/charmbracelet/lipgloss"
var style = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#FAFAFA")).
Background(lipgloss.Color("#7D56F4")).
PaddingTop(2).
PaddingLeft(4).
Width(22)
fmt.Println(style.Render("Hello, kitty"))
```
## 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.
The terminal's color profile will be automatically detected, and colors outside
the gamut of the current palette will be automatically coerced to their closest
available value.
### Adaptive Colors
You can also specify color options for light and dark backgrounds:
```go
lipgloss.AdaptiveColor{Light: "236", Dark: "248"}
```
The terminal's background color will automatically be detected and the
appropriate color will be chosen at runtime.
### Complete Colors
CompleteColor specifies exact values for True Color, ANSI256, and ANSI color
profiles.
```go
lipgloss.CompleteColor{TrueColor: "#0000FF", ANSI256: "86", ANSI: "5"}
```
Automatic color degradation will not be performed in this case and it will be
based on the color specified.
### Complete Adaptive Colors
You can use `CompleteColor` with `AdaptiveColor` to specify the exact values for
light and dark backgrounds without automatic color degradation.
```go
lipgloss.CompleteAdaptiveColor{
Light: CompleteColor{TrueColor: "#d7ffae", ANSI256: "193", ANSI: "11"},
Dark: CompleteColor{TrueColor: "#d75fee", ANSI256: "163", ANSI: "5"},
}
```
## 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)
```
## 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)
```
## 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)
```
For more on borders see [the docs][docs].
## 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` data structures contains only primitive types, 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
```
## Rendering
Generally, you just call the `Render(string...)` method on a `lipgloss.Style`:
```go
style := lipgloss.NewStyle().Bold(true).SetString("Hello,")
fmt.Println(style.Render("kitty.")) // Hello, kitty.
fmt.Println(style.Render("puppy.")) // Hello, puppy.
```
But you could also use the Stringer interface:
```go
var style = lipgloss.NewStyle().SetString("你好,猫咪。").Bold(true)
fmt.Println(style) // 你好,猫咪。
```
### Custom Renderers
Custom renderers allow you to render to a specific outputs. This is
particularly important when you want to render to different outputs and
correctly detect the color profile and dark background status for each, such as
in a server-client situation.
```go
func myLittleHandler(sess ssh.Session) {
// Create a renderer for the client.
renderer := lipgloss.NewRenderer(sess)
// Create a new style on the renderer.
style := renderer.NewStyle().Background(lipgloss.AdaptiveColor{Light: "63", Dark: "228"})
// Render. The color profile and dark background state will be correctly detected.
io.WriteString(sess, style.Render("Heyyyyyyy"))
}
```
For an example on using a custom renderer over SSH with [Wish][wish] see the
[SSH example][ssh-example].
## Utilities
In addition to pure styling, Lip Gloss also ships with some utilities to help
assemble your layouts.
### Joining Paragraphs
Horizontally and vertically joining paragraphs is a cinch.
```go
// Horizontally join three paragraphs along their bottom edges
lipgloss.JoinHorizontal(lipgloss.Bottom, paragraphA, paragraphB, paragraphC)
// Vertically join two paragraphs along their center axes
lipgloss.JoinVertical(lipgloss.Center, paragraphA, paragraphB)
// Horizontally join three paragraphs, with the shorter ones aligning 20%
// from the top of the tallest
lipgloss.JoinHorizontal(0.2, paragraphA, paragraphB, paragraphC)
```
### Measuring Width and Height
Sometimes you’ll want to know the width and height of text blocks when building
your layouts.
```go
// Render a block of text.
var style = lipgloss.NewStyle().
Width(40).
Padding(2)
var block string = style.Render(someLongString)
// Get the actual, physical dimensions of the text block.
width := lipgloss.Width(block)
height := lipgloss.Height(block)
// Here's a shorthand function.
w, h := lipgloss.Size(block)
```
### Placing Text in Whitespace
Sometimes you’ll simply want to place a block of text in whitespace.
```go
// Center a paragraph horizontally in a space 80 cells wide. The height of
// the block returned will be as tall as the input paragraph.
block := lipgloss.PlaceHorizontal(80, lipgloss.Center, fancyStyledParagraph)
// Place a paragraph at the bottom of a space 30 cells tall. The width of
// the text block returned will be as wide as the input paragraph.
block := lipgloss.PlaceVertical(30, lipgloss.Bottom, fancyStyledParagraph)
// Place a paragraph in the bottom right corner of a 30x80 cell space.
block := lipgloss.Place(30, 80, lipgloss.Right, lipgloss.Bottom, fancyStyledParagraph)
```
You can also style the whitespace. For details, see [the docs][docs].
## Rendering Tables
Lip Gloss ships with a table rendering sub-package.
```go
import "github.com/charmbracelet/lipgloss/table"
```
Define some rows of data.
```go
rows := [][]string{
{"Chinese", "您好", "你好"},
{"Japanese", "こんにちは", "やあ"},
{"Arabic", "أهلين", "أهلا"},
{"Russian", "Здравствуйте", "Привет"},
{"Spanish", "Hola", "¿Qué tal?"},
}
```
Use the table package to style and render the table.
```go
var (
purple = lipgloss.Color("99")
gray = lipgloss.Color("245")
lightGray = lipgloss.Color("241")
headerStyle = lipgloss.NewStyle().Foreground(purple).Bold(true).Align(lipgloss.Center)
cellStyle = lipgloss.NewStyle().Padding(0, 1).Width(14)
oddRowStyle = cellStyle.Foreground(gray)
evenRowStyle = cellStyle.Foreground(lightGray)
)
t := table.New().
Border(lipgloss.NormalBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(purple)).
StyleFunc(func(row, col int) lipgloss.Style {
switch {
case row == table.HeaderRow:
return headerStyle
case row%2 == 0:
return evenRowStyle
default:
return oddRowStyle
}
}).
Headers("LANGUAGE", "FORMAL", "INFORMAL").
Rows(rows...)
// You can also add tables row-by-row
t.Row("English", "You look absolutely fabulous.", "How's it going?")
```
Print the table.
```go
fmt.Println(t)
```

> [!WARNING]
> Table `Rows` need to be declared before `Offset` otherwise it does nothing.
### Table Borders
There are helpers to generate tables in markdown or ASCII style:
#### Markdown Table
```go
table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false)
```
```
| LANGUAGE | FORMAL | INFORMAL |
|----------|--------------|-----------|
| Chinese | Nǐn hǎo | Nǐ hǎo |
| French | Bonjour | Salut |
| Russian | Zdravstvuyte | Privet |
| Spanish | Hola | ¿Qué tal? |
```
#### ASCII Table
```go
table.New().Border(lipgloss.ASCIIBorder())
```
```
+----------+--------------+-----------+
| LANGUAGE | FORMAL | INFORMAL |
+----------+--------------+-----------+
| Chinese | Nǐn hǎo | Nǐ hǎo |
| French | Bonjour | Salut |
| Russian | Zdravstvuyte | Privet |
| Spanish | Hola | ¿Qué tal? |
+----------+--------------+-----------+
```
For more on tables see [the docs](https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc) and [examples](https://github.com/charmbracelet/lipgloss/tree/master/examples/table).
## Rendering Lists
Lip Gloss ships with a list rendering sub-package.
```go
import "github.com/charmbracelet/lipgloss/list"
```
Define a new list.
```go
l := list.New("A", "B", "C")
```
Print the list.
```go
fmt.Println(l)
// • A
// • B
// • C
```
Lists have the ability to nest.
```go
l := list.New(
"A", list.New("Artichoke"),
"B", list.New("Baking Flour", "Bananas", "Barley", "Bean Sprouts"),
"C", list.New("Cashew Apple", "Cashews", "Coconut Milk", "Curry Paste", "Currywurst"),
"D", list.New("Dill", "Dragonfruit", "Dried Shrimp"),
"E", list.New("Eggs"),
"F", list.New("Fish Cake", "Furikake"),
"J", list.New("Jicama"),
"K", list.New("Kohlrabi"),
"L", list.New("Leeks", "Lentils", "Licorice Root"),
)
```
Print the list.
```go
fmt.Println(l)
```
Lists can be customized via their enumeration function as well as using
`lipgloss.Style`s.
```go
enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1)
itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")).MarginRight(1)
l := list.New(
"Glossier",
"Claire’s Boutique",
"Nyx",
"Mac",
"Milk",
).
Enumerator(list.Roman).
EnumeratorStyle(enumeratorStyle).
ItemStyle(itemStyle)
```
Print the list.
In addition to the predefined enumerators (`Arabic`, `Alphabet`, `Roman`, `Bullet`, `Tree`),
you may also define your own custom enumerator:
```go
l := list.New("Duck", "Duck", "Duck", "Duck", "Goose", "Duck", "Duck")
func DuckDuckGooseEnumerator(l list.Items, i int) string {
if l.At(i).Value() == "Goose" {
return "Honk →"
}
return ""
}
l = l.Enumerator(DuckDuckGooseEnumerator)
```
Print the list:
If you need, you can also build lists incrementally:
```go
l := list.New()
for i := 0; i < repeat; i++ {
l.Item("Lip Gloss")
}
```
## Rendering Trees
Lip Gloss ships with a tree rendering sub-package.
```go
import "github.com/charmbracelet/lipgloss/tree"
```
Define a new tree.
```go
t := tree.Root(".").
Child("A", "B", "C")
```
Print the tree.
```go
fmt.Println(t)
// .
// ├── A
// ├── B
// └── C
```
Trees have the ability to nest.
```go
t := tree.Root(".").
Child("macOS").
Child(
tree.New().
Root("Linux").
Child("NixOS").
Child("Arch Linux (btw)").
Child("Void Linux"),
).
Child(
tree.New().
Root("BSD").
Child("FreeBSD").
Child("OpenBSD"),
)
```
Print the tree.
```go
fmt.Println(t)
```
Trees can be customized via their enumeration function as well as using
`lipgloss.Style`s.
```go
enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1)
rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35"))
itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212"))
t := tree.
Root("⁜ Makeup").
Child(
"Glossier",
"Fenty Beauty",
tree.New().Child(
"Gloss Bomb Universal Lip Luminizer",
"Hot Cheeks Velour Blushlighter",
),
"Nyx",
"Mac",
"Milk",
).
Enumerator(tree.RoundedEnumerator).
EnumeratorStyle(enumeratorStyle).
RootStyle(rootStyle).
ItemStyle(itemStyle)
```
Print the tree.
The predefined enumerators for trees are `DefaultEnumerator` and `RoundedEnumerator`.
If you need, you can also build trees incrementally:
```go
t := tree.New()
for i := 0; i < repeat; i++ {
t.Child("Lip Gloss")
}
```
---
## FAQ
Why are things misaligning? Why are borders at the wrong widths?
This is most likely due to your locale and encoding, particularly with
regard to Chinese, Japanese, and Korean (for example, zh_CN.UTF-8
or ja_JP.UTF-8). The most direct way to fix this is to set
RUNEWIDTH_EASTASIAN=0 in your environment.
For details see https://github.com/charmbracelet/lipgloss/issues/40.
Why isn't Lip Gloss displaying colors?
Lip Gloss automatically degrades colors to the best available option in the
given terminal, and if output's not a TTY it will remove color output entirely.
This is common when running tests, CI, or when piping output elsewhere.
If necessary, you can force a color profile in your tests with
SetColorProfile.
```go
import (
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
)
lipgloss.SetColorProfile(termenv.TrueColor)
```
_Note:_ this option limits the flexibility of your application and can cause
ANSI escape codes to be output in cases where that might not be desired. Take
careful note of your use case and environment before choosing to force a color
profile.
## What about [Bubble Tea][tea]?
Lip Gloss doesn’t replace Bubble Tea. Rather, it is an excellent Bubble Tea
companion. It was designed to make assembling terminal user interface views as
simple and fun as possible so that you can focus on building your application
instead of concerning yourself with low-level layout details.
In simple terms, you can use Lip Gloss to help build your Bubble Tea views.
[tea]: https://github.com/charmbracelet/tea
## Under the Hood
Lip Gloss is built on the excellent [Termenv][termenv] and [Reflow][reflow]
libraries which deal with color and ANSI-aware text operations, respectively.
For many use cases Termenv and Reflow will be sufficient for your needs.
[termenv]: https://github.com/muesli/termenv
[reflow]: https://github.com/muesli/reflow
## Rendering Markdown
For a more document-centric rendering solution with support for things like
lists, tables, and syntax-highlighted code have a look at [Glamour][glamour],
the stylesheet-based Markdown renderer.
[glamour]: https://github.com/charmbracelet/glamour
## Contributing
See [contributing][contribute].
[contribute]: https://github.com/charmbracelet/lipgloss/contribute
## Feedback
We’d love to hear your thoughts on this project. Feel free to drop us a note!
- [Twitter](https://twitter.com/charmcli)
- [The Fediverse](https://mastodon.social/@charmcli)
- [Discord](https://charm.sh/chat)
## License
[MIT](https://github.com/charmbracelet/lipgloss/raw/master/LICENSE)
---
Part of [Charm](https://charm.sh).
Charm热爱开源 • Charm loves open source
[docs]: https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc
[wish]: https://github.com/charmbracelet/wish
[ssh-example]: examples/ssh
charmbracelet-lipgloss-500b193/Taskfile.yaml 0000664 0000000 0000000 00000000430 14764354362 0021046 0 ustar 00root root 0000000 0000000 # 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}}
charmbracelet-lipgloss-500b193/align.go 0000664 0000000 0000000 00000004215 14764354362 0020046 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"strings"
"github.com/charmbracelet/x/ansi"
"github.com/muesli/termenv"
)
// Perform text alignment. If the string is multi-lined, we also make all lines
// the same width by padding them with spaces. If a termenv style is passed,
// use that to style the spaces added.
func alignTextHorizontal(str string, pos Position, width int, style *termenv.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 { //nolint:exhaustive
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, _ *termenv.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
}
charmbracelet-lipgloss-500b193/align_test.go 0000664 0000000 0000000 00000003023 14764354362 0021101 0 ustar 00root root 0000000 0000000 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)
}
}
}
charmbracelet-lipgloss-500b193/ansi_unix.go 0000664 0000000 0000000 00000000222 14764354362 0020743 0 ustar 00root root 0000000 0000000 //go:build !windows
// +build !windows
package lipgloss
// enableLegacyWindowsANSI is only needed on Windows.
func enableLegacyWindowsANSI() {}
charmbracelet-lipgloss-500b193/ansi_windows.go 0000664 0000000 0000000 00000000745 14764354362 0021464 0 ustar 00root root 0000000 0000000 //go:build windows
// +build windows
package lipgloss
import (
"sync"
"github.com/muesli/termenv"
)
var enableANSI sync.Once
// enableANSIColors 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. Also note that Windows Terminal supports colors
// by default.
func enableLegacyWindowsANSI() {
enableANSI.Do(func() {
_, _ = termenv.EnableWindowsANSIConsole()
})
}
charmbracelet-lipgloss-500b193/borders.go 0000664 0000000 0000000 00000026120 14764354362 0020413 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"strings"
"github.com/charmbracelet/x/ansi"
"github.com/muesli/termenv"
"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 {
w := maxRuneWidth(piece)
if w > maxWidth {
maxWidth = w
}
}
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
}
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)
topFG = s.getAsColor(borderTopForegroundKey)
rightFG = s.getAsColor(borderRightForegroundKey)
bottomFG = s.getAsColor(borderBottomForegroundKey)
leftFG = s.getAsColor(borderLeftForegroundKey)
topBG = s.getAsColor(borderTopBackgroundKey)
rightBG = s.getAsColor(borderRightBackgroundKey)
bottomBG = s.getAsColor(borderBottomBackgroundKey)
leftBG = s.getAsColor(borderLeftBackgroundKey)
)
// If a border is set and no sides have been specifically turned on or off
// render borders on all sides.
if s.implicitBorders() {
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 && border.Right == "" {
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 out strings.Builder
// Render top
if hasTop {
top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
top = s.styleBorder(top, topFG, topBG)
out.WriteString(top)
out.WriteRune('\n')
}
leftRunes := []rune(border.Left)
leftIndex := 0
rightRunes := []rune(border.Right)
rightIndex := 0
// Render sides
for i, l := range lines {
if hasLeft {
r := string(leftRunes[leftIndex])
leftIndex++
if leftIndex >= len(leftRunes) {
leftIndex = 0
}
out.WriteString(s.styleBorder(r, leftFG, leftBG))
}
out.WriteString(l)
if hasRight {
r := string(rightRunes[rightIndex])
rightIndex++
if rightIndex >= len(rightRunes) {
rightIndex = 0
}
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)
bottom = s.styleBorder(bottom, bottomFG, bottomBG)
out.WriteRune('\n')
out.WriteString(bottom)
}
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 := leftWidth + rightWidth; i < width+rightWidth; {
out.WriteRune(runes[j])
j++
if j >= len(runes) {
j = 0
}
i += ansi.StringWidth(string(runes[j]))
}
out.WriteString(right)
return out.String()
}
// Apply foreground and background styling to a border.
func (s Style) styleBorder(border string, fg, bg TerminalColor) string {
if fg == noColor && bg == noColor {
return border
}
style := termenv.Style{}
if fg != noColor {
style = style.Foreground(fg.color(s.r))
}
if bg != noColor {
style = style.Background(bg.color(s.r))
}
return style.Styled(border)
}
func maxRuneWidth(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
}
func getFirstRuneAsString(str string) string {
if str == "" {
return str
}
r := []rune(str)
return string(r[0])
}
charmbracelet-lipgloss-500b193/borders_test.go 0000664 0000000 0000000 00000004411 14764354362 0021451 0 ustar 00root root 0000000 0000000 package lipgloss
import "testing"
func TestStyle_GetBorderSizes(t *testing.T) {
tests := []struct {
name string
style Style
wantX int
wantY int
}{
{
name: "Default style",
style: NewStyle(),
wantX: 0,
wantY: 0,
},
{
name: "Border(NormalBorder())",
style: NewStyle().Border(NormalBorder()),
wantX: 2,
wantY: 2,
},
{
name: "Border(NormalBorder(), true)",
style: NewStyle().Border(NormalBorder(), true),
wantX: 2,
wantY: 2,
},
{
name: "Border(NormalBorder(), true, false)",
style: NewStyle().Border(NormalBorder(), true, false),
wantX: 0,
wantY: 2,
},
{
name: "Border(NormalBorder(), true, true, false)",
style: NewStyle().Border(NormalBorder(), true, true, false),
wantX: 2,
wantY: 1,
},
{
name: "Border(NormalBorder(), true, true, false, false)",
style: NewStyle().Border(NormalBorder(), true, true, false, false),
wantX: 1,
wantY: 1,
},
{
name: "BorderTop(true).BorderStyle(NormalBorder())",
style: NewStyle().BorderTop(true).BorderStyle(NormalBorder()),
wantX: 0,
wantY: 1,
},
{
name: "BorderStyle(NormalBorder())",
style: NewStyle().BorderStyle(NormalBorder()),
wantX: 2,
wantY: 2,
},
{
name: "Custom BorderStyle",
style: NewStyle().BorderStyle(Border{Left: "123456789"}),
wantX: 1, // left and right borders are laid out vertically, one rune per row
wantY: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotX := tt.style.GetHorizontalBorderSize()
if gotX != tt.wantX {
t.Errorf("Style.GetHorizontalBorderSize() got %d, want %d", gotX, tt.wantX)
}
gotY := tt.style.GetVerticalBorderSize()
if gotY != tt.wantY {
t.Errorf("Style.GetVerticalBorderSize() got %d, want %d", gotY, tt.wantY)
}
gotX = tt.style.GetHorizontalFrameSize()
if gotX != tt.wantX {
t.Errorf("Style.GetHorizontalFrameSize() got %d, want %d", gotX, tt.wantX)
}
gotY = tt.style.GetVerticalFrameSize()
if gotY != tt.wantY {
t.Errorf("Style.GetVerticalFrameSize() got %d, want %d", gotY, tt.wantY)
}
gotX, gotY = tt.style.GetFrameSize()
if gotX != tt.wantX || gotY != tt.wantY {
t.Errorf("Style.GetFrameSize() got (%d, %d), want (%d, %d)", gotX, gotY, tt.wantX, tt.wantY)
}
})
}
}
charmbracelet-lipgloss-500b193/color.go 0000664 0000000 0000000 00000011450 14764354362 0020071 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"strconv"
"github.com/muesli/termenv"
)
// TerminalColor is a color intended to be rendered in the terminal.
type TerminalColor interface {
color(*Renderer) termenv.Color
RGBA() (r, g, b, a uint32)
}
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{}
func (NoColor) color(*Renderer) termenv.Color {
return termenv.NoColor{}
}
// 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.
//
// Deprecated.
func (n NoColor) RGBA() (r, g, b, a uint32) {
return 0x0, 0x0, 0x0, 0xFFFF //nolint:mnd
}
// Color specifies a color by hex or ANSI value. For example:
//
// ansiColor := lipgloss.Color("21")
// hexColor := lipgloss.Color("#0000ff")
type Color string
func (c Color) color(r *Renderer) termenv.Color {
return r.ColorProfile().Color(string(c))
}
// RGBA returns the RGBA value of this color. This satisfies the Go Color
// interface. Note that on error we return black with 100% opacity, or:
//
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
//
// Deprecated.
func (c Color) RGBA() (r, g, b, a uint32) {
return termenv.ConvertToRGB(c.color(renderer)).RGBA()
}
// ANSIColor is a color specified by an ANSI color value. It's merely syntactic
// sugar for the more general Color function. Invalid colors will render as
// black.
//
// Example usage:
//
// // These two statements are equivalent.
// colorA := lipgloss.ANSIColor(21)
// colorB := lipgloss.Color("21")
type ANSIColor uint
func (ac ANSIColor) color(r *Renderer) termenv.Color {
return Color(strconv.FormatUint(uint64(ac), 10)).color(r)
}
// RGBA returns the RGBA value of this color. This satisfies the Go Color
// interface. Note that on error we return black with 100% opacity, or:
//
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
//
// Deprecated.
func (ac ANSIColor) RGBA() (r, g, b, a uint32) {
cf := Color(strconv.FormatUint(uint64(ac), 10))
return cf.RGBA()
}
// 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 string
Dark string
}
func (ac AdaptiveColor) color(r *Renderer) termenv.Color {
if r.HasDarkBackground() {
return Color(ac.Dark).color(r)
}
return Color(ac.Light).color(r)
}
// RGBA returns the RGBA value of this color. This satisfies the Go Color
// interface. Note that on error we return black with 100% opacity, or:
//
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
//
// Deprecated.
func (ac AdaptiveColor) RGBA() (r, g, b, a uint32) {
return termenv.ConvertToRGB(ac.color(renderer)).RGBA()
}
// CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color
// profiles. Automatic color degradation will not be performed.
type CompleteColor struct {
TrueColor string
ANSI256 string
ANSI string
}
func (c CompleteColor) color(r *Renderer) termenv.Color {
p := r.ColorProfile()
switch p { //nolint:exhaustive
case termenv.TrueColor:
return p.Color(c.TrueColor)
case termenv.ANSI256:
return p.Color(c.ANSI256)
case termenv.ANSI:
return p.Color(c.ANSI)
default:
return termenv.NoColor{}
}
}
// RGBA returns the RGBA value of this color. This satisfies the Go Color
// interface. Note that on error we return black with 100% opacity, or:
//
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
// CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color
//
// Deprecated.
func (c CompleteColor) RGBA() (r, g, b, a uint32) {
return termenv.ConvertToRGB(c.color(renderer)).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
}
func (cac CompleteAdaptiveColor) color(r *Renderer) termenv.Color {
if r.HasDarkBackground() {
return cac.Dark.color(r)
}
return cac.Light.color(r)
}
// RGBA returns the RGBA value of this color. This satisfies the Go Color
// interface. Note that on error we return black with 100% opacity, or:
//
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
//
// Deprecated.
func (cac CompleteAdaptiveColor) RGBA() (r, g, b, a uint32) {
return termenv.ConvertToRGB(cac.color(renderer)).RGBA()
}
charmbracelet-lipgloss-500b193/color_test.go 0000664 0000000 0000000 00000012541 14764354362 0021132 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"image/color"
"testing"
"github.com/muesli/termenv"
)
func TestSetColorProfile(t *testing.T) {
r := renderer
input := "hello"
tt := []struct {
name string
profile termenv.Profile
expected string
}{
{
"ascii",
termenv.Ascii,
"hello",
},
{
"ansi",
termenv.ANSI,
"\x1b[94mhello\x1b[0m",
},
{
"ansi256",
termenv.ANSI256,
"\x1b[38;5;62mhello\x1b[0m",
},
{
"truecolor",
termenv.TrueColor,
"\x1b[38;2;89;86;224mhello\x1b[0m",
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
r.SetColorProfile(tc.profile)
style := NewStyle().Foreground(Color("#5A56E0"))
res := style.Render(input)
if res != tc.expected {
t.Errorf("Expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
tc.expected, formatEscapes(tc.expected),
res, formatEscapes(res))
}
})
}
}
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 {
h := hexToColor(tc.input)
o := uint(h.R)<<16 + uint(h.G)<<8 + uint(h.B)
if o != tc.expected {
t.Errorf("expected %X, got %X (test #%d)", tc.expected, o, i+1)
}
}
}
func TestRGBA(t *testing.T) {
tt := []struct {
profile termenv.Profile
darkBg bool
input TerminalColor
expected uint
}{
// lipgloss.Color
{
termenv.TrueColor,
true,
Color("#FF0000"),
0xFF0000,
},
{
termenv.TrueColor,
true,
Color("9"),
0xFF0000,
},
{
termenv.TrueColor,
true,
Color("21"),
0x0000FF,
},
// lipgloss.AdaptiveColor
{
termenv.TrueColor,
true,
AdaptiveColor{Light: "#0000FF", Dark: "#FF0000"},
0xFF0000,
},
{
termenv.TrueColor,
false,
AdaptiveColor{Light: "#0000FF", Dark: "#FF0000"},
0x0000FF,
},
{
termenv.TrueColor,
true,
AdaptiveColor{Light: "21", Dark: "9"},
0xFF0000,
},
{
termenv.TrueColor,
false,
AdaptiveColor{Light: "21", Dark: "9"},
0x0000FF,
},
// lipgloss.CompleteColor
{
termenv.TrueColor,
true,
CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
0xFF0000,
},
{
termenv.ANSI256,
true,
CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
0xFFFFFF,
},
{
termenv.ANSI,
true,
CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
0x0000FF,
},
{
termenv.TrueColor,
true,
CompleteColor{TrueColor: "", ANSI256: "231", ANSI: "12"},
0x000000,
},
// lipgloss.CompleteAdaptiveColor
// dark
{
termenv.TrueColor,
true,
CompleteAdaptiveColor{
Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"},
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
},
0xFF0000,
},
{
termenv.ANSI256,
true,
CompleteAdaptiveColor{
Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"},
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
},
0xFFFFFF,
},
{
termenv.ANSI,
true,
CompleteAdaptiveColor{
Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"},
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
},
0x0000FF,
},
// light
{
termenv.TrueColor,
false,
CompleteAdaptiveColor{
Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"},
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
},
0x0000FF,
},
{
termenv.ANSI256,
false,
CompleteAdaptiveColor{
Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"},
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
},
0x0000FF,
},
{
termenv.ANSI,
false,
CompleteAdaptiveColor{
Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"},
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
},
0xFF0000,
},
}
r := DefaultRenderer()
for i, tc := range tt {
r.SetColorProfile(tc.profile)
r.SetHasDarkBackground(tc.darkBg)
r, g, b, _ := 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)
}
}
}
// hexToColor translates a hex color string (#RRGGBB or #RGB) into a color.RGB,
// which satisfies the color.Color interface. If an invalid string is passed
// black with 100% opacity will be returned: or, in hex format, 0x000000FF.
func hexToColor(hex string) (c color.RGBA) {
c.A = 0xFF
if hex == "" || hex[0] != '#' {
return c
}
const (
fullFormat = 7 // #RRGGBB
shortFormat = 4 // #RGB
)
switch len(hex) {
case fullFormat:
const offset = 4
c.R = hexToByte(hex[1])<= '0' && b <= '9':
return b - '0'
case b >= 'a' && b <= 'f':
return b - 'a' + offset
case b >= 'A' && b <= 'F':
return b - 'A' + offset
}
// Invalid, but just return 0.
return 0
}
charmbracelet-lipgloss-500b193/examples/ 0000775 0000000 0000000 00000000000 14764354362 0020241 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/go.mod 0000664 0000000 0000000 00000003537 14764354362 0021357 0 ustar 00root root 0000000 0000000 module examples
go 1.19
replace github.com/charmbracelet/lipgloss => ../
replace github.com/charmbracelet/lipgloss/list => ../list
require (
github.com/charmbracelet/lipgloss v0.11.0
github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917
github.com/charmbracelet/wish v1.4.0
github.com/creack/pty v1.1.21
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/muesli/gamut v0.3.1
github.com/muesli/termenv v0.15.2
golang.org/x/term v0.27.0
)
require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbletea v0.25.0 // indirect
github.com/charmbracelet/keygen v0.5.0 // indirect
github.com/charmbracelet/log v0.4.0 // indirect
github.com/charmbracelet/x/ansi v0.6.0 // indirect
github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 // indirect
github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 // indirect
github.com/muesli/kmeans v0.3.1 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
)
charmbracelet-lipgloss-500b193/examples/go.sum 0000664 0000000 0000000 00000016456 14764354362 0021410 0 ustar 00root root 0000000 0000000 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-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/keygen v0.5.0 h1:XY0fsoYiCSM9axkrU+2ziE6u6YjJulo/b9Dghnw6MZc=
github.com/charmbracelet/keygen v0.5.0/go.mod h1:DfvCgLHxZ9rJxdK0DGw3C/LkV4SgdGbnliHcObV3L+8=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917 h1:NZKjJ7d/pzk/AfcJYEzmF8M48JlIrrY00RR5JdDc3io=
github.com/charmbracelet/ssh v0.0.0-20240401141849-854cddfa2917/go.mod h1:8/Ve8iGRRIGFM1kepYfRF2pEOF5Y3TEZYoJaA54228U=
github.com/charmbracelet/wish v1.4.0 h1:pL1uVP/YuYgJheHEj98teZ/n6pMYnmlZq/fcHvomrfc=
github.com/charmbracelet/wish v1.4.0/go.mod h1:ew4/MjJVfW/akEO9KmrQHQv1F7bQRGscRMrA+KtovTk=
github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA=
github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 h1:3RXpZWGWTOeVXCTv0Dnzxdv/MhNUkBfEcbaTY0zrTQI=
github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd h1:HqBjkSFXXfW4IgX3TMKipWoPEN08T3Pi4SA/3DLss/U=
github.com/charmbracelet/x/exp/term v0.0.0-20240328150354-ab9afc214dfd/go.mod h1:6GZ13FjIP6eOCqWU4lqgveGnYxQo9c3qBzHPeFu4HBE=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
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/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/clusters v0.0.0-20180605185049-a07a36e67d36/go.mod h1:mw5KDqUj0eLj/6DUNINLVJNoPTFkEuGMHtJsXLviLkY=
github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 h1:p4A2Jx7Lm3NV98VRMKlyWd3nqf8obft8NfXlAUmqd3I=
github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762/go.mod h1:mw5KDqUj0eLj/6DUNINLVJNoPTFkEuGMHtJsXLviLkY=
github.com/muesli/gamut v0.3.1 h1:8hozovcrDBWLLAwuOXC+UDyO0/uNroIdXAmY/lQOMHo=
github.com/muesli/gamut v0.3.1/go.mod h1:BED0DN21PXU1YaYNwaTmX9700SRHPcWWd6Llj0zsz5k=
github.com/muesli/kmeans v0.3.1 h1:KshLQ8wAETfLWOJKMuDCVYHnafddSa1kwGh/IypGIzY=
github.com/muesli/kmeans v0.3.1/go.mod h1:8/OvJW7cHc1BpRf8URb43m+vR105DDe+Kj1WcFXYDqc=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
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/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
charmbracelet-lipgloss-500b193/examples/layout/ 0000775 0000000 0000000 00000000000 14764354362 0021556 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/layout/main.go 0000664 0000000 0000000 00000023317 14764354362 0023037 0 ustar 00root root 0000000 0000000 package main
// This example demonstrates various Lip Gloss style and layout features.
import (
"fmt"
"image/color"
"os"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/lucasb-eyer/go-colorful"
"github.com/muesli/gamut"
"golang.org/x/term"
)
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
columnWidth = 30
)
// Style definitions.
var (
// General.
normal = lipgloss.Color("#EEEEEE")
subtle = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"}
highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"}
special = lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}
blends = gamut.Blends(lipgloss.Color("#F25D94"), lipgloss.Color("#EDFF82"), 50)
base = lipgloss.NewStyle().Foreground(normal)
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 = base.MarginTop(1)
infoStyle = base.
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(2).
Height(8).
Width(columnWidth + 1)
listHeader = base.
BorderStyle(lipgloss.NormalBorder()).
BorderBottom(true).
BorderForeground(subtle).
MarginRight(2).
Render
listItem = base.PaddingLeft(2).Render
checkMark = lipgloss.NewStyle().SetString("✓").
Foreground(special).
PaddingRight(1).
String()
listDone = func(s string) string {
return checkMark + lipgloss.NewStyle().
Strikethrough(true).
Foreground(lipgloss.AdaptiveColor{Light: "#969B86", Dark: "#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(lipgloss.AdaptiveColor{Light: "#343433", Dark: "#C1C6B2"}).
Background(lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#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"))
// Page.
docStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2)
)
func main() {
physicalWidth, _, _ := term.GetSize(int(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
c := lipgloss.Color(v[0])
fmt.Fprint(&title, titleStyle.MarginLeft(i*offset).Background(c))
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")
question := lipgloss.NewStyle().Width(50).Align(lipgloss.Center).Render(rainbow(lipgloss.NewStyle(), "Are you sure you want to eat marmalade?", blends))
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.WithWhitespaceForeground(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(lipgloss.Color(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.Width(columnWidth).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, 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
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("Ravishing")
bar := lipgloss.JoinHorizontal(lipgloss.Top,
statusKey,
statusVal,
encoding,
fishCake,
)
doc.WriteString(statusBarStyle.Width(width).Render(bar))
}
if physicalWidth > 0 {
docStyle = docStyle.MaxWidth(physicalWidth)
}
// Okay, let's print it
fmt.Println(docStyle.Render(doc.String()))
}
func colorGrid(xSteps, ySteps int) [][]string {
x0y0, _ := colorful.Hex("#F25D94")
x1y0, _ := colorful.Hex("#EDFF82")
x0y1, _ := colorful.Hex("#643AFF")
x1y1, _ := colorful.Hex("#14F9D5")
x0 := make([]colorful.Color, ySteps)
for i := range x0 {
x0[i] = x0y0.BlendLuv(x0y1, float64(i)/float64(ySteps))
}
x1 := make([]colorful.Color, ySteps)
for i := range x1 {
x1[i] = x1y0.BlendLuv(x1y1, float64(i)/float64(ySteps))
}
grid := make([][]string, ySteps)
for x := 0; x < ySteps; x++ {
y0 := x0[x]
grid[x] = make([]string, xSteps)
for y := 0; y < xSteps; y++ {
grid[x][y] = y0.BlendLuv(x1[x], float64(y)/float64(xSteps)).Hex()
}
}
return grid
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func rainbow(base lipgloss.Style, s string, colors []color.Color) string {
var str string
for i, ss := range s {
color, _ := colorful.MakeColor(colors[i%len(colors)])
str = str + base.Foreground(lipgloss.Color(color.Hex())).Render(string(ss))
}
return str
}
charmbracelet-lipgloss-500b193/examples/list/ 0000775 0000000 0000000 00000000000 14764354362 0021214 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/list/duckduckgoose/ 0000775 0000000 0000000 00000000000 14764354362 0024046 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/list/duckduckgoose/main.go 0000664 0000000 0000000 00000001114 14764354362 0025316 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/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)
fmt.Println(l)
}
charmbracelet-lipgloss-500b193/examples/list/glow/ 0000775 0000000 0000000 00000000000 14764354362 0022164 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/list/glow/main.go 0000664 0000000 0000000 00000002301 14764354362 0023433 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/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())
}
fmt.Println()
fmt.Println(l)
}
charmbracelet-lipgloss-500b193/examples/list/grocery/ 0000775 0000000 0000000 00000000000 14764354362 0022666 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/list/grocery/main.go 0000664 0000000 0000000 00000002720 14764354362 0024142 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/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)
fmt.Println(l)
}
charmbracelet-lipgloss-500b193/examples/list/roman/ 0000775 0000000 0000000 00000000000 14764354362 0022330 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/list/roman/main.go 0000664 0000000 0000000 00000000752 14764354362 0023607 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/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)
fmt.Println(l)
}
charmbracelet-lipgloss-500b193/examples/list/simple/ 0000775 0000000 0000000 00000000000 14764354362 0022505 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/list/simple/main.go 0000664 0000000 0000000 00000000333 14764354362 0023757 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss/list"
)
func main() {
l := list.New(
"A",
"B",
"C",
list.New(
"D",
"E",
"F",
).Enumerator(list.Roman),
"G",
)
fmt.Println(l)
}
charmbracelet-lipgloss-500b193/examples/list/sublist/ 0000775 0000000 0000000 00000000000 14764354362 0022701 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/list/sublist/main.go 0000664 0000000 0000000 00000015612 14764354362 0024161 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/list"
"github.com/charmbracelet/lipgloss/table"
"github.com/lucasb-eyer/go-colorful"
)
func main() {
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 := lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#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(lipgloss.AdaptiveColor{Light: "#969B86", Dark: "#696969"})
default:
return lipgloss.NewStyle()
}
}
colors := colorGrid(1, 5)
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(lipgloss.Color(colors[index][0]))
}
return titleStyle.Padding(0, 5-index, 0, index+2).MaxWidth(20).Background(lipgloss.Color(colors[index][0]))
}
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(lipgloss.Color(colors[4][0])).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(lipgloss.Color(colors[3][0])).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_™")
fmt.Println(l)
}
func colorGrid(xSteps, ySteps int) [][]string {
x0y0, _ := colorful.Hex("#F25D94")
x1y0, _ := colorful.Hex("#EDFF82")
x0y1, _ := colorful.Hex("#643AFF")
x1y1, _ := colorful.Hex("#14F9D5")
x0 := make([]colorful.Color, ySteps)
for i := range x0 {
x0[i] = x0y0.BlendLuv(x0y1, float64(i)/float64(ySteps))
}
x1 := make([]colorful.Color, ySteps)
for i := range x1 {
x1[i] = x1y0.BlendLuv(x1y1, float64(i)/float64(ySteps))
}
grid := make([][]string, ySteps)
for x := 0; x < ySteps; x++ {
y0 := x0[x]
grid[x] = make([]string, xSteps)
for y := 0; y < xSteps; y++ {
grid[x][y] = y0.BlendLuv(x1[x], float64(y)/float64(xSteps)).Hex()
}
}
return grid
}
charmbracelet-lipgloss-500b193/examples/ssh/ 0000775 0000000 0000000 00000000000 14764354362 0021036 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/ssh/main.go 0000664 0000000 0000000 00000012162 14764354362 0022313 0 ustar 00root root 0000000 0000000 package main
// This example demonstrates how to use a custom Lip Gloss renderer with Wish,
// a package for building custom SSH servers.
//
// The big advantage to using custom renderers here is that we can accurately
// detect the background color and color profile for each client and render
// against that accordingly.
//
// For details on wish see: https://github.com/charmbracelet/wish/
import (
"fmt"
"log"
"os"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/ssh"
"github.com/charmbracelet/wish"
lm "github.com/charmbracelet/wish/logging"
"github.com/creack/pty"
"github.com/muesli/termenv"
)
// 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 against a given renderer.
func makeStyles(r *lipgloss.Renderer) styles {
return styles{
bold: r.NewStyle().SetString("bold").Bold(true),
faint: r.NewStyle().SetString("faint").Faint(true),
italic: r.NewStyle().SetString("italic").Italic(true),
underline: r.NewStyle().SetString("underline").Underline(true),
strikethrough: r.NewStyle().SetString("strikethrough").Strikethrough(true),
red: r.NewStyle().SetString("red").Foreground(lipgloss.Color("#E88388")),
green: r.NewStyle().SetString("green").Foreground(lipgloss.Color("#A8CC8C")),
yellow: r.NewStyle().SetString("yellow").Foreground(lipgloss.Color("#DBAB79")),
blue: r.NewStyle().SetString("blue").Foreground(lipgloss.Color("#71BEF2")),
magenta: r.NewStyle().SetString("magenta").Foreground(lipgloss.Color("#D290E4")),
cyan: r.NewStyle().SetString("cyan").Foreground(lipgloss.Color("#66C2CD")),
gray: r.NewStyle().SetString("gray").Foreground(lipgloss.Color("#B9BFCA")),
}
}
// Bridge Wish and Termenv so we can query for a user's terminal capabilities.
type sshOutput struct {
ssh.Session
tty *os.File
}
func (s *sshOutput) Write(p []byte) (int, error) {
return s.Session.Write(p)
}
func (s *sshOutput) Read(p []byte) (int, error) {
return s.Session.Read(p)
}
func (s *sshOutput) Fd() uintptr {
return s.tty.Fd()
}
type sshEnviron struct {
environ []string
}
func (s *sshEnviron) Getenv(key string) string {
for _, v := range s.environ {
if strings.HasPrefix(v, key+"=") {
return v[len(key)+1:]
}
}
return ""
}
func (s *sshEnviron) Environ() []string {
return s.environ
}
// Create a termenv.Output from the session.
func outputFromSession(sess ssh.Session) *termenv.Output {
sshPty, _, _ := sess.Pty()
_, tty, err := pty.Open()
if err != nil {
log.Fatal(err)
}
o := &sshOutput{
Session: sess,
tty: tty,
}
environ := sess.Environ()
environ = append(environ, fmt.Sprintf("TERM=%s", sshPty.Term))
e := &sshEnviron{environ: environ}
// We need to use unsafe mode here because the ssh session is not running
// locally and we already know that the session is a TTY.
return termenv.NewOutput(o, termenv.WithUnsafe(), termenv.WithEnvironment(e))
}
// Handle SSH requests.
func handler(next ssh.Handler) ssh.Handler {
return func(sess ssh.Session) {
// Get client's output.
clientOutput := outputFromSession(sess)
pty, _, active := sess.Pty()
if !active {
next(sess)
return
}
width := pty.Window.Width
// Initialize new renderer for the client.
renderer := lipgloss.NewRenderer(sess)
renderer.SetOutput(clientOutput)
// Initialize new styles against the renderer.
styles := makeStyles(renderer)
str := strings.Builder{}
fmt.Fprintf(&str, "\n\n%s %s %s %s %s",
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,
)
fmt.Fprintf(&str, "%s %t %s\n\n", styles.bold.UnsetString().Render("Has dark background?"),
renderer.HasDarkBackground(),
renderer.Output().BackgroundColor())
block := renderer.Place(width,
lipgloss.Height(str.String()), lipgloss.Center, lipgloss.Center, str.String(),
lipgloss.WithWhitespaceChars("/"),
lipgloss.WithWhitespaceForeground(lipgloss.AdaptiveColor{Light: "250", Dark: "236"}),
)
// Render to client.
wish.WriteString(sess, block)
next(sess)
}
}
func main() {
port := 3456
s, err := wish.NewServer(
wish.WithAddress(fmt.Sprintf(":%d", port)),
wish.WithHostKeyPath("ssh_example"),
wish.WithMiddleware(handler, lm.Middleware()),
)
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)
}
}
charmbracelet-lipgloss-500b193/examples/table/ 0000775 0000000 0000000 00000000000 14764354362 0021330 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/table/ansi/ 0000775 0000000 0000000 00000000000 14764354362 0022262 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/table/ansi/main.go 0000664 0000000 0000000 00000000540 14764354362 0023534 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/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"))
fmt.Println(t.Render())
}
charmbracelet-lipgloss-500b193/examples/table/chess/ 0000775 0000000 0000000 00000000000 14764354362 0022435 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/table/chess/main.go 0000664 0000000 0000000 00000002324 14764354362 0023711 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
func main() {
re := lipgloss.NewRenderer(os.Stdout)
labelStyle := re.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 "))
fmt.Println(lipgloss.JoinVertical(lipgloss.Right, lipgloss.JoinHorizontal(lipgloss.Center, files, t.Render()), ranks) + "\n")
}
charmbracelet-lipgloss-500b193/examples/table/demo.tape 0000664 0000000 0000000 00000000413 14764354362 0023125 0 ustar 00root root 0000000 0000000 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
charmbracelet-lipgloss-500b193/examples/table/languages/ 0000775 0000000 0000000 00000000000 14764354362 0023276 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/table/languages/main.go 0000664 0000000 0000000 00000003612 14764354362 0024553 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"os"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
const (
purple = lipgloss.Color("99")
gray = lipgloss.Color("245")
lightGray = lipgloss.Color("241")
)
func main() {
re := lipgloss.NewRenderer(os.Stdout)
var (
// HeaderStyle is the lipgloss style used for the table headers.
HeaderStyle = re.NewStyle().Foreground(purple).Bold(true).Align(lipgloss.Center)
// CellStyle is the base lipgloss style used for the table rows.
CellStyle = re.NewStyle().Padding(0, 1).Width(14)
// OddRowStyle is the lipgloss style used for odd-numbered table rows.
OddRowStyle = CellStyle.Foreground(gray)
// EvenRowStyle is the lipgloss style used for even-numbered table rows.
EvenRowStyle = CellStyle.Foreground(lightGray)
// BorderStyle is the lipgloss style used for the table border.
BorderStyle = lipgloss.NewStyle().Foreground(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?")
fmt.Println(t)
}
charmbracelet-lipgloss-500b193/examples/table/mindy/ 0000775 0000000 0000000 00000000000 14764354362 0022450 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/table/mindy/main.go 0000664 0000000 0000000 00000002505 14764354362 0023725 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"os"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
func main() {
re := lipgloss.NewRenderer(os.Stdout)
labelStyle := re.NewStyle().Width(3).Align(lipgloss.Right)
swatchStyle := re.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 {
case col%2 == 0:
return labelStyle.Foreground(color)
default:
return swatchStyle.Background(color)
}
})
fmt.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)
}
charmbracelet-lipgloss-500b193/examples/table/pokemon/ 0000775 0000000 0000000 00000000000 14764354362 0023000 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/table/pokemon/main.go 0000664 0000000 0000000 00000007711 14764354362 0024261 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
func main() {
re := lipgloss.NewRenderer(os.Stdout)
baseStyle := re.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]lipgloss.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]lipgloss.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(re.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"))
})
fmt.Println(t)
}
charmbracelet-lipgloss-500b193/examples/tree/ 0000775 0000000 0000000 00000000000 14764354362 0021200 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/tree/background/ 0000775 0000000 0000000 00000000000 14764354362 0023317 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/tree/background/main.go 0000664 0000000 0000000 00000001403 14764354362 0024570 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/tree"
)
func main() {
enumeratorStyle := 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(enumeratorStyle).
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"),
)
fmt.Println(t)
}
charmbracelet-lipgloss-500b193/examples/tree/files/ 0000775 0000000 0000000 00000000000 14764354362 0022302 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/tree/files/main.go 0000664 0000000 0000000 00000002630 14764354362 0023556 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/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).
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)
}
fmt.Println(t)
}
charmbracelet-lipgloss-500b193/examples/tree/makeup/ 0000775 0000000 0000000 00000000000 14764354362 0022462 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/tree/makeup/main.go 0000664 0000000 0000000 00000001307 14764354362 0023736 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/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).
RootStyle(rootStyle).
ItemStyle(itemStyle)
fmt.Println(t)
}
charmbracelet-lipgloss-500b193/examples/tree/rounded/ 0000775 0000000 0000000 00000000000 14764354362 0022640 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/tree/rounded/main.go 0000664 0000000 0000000 00000001271 14764354362 0024114 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/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)
fmt.Println(t)
}
charmbracelet-lipgloss-500b193/examples/tree/simple/ 0000775 0000000 0000000 00000000000 14764354362 0022471 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/tree/simple/main.go 0000664 0000000 0000000 00000000552 14764354362 0023746 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss/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"),
)
fmt.Println(t)
}
charmbracelet-lipgloss-500b193/examples/tree/styles/ 0000775 0000000 0000000 00000000000 14764354362 0022523 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/tree/styles/main.go 0000664 0000000 0000000 00000000775 14764354362 0024007 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/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),
"Mac",
"Milk",
).
EnumeratorStyle(purple)
fmt.Println(t)
}
charmbracelet-lipgloss-500b193/examples/tree/toggle/ 0000775 0000000 0000000 00000000000 14764354362 0022461 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/examples/tree/toggle/main.go 0000664 0000000 0000000 00000003062 14764354362 0023735 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/tree"
)
type styles struct {
base,
block,
enumerator,
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.enumerator = 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).
EnumeratorStyle(s.enumerator).
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},
)
fmt.Println(s.block.Render(t.String()))
}
charmbracelet-lipgloss-500b193/get.go 0000664 0000000 0000000 00000040127 14764354362 0017535 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"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.getAsBool(underlineKey, false)
}
// 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() TerminalColor {
return s.getAsColor(foregroundKey)
}
// GetBackground returns the style's background color. If no value is set
// NoColor{} is returned.
func (s Style) GetBackground() TerminalColor {
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)
}
// 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)
}
// 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() TerminalColor {
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() TerminalColor {
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() TerminalColor {
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() TerminalColor {
return s.getAsColor(borderLeftForegroundKey)
}
// GetBorderTopBackground returns the style's border top background color. If
// no value is set NoColor{} is returned.
func (s Style) GetBorderTopBackground() TerminalColor {
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() TerminalColor {
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() TerminalColor {
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() TerminalColor {
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.getAsBool(borderTopKey, false) && !s.implicitBorders() {
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.getAsBool(borderLeftKey, false) && !s.implicitBorders() {
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.getAsBool(borderBottomKey, false) && !s.implicitBorders() {
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.getAsBool(borderRightKey, false) && !s.implicitBorders() {
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)
}
// Returns whether or not the given property is set.
func (s Style) isSet(k propKey) bool {
return s.props.has(k)
}
func (s Style) getAsBool(k propKey, defaultVal bool) bool {
if !s.isSet(k) {
return defaultVal
}
return s.attrs&int(k) != 0
}
func (s Style) getAsColor(k propKey) TerminalColor {
if !s.isSet(k) {
return noColor
}
var c TerminalColor
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
}
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 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
}
// Returns whether or not the style has implicit borders. This happens when
// a border style has been set but no border sides have been explicitly turned
// on or off.
func (s Style) implicitBorders() bool {
var (
borderStyle = s.getBorderStyle()
topSet = s.isSet(borderTopKey)
rightSet = s.isSet(borderRightKey)
bottomSet = s.isSet(borderBottomKey)
leftSet = s.isSet(borderLeftKey)
)
return borderStyle != noBorder && !(topSet || rightSet || bottomSet || leftSet)
}
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) {
lines = strings.Split(s, "\n")
for _, l := range lines {
w := ansi.StringWidth(l)
if widest < w {
widest = w
}
}
return lines, widest
}
charmbracelet-lipgloss-500b193/go.mod 0000664 0000000 0000000 00000001776 14764354362 0017544 0 ustar 00root root 0000000 0000000 module github.com/charmbracelet/lipgloss
retract v0.7.0 // v0.7.0 introduces a bug that causes some apps to freeze.
retract v0.11.1 // v0.11.1 uses a broken version of x/ansi StringWidth that causes some lines to wrap incorrectly.
go 1.18
require (
github.com/aymanbagabas/go-udiff v0.2.0
github.com/charmbracelet/x/ansi v0.8.0
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a
github.com/muesli/termenv v0.16.0
github.com/rivo/uniseg v0.4.7
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.30.0 // indirect
)
charmbracelet-lipgloss-500b193/go.sum 0000664 0000000 0000000 00000005662 14764354362 0017567 0 ustar 00root root 0000000 0000000 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
charmbracelet-lipgloss-500b193/join.go 0000664 0000000 0000000 00000010040 14764354362 0017704 0 ustar 00root root 0000000 0000000 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 { //nolint:exhaustive
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 { //nolint:exhaustive
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) {
b.WriteRune('\n')
}
}
}
return b.String()
}
charmbracelet-lipgloss-500b193/join_test.go 0000664 0000000 0000000 00000002113 14764354362 0020745 0 ustar 00root root 0000000 0000000 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)
}
})
}
}
charmbracelet-lipgloss-500b193/list/ 0000775 0000000 0000000 00000000000 14764354362 0017376 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/list/enumerator.go 0000664 0000000 0000000 00000005006 14764354362 0022107 0 ustar 00root root 0000000 0000000 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 "-"
}
charmbracelet-lipgloss-500b193/list/list.go 0000664 0000000 0000000 00000013625 14764354362 0020707 0 ustar 00root root 0000000 0000000 // 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
import (
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/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
}
// 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
}
charmbracelet-lipgloss-500b193/list/list_test.go 0000664 0000000 0000000 00000023471 14764354362 0021746 0 ustar 00root root 0000000 0000000 package list_test
import (
"strings"
"testing"
"unicode"
"github.com/aymanbagabas/go-udiff"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/list"
"github.com/charmbracelet/lipgloss/tree"
)
// 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")
expected := `
• Foo
• Bar
• Baz
`
assertEqual(t, expected, l.String())
}
func TestListItems(t *testing.T) {
l := list.New().
Items([]string{"Foo", "Bar", "Baz"})
expected := `
• Foo
• Bar
• Baz
`
assertEqual(t, expected, 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")
expected := `
• Foo
• Bar
I. Hi
II. Hello
III. Halo
• Qux
`
assertEqual(t, expected, l.String())
}
func TestSublistItems(t *testing.T) {
l := list.New(
"A",
"B",
"C",
list.New(
"D",
"E",
"F",
).Enumerator(list.Roman),
"G",
)
expected := `
• A
• B
• C
I. D
II. E
III. F
• G
`
assertEqual(t, expected, 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).
Enumerator(list.Alphabet).
Item("foo").
Item("Deeper").
Item(
list.New().
EnumeratorStyle(style1).
Enumerator(list.Arabic).
Item("a").
Item("b").
Item("Even Deeper, inherit parent renderer").
Item(
list.New().
Enumerator(list.Asterisk).
EnumeratorStyle(style2).
Item("sus").
Item("d minor").
Item("f#").
Item("One ore level, with another renderer").
Item(
list.New().
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().
EnumeratorStyle(style2).
Child("another\nmultine\nstring").
Child("something").
Child("a subtree").
Child(
tree.New().
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")
expected := `
• Foo
• Bar
• foo2
• bar2
• Qux
I. aaa
II. bbb
• Deep
A. foo
B. Deeper
1. a
2. b
3. Even Deeper, inherit parent renderer
* sus
* d minor
* f#
* One ore level, with another renderer
- a
multine
string
- hoccus poccus
- abra kadabra
- And finally, a tree within all this
├── another
│ multine
│ string
├── something
├── a subtree
│ ├── yup
│ ├── many itens
│ └── another
├── hallo
└── wunderbar!
- this is a tree
and other obvious statements
C. bar
• Baz
`
assertEqual(t, expected, l.String())
}
func TestMultiline(t *testing.T) {
l := list.New().
Item("Item1\nline 2\nline 3").
Item("Item2\nline 2\nline 3").
Item("3")
expected := `
• Item1
line 2
line 3
• Item2
line 2
line 3
• 3
`
assertEqual(t, expected, l.String())
}
func TestListIntegers(t *testing.T) {
l := list.New().
Item("1").
Item("2").
Item("3")
expected := `
• 1
• 2
• 3
`
assertEqual(t, expected, 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")
assertEqual(t, test.expected, 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")
assertEqual(t, test.expected, 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)
}
expected := strings.TrimPrefix(`
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`, "\n")
assertEqual(t, expected, l.String())
}
func TestSubListItems(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"),
)
expected := `
• S
• neovim
• vscode
• HI
• vim
• doom emacs
• Parent 2
• I like fuzzy socks
`
assertEqual(t, expected, 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")
}
charmbracelet-lipgloss-500b193/position.go 0000664 0000000 0000000 00000010452 14764354362 0020620 0 ustar 00root root 0000000 0000000 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 renderer.Place(width, height, hPos, vPos, str, opts...)
}
// Place places a string or text block vertically in an unstyled box of a given
// width or height.
func (r *Renderer) Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string {
return r.PlaceVertical(height, vPos, r.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 {
return renderer.PlaceHorizontal(width, pos, str, 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 noöp.
func (r *Renderer) PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string {
lines, contentWidth := getLines(str)
gap := width - contentWidth
if gap <= 0 {
return str
}
ws := newWhitespace(r, 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 { //nolint:exhaustive
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 {
return renderer.PlaceVertical(height, pos, str, opts...)
}
// 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 noöp.
func (r *Renderer) 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(r, opts...)
_, width := getLines(str)
emptyLine := ws.render(width)
b := strings.Builder{}
switch pos { //nolint:exhaustive
case Top:
b.WriteString(str)
b.WriteRune('\n')
for i := 0; i < gap; i++ {
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 i := 0; i < bottom; i++ {
b.WriteRune('\n')
b.WriteString(emptyLine)
}
}
return b.String()
}
charmbracelet-lipgloss-500b193/ranges.go 0000664 0000000 0000000 00000002225 14764354362 0020232 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"strings"
"github.com/charmbracelet/x/ansi"
)
// StyleRanges allows to, given a string, style ranges of it differently.
// The function will take into account existing styles.
// 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 that can be used with [StyleRanges].
func NewRange(start, end int, style Style) Range {
return Range{start, end, style}
}
// Range to be used with [StyleRanges].
type Range struct {
Start, End int
Style Style
}
charmbracelet-lipgloss-500b193/ranges_test.go 0000664 0000000 0000000 00000005215 14764354362 0021273 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"testing"
"github.com/muesli/termenv"
)
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[0m",
},
{
name: "multiple ranges",
input: "hello world",
ranges: []Range{
NewRange(0, 5, NewStyle().Bold(true)),
NewRange(6, 11, NewStyle().Italic(true)),
},
expected: "\x1b[1mhello\x1b[0m \x1b[3mworld\x1b[0m",
},
{
name: "overlapping with existing ANSI",
input: "hello \x1b[32mworld\x1b[0m",
ranges: []Range{
NewRange(0, 5, NewStyle().Bold(true)),
},
expected: "\x1b[1mhello\x1b[0m \x1b[32mworld\x1b[0m",
},
{
name: "style at start",
input: "hello world",
ranges: []Range{
NewRange(0, 5, NewStyle().Bold(true)),
},
expected: "\x1b[1mhello\x1b[0m world",
},
{
name: "style at end",
input: "hello world",
ranges: []Range{
NewRange(6, 11, NewStyle().Bold(true)),
},
expected: "hello \x1b[1mworld\x1b[0m",
},
{
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[0m beautiful \x1b[3mworld\x1b[0m",
},
{
name: "adjacent ranges",
input: "hello world",
ranges: []Range{
NewRange(0, 5, NewStyle().Bold(true)),
NewRange(6, 11, NewStyle().Italic(true)),
},
expected: "\x1b[1mhello\x1b[0m \x1b[3mworld\x1b[0m",
},
{
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[0m \x1b[3m你好\x1b[0m \x1b[1m世界\x1b[0m",
},
{
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[0m\x1b[90m\x1b[39m\x1b[3mnloads",
},
}
for _, tt := range tests {
renderer.SetColorProfile(termenv.ANSI)
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)
}
})
}
}
charmbracelet-lipgloss-500b193/renderer.go 0000664 0000000 0000000 00000012005 14764354362 0020556 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"io"
"sync"
"github.com/muesli/termenv"
)
// We're manually creating the struct here to avoid initializing the output and
// query the terminal multiple times.
var renderer = &Renderer{
output: termenv.DefaultOutput(),
}
// Renderer is a lipgloss terminal renderer.
type Renderer struct {
output *termenv.Output
colorProfile termenv.Profile
hasDarkBackground bool
getColorProfile sync.Once
explicitColorProfile bool
getBackgroundColor sync.Once
explicitBackgroundColor bool
mtx sync.RWMutex
}
// DefaultRenderer returns the default renderer.
func DefaultRenderer() *Renderer {
return renderer
}
// SetDefaultRenderer sets the default global renderer.
func SetDefaultRenderer(r *Renderer) {
renderer = r
}
// NewRenderer creates a new Renderer.
//
// w will be used to determine the terminal's color capabilities.
func NewRenderer(w io.Writer, opts ...termenv.OutputOption) *Renderer {
r := &Renderer{
output: termenv.NewOutput(w, opts...),
}
return r
}
// Output returns the termenv output.
func (r *Renderer) Output() *termenv.Output {
r.mtx.RLock()
defer r.mtx.RUnlock()
return r.output
}
// SetOutput sets the termenv output.
func (r *Renderer) SetOutput(o *termenv.Output) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.output = o
}
// ColorProfile returns the detected termenv color profile.
func (r *Renderer) ColorProfile() termenv.Profile {
r.mtx.RLock()
defer r.mtx.RUnlock()
if !r.explicitColorProfile {
r.getColorProfile.Do(func() {
// NOTE: we don't need to lock here because sync.Once provides its
// own locking mechanism.
r.colorProfile = r.output.EnvColorProfile()
})
}
return r.colorProfile
}
// ColorProfile returns the detected termenv color profile.
func ColorProfile() termenv.Profile {
return renderer.ColorProfile()
}
// SetColorProfile sets the color profile on the renderer. This function exists
// mostly for testing purposes so that you can assure you're testing against
// a specific profile.
//
// Outside of testing you likely won't want to use this function as the color
// profile will detect and cache the terminal's color capabilities and choose
// the best available profile.
//
// Available color profiles are:
//
// termenv.Ascii // no color, 1-bit
// termenv.ANSI //16 colors, 4-bit
// termenv.ANSI256 // 256 colors, 8-bit
// termenv.TrueColor // 16,777,216 colors, 24-bit
//
// This function is thread-safe.
func (r *Renderer) SetColorProfile(p termenv.Profile) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.colorProfile = p
r.explicitColorProfile = true
}
// SetColorProfile sets the color profile on the default renderer. This
// function exists mostly for testing purposes so that you can assure you're
// testing against a specific profile.
//
// Outside of testing you likely won't want to use this function as the color
// profile will detect and cache the terminal's color capabilities and choose
// the best available profile.
//
// Available color profiles are:
//
// termenv.Ascii // no color, 1-bit
// termenv.ANSI //16 colors, 4-bit
// termenv.ANSI256 // 256 colors, 8-bit
// termenv.TrueColor // 16,777,216 colors, 24-bit
//
// This function is thread-safe.
func SetColorProfile(p termenv.Profile) {
renderer.SetColorProfile(p)
}
// HasDarkBackground returns whether or not the terminal has a dark background.
func HasDarkBackground() bool {
return renderer.HasDarkBackground()
}
// HasDarkBackground returns whether or not the renderer will render to a dark
// background. A dark background can either be auto-detected, or set explicitly
// on the renderer.
func (r *Renderer) HasDarkBackground() bool {
r.mtx.RLock()
defer r.mtx.RUnlock()
if !r.explicitBackgroundColor {
r.getBackgroundColor.Do(func() {
// NOTE: we don't need to lock here because sync.Once provides its
// own locking mechanism.
r.hasDarkBackground = r.output.HasDarkBackground()
})
}
return r.hasDarkBackground
}
// SetHasDarkBackground sets the background color detection value for the
// default renderer. This function exists mostly for testing purposes so that
// you can assure you're testing against a specific background color setting.
//
// Outside of testing you likely won't want to use this function as the
// backgrounds value will be automatically detected and cached against the
// terminal's current background color setting.
//
// This function is thread-safe.
func SetHasDarkBackground(b bool) {
renderer.SetHasDarkBackground(b)
}
// SetHasDarkBackground sets the background color detection value on the
// renderer. This function exists mostly for testing purposes so that you can
// assure you're testing against a specific background color setting.
//
// Outside of testing you likely won't want to use this function as the
// backgrounds value will be automatically detected and cached against the
// terminal's current background color setting.
//
// This function is thread-safe.
func (r *Renderer) SetHasDarkBackground(b bool) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.hasDarkBackground = b
r.explicitBackgroundColor = true
}
charmbracelet-lipgloss-500b193/renderer_test.go 0000664 0000000 0000000 00000002100 14764354362 0021610 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"io"
"os"
"testing"
"github.com/muesli/termenv"
)
func TestRendererHasDarkBackground(t *testing.T) {
r1 := NewRenderer(os.Stdout)
r1.SetHasDarkBackground(false)
if r1.HasDarkBackground() {
t.Error("Expected renderer to have light background")
}
r2 := NewRenderer(os.Stdout)
r2.SetHasDarkBackground(true)
if !r2.HasDarkBackground() {
t.Error("Expected renderer to have dark background")
}
}
func TestRendererWithOutput(t *testing.T) {
f, err := os.Create(t.Name())
if err != nil {
t.Fatal(err)
}
defer f.Close()
defer os.Remove(f.Name())
r := NewRenderer(f)
r.SetColorProfile(termenv.TrueColor)
if r.ColorProfile() != termenv.TrueColor {
t.Error("Expected renderer to use true color")
}
}
func TestRace(t *testing.T) {
r := NewRenderer(io.Discard)
o := r.Output()
for i := 0; i < 100; i++ {
t.Run("SetColorProfile", func(t *testing.T) {
t.Parallel()
r.SetHasDarkBackground(false)
r.HasDarkBackground()
r.SetOutput(o)
r.SetColorProfile(termenv.ANSI256)
r.SetHasDarkBackground(true)
r.Output()
})
}
}
charmbracelet-lipgloss-500b193/runes.go 0000664 0000000 0000000 00000001573 14764354362 0020114 0 ustar 00root root 0000000 0000000 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()
}
charmbracelet-lipgloss-500b193/runes_test.go 0000664 0000000 0000000 00000002240 14764354362 0021143 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"strings"
"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[0mello",
},
{
"你好 1",
"你好",
[]int{1},
"你\x1b[7m好\x1b[0m",
},
{
"hello 你好 6,7",
"hello 你好",
[]int{6, 7},
"hello \x1b[7m你好\x1b[0m",
},
{
"hello 1,3",
"hello",
[]int{1, 3},
"h\x1b[7me\x1b[0ml\x1b[7ml\x1b[0mo",
},
{
"你好 0,1",
"你好",
[]int{0, 1},
"\x1b[7m你好\x1b[0m",
},
}
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`%s`\n`%s`\n\nActual Output:\n\n`%s`\n`%s`\n\n",
tc.expected, formatEscapes(tc.expected),
res, formatEscapes(res))
}
})
}
}
func formatEscapes(str string) string {
return strings.ReplaceAll(str, "\x1b", "\\x1b")
}
charmbracelet-lipgloss-500b193/set.go 0000664 0000000 0000000 00000054045 14764354362 0017555 0 ustar 00root root 0000000 0000000 package lipgloss
// Set a value on the underlying rules map.
func (s *Style) set(key propKey, value interface{}) {
// 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 { //nolint:exhaustive
case foregroundKey:
s.fgColor = colorOrNil(value)
case backgroundKey:
s.bgColor = colorOrNil(value)
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 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 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 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)
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 { //nolint:exhaustive
case foregroundKey:
s.set(foregroundKey, i.fgColor)
case backgroundKey:
s.set(backgroundKey, i.bgColor)
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 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 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 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 interface{}) TerminalColor {
if c, ok := c.(TerminalColor); 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
// UnderlineSpaces.
func (s Style) Underline(v bool) Style {
s.set(underlineKey, v)
return s
}
// Strikethrough sets a strikethrough rule. By default, strikes will not be
// drawn on whitespace like margins and padding. To change this behavior set
// StrikethroughSpaces.
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 TerminalColor) Style {
s.set(foregroundKey, c)
return s
}
// Background sets a background color.
func (s Style) Background(c TerminalColor) Style {
s.set(backgroundKey, c)
return s
}
// Width sets the width of the block before applying margins. The width, if
// set, also determines where text will wrap.
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
}
// 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 TerminalColor) Style {
s.set(marginBackgroundKey, c)
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 ...TerminalColor) 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 TerminalColor) Style {
s.set(borderTopForegroundKey, c)
return s
}
// BorderRightForeground sets the foreground color for the right side of the
// border.
func (s Style) BorderRightForeground(c TerminalColor) Style {
s.set(borderRightForegroundKey, c)
return s
}
// BorderBottomForeground sets the foreground color for the bottom of the
// border.
func (s Style) BorderBottomForeground(c TerminalColor) Style {
s.set(borderBottomForegroundKey, c)
return s
}
// BorderLeftForeground sets the foreground color for the left side of the
// border.
func (s Style) BorderLeftForeground(c TerminalColor) Style {
s.set(borderLeftForegroundKey, c)
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 ...TerminalColor) 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 TerminalColor) Style {
s.set(borderTopBackgroundKey, c)
return s
}
// BorderRightBackground sets the background color of right side the border.
func (s Style) BorderRightBackground(c TerminalColor) Style {
s.set(borderRightBackgroundKey, c)
return s
}
// BorderBottomBackground sets the background color of the bottom of the
// border.
func (s Style) BorderBottomBackground(c TerminalColor) Style {
s.set(borderBottomBackgroundKey, c)
return s
}
// BorderLeftBackground set the background color of the left side of the
// border.
func (s Style) BorderLeftBackground(c TerminalColor) 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
}
// Renderer sets the renderer for the style. This is useful for changing the
// renderer for a style that is being used in a different context.
func (s Style) Renderer(r *Renderer) Style {
s.r = r
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 ...TerminalColor) (top, right, bottom, left TerminalColor, 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
}
charmbracelet-lipgloss-500b193/size.go 0000664 0000000 0000000 00000002216 14764354362 0017725 0 ustar 00root root 0000000 0000000 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) len([]rune(string) as neither
// will give you accurate results.
func Width(str string) (width int) {
for _, l := range strings.Split(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 strings use \r\n for newlines you should
// convert them to \n first, or simply write a separate function for measuring
// height.
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
}
charmbracelet-lipgloss-500b193/style.go 0000664 0000000 0000000 00000031470 14764354362 0020117 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"strings"
"unicode"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/cellbuf"
"github.com/muesli/termenv"
)
const tabWidthDefault = 4
// Property for a key.
type propKey int64
// Available properties.
const (
// Boolean props come first.
boldKey propKey = 1 << iota
italicKey
underlineKey
strikethroughKey
reverseKey
blinkKey
faintKey
underlineSpacesKey
strikethroughSpacesKey
colorWhitespaceKey
// Non-boolean props.
foregroundKey
backgroundKey
widthKey
heightKey
alignHorizontalKey
alignVerticalKey
// Padding.
paddingTopKey
paddingRightKey
paddingBottomKey
paddingLeftKey
// Margins.
marginTopKey
marginRightKey
marginBottomKey
marginLeftKey
marginBackgroundKey
// Border runes.
borderStyleKey
// Border edges.
borderTopKey
borderRightKey
borderBottomKey
borderLeftKey
// Border foreground colors.
borderTopForegroundKey
borderRightForegroundKey
borderBottomForegroundKey
borderLeftForegroundKey
// Border background colors.
borderTopBackgroundKey
borderRightBackgroundKey
borderBottomBackgroundKey
borderLeftBackgroundKey
inlineKey
maxWidthKey
maxHeightKey
tabWidthKey
transformKey
)
// 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
}
// 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. It takes an optional string
// value to be set as the underlying string value for this style.
func NewStyle() Style {
return renderer.NewStyle()
}
// 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. It takes an optional string
// value to be set as the underlying string value for this style.
func (r *Renderer) NewStyle() Style {
s := Style{r: r}
return s
}
// Style contains a set of rules that comprise a style as a whole.
type Style struct {
r *Renderer
props props
value string
// we store bool props values here
attrs int
// props that have values
fgColor TerminalColor
bgColor TerminalColor
width int
height int
alignHorizontal Position
alignVertical Position
paddingTop int
paddingRight int
paddingBottom int
paddingLeft int
marginTop int
marginRight int
marginBottom int
marginLeft int
marginBgColor TerminalColor
borderStyle Border
borderTopFgColor TerminalColor
borderRightFgColor TerminalColor
borderBottomFgColor TerminalColor
borderLeftFgColor TerminalColor
borderTopBgColor TerminalColor
borderRightBgColor TerminalColor
borderBottomBgColor TerminalColor
borderLeftBgColor TerminalColor
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.r == nil {
s.r = renderer
}
if s.value != "" {
strs = append([]string{s.value}, strs...)
}
var (
str = joinString(strs...)
p = s.r.ColorProfile()
te = p.String()
teSpace = p.String()
teWhitespace = p.String()
bold = s.getAsBool(boldKey, false)
italic = s.getAsBool(italicKey, false)
underline = s.getAsBool(underlineKey, 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)
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)
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)
)
if transform != nil {
str = transform(str)
}
if s.props == 0 {
return s.maybeConvertTabs(str)
}
// Enable support for ANSI on the legacy Windows cmd.exe console. This is a
// no-op on non-Windows systems and on Windows runs only once.
enableLegacyWindowsANSI()
if bold {
te = te.Bold()
}
if italic {
te = te.Italic()
}
if underline {
te = te.Underline()
}
if reverse {
teWhitespace = teWhitespace.Reverse()
te = te.Reverse()
}
if blink {
te = te.Blink()
}
if faint {
te = te.Faint()
}
if fg != noColor {
te = te.Foreground(fg.color(s.r))
if styleWhitespace {
teWhitespace = teWhitespace.Foreground(fg.color(s.r))
}
if useSpaceStyler {
teSpace = teSpace.Foreground(fg.color(s.r))
}
}
if bg != noColor {
te = te.Background(bg.color(s.r))
if colorWhitespace {
teWhitespace = teWhitespace.Background(bg.color(s.r))
}
if useSpaceStyler {
teSpace = teSpace.Background(bg.color(s.r))
}
}
if underline {
te = te.Underline()
}
if strikethrough {
te = te.CrossOut()
}
if underlineSpaces {
teSpace = teSpace.Underline()
}
if strikethroughSpaces {
teSpace = teSpace.CrossOut()
}
// 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", "")
}
// Word wrap
if !inline && width > 0 {
wrapAt := width - leftPadding - rightPadding
str = cellbuf.Wrap(str, wrapAt, "")
}
// Render core text
{
var b strings.Builder
l := strings.Split(str, "\n")
for i := range l {
if useSpaceStyler {
// Look for spaces and apply a different styler
for _, r := range l[i] {
if unicode.IsSpace(r) {
b.WriteString(teSpace.Styled(string(r)))
continue
}
b.WriteString(te.Styled(string(r)))
}
} else {
b.WriteString(te.Styled(l[i]))
}
if i != len(l)-1 {
b.WriteRune('\n')
}
}
str = b.String()
}
// Padding
if !inline { //nolint:nestif
if leftPadding > 0 {
var st *termenv.Style
if colorWhitespace || styleWhitespace {
st = &teWhitespace
}
str = padLeft(str, leftPadding, st)
}
if rightPadding > 0 {
var st *termenv.Style
if colorWhitespace || styleWhitespace {
st = &teWhitespace
}
str = padRight(str, rightPadding, st)
}
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 *termenv.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)
styler termenv.Style
)
bgc := s.getAsColor(marginBackgroundKey)
if bgc != noColor {
styler = styler.Background(bgc.color(s.r))
}
// Add left and right margin
str = padLeft(str, leftMargin, &styler)
str = padRight(str, rightMargin, &styler)
// Top/bottom margin
if !inline {
_, width := getLines(str)
spaces := strings.Repeat(" ", width)
if topMargin > 0 {
str = styler.Styled(strings.Repeat(spaces+"\n", topMargin)) + str
}
if bottomMargin > 0 {
str += styler.Styled(strings.Repeat("\n"+spaces, bottomMargin))
}
}
return str
}
// Apply left padding.
func padLeft(str string, n int, style *termenv.Style) string {
return pad(str, -n, style)
}
// Apply right padding.
func padRight(str string, n int, style *termenv.Style) string {
return pad(str, n, style)
}
// 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.
func pad(str string, n int, style *termenv.Style) string {
if n == 0 {
return str
}
sp := strings.Repeat(" ", abs(n))
if style != nil {
sp = style.Styled(sp)
}
b := strings.Builder{}
l := strings.Split(str, "\n")
for i := range l {
switch {
// pad right
case n > 0:
b.WriteString(l[i])
b.WriteString(sp)
// pad left
default:
b.WriteString(sp)
b.WriteString(l[i])
}
if i != len(l)-1 {
b.WriteRune('\n')
}
}
return b.String()
}
func max(a, b int) int { //nolint:unparam,predeclared
if a > b {
return a
}
return b
}
func min(a, b int) int { //nolint:predeclared
if a < b {
return a
}
return b
}
func abs(a int) int {
if a < 0 {
return -a
}
return a
}
charmbracelet-lipgloss-500b193/style_test.go 0000664 0000000 0000000 00000033623 14764354362 0021160 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"fmt"
"io"
"reflect"
"strings"
"testing"
"github.com/muesli/termenv"
)
func TestUnderline(t *testing.T) {
r := NewRenderer(io.Discard)
r.SetColorProfile(termenv.TrueColor)
r.SetHasDarkBackground(true)
t.Parallel()
tt := []struct {
style Style
expected string
}{
{
r.NewStyle().Underline(true),
"\x1b[4;4ma\x1b[0m\x1b[4;4mb\x1b[0m\x1b[4m \x1b[0m\x1b[4;4mc\x1b[0m",
},
{
r.NewStyle().Underline(true).UnderlineSpaces(true),
"\x1b[4;4ma\x1b[0m\x1b[4;4mb\x1b[0m\x1b[4m \x1b[0m\x1b[4;4mc\x1b[0m",
},
{
r.NewStyle().Underline(true).UnderlineSpaces(false),
"\x1b[4;4ma\x1b[0m\x1b[4;4mb\x1b[0m \x1b[4;4mc\x1b[0m",
},
{
r.NewStyle().UnderlineSpaces(true),
"ab\x1b[4m \x1b[0mc",
},
}
for i, tc := range tt {
s := tc.style.SetString("ab c")
res := s.Render()
if res != tc.expected {
t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
i, tc.expected, formatEscapes(tc.expected),
res, formatEscapes(res))
}
}
}
func TestStrikethrough(t *testing.T) {
r := NewRenderer(io.Discard)
r.SetColorProfile(termenv.TrueColor)
r.SetHasDarkBackground(true)
t.Parallel()
tt := []struct {
style Style
expected string
}{
{
r.NewStyle().Strikethrough(true),
"\x1b[9ma\x1b[0m\x1b[9mb\x1b[0m\x1b[9m \x1b[0m\x1b[9mc\x1b[0m",
},
{
r.NewStyle().Strikethrough(true).StrikethroughSpaces(true),
"\x1b[9ma\x1b[0m\x1b[9mb\x1b[0m\x1b[9m \x1b[0m\x1b[9mc\x1b[0m",
},
{
r.NewStyle().Strikethrough(true).StrikethroughSpaces(false),
"\x1b[9ma\x1b[0m\x1b[9mb\x1b[0m \x1b[9mc\x1b[0m",
},
{
r.NewStyle().StrikethroughSpaces(true),
"ab\x1b[9m \x1b[0mc",
},
}
for i, tc := range tt {
s := tc.style.SetString("ab c")
res := s.Render()
if res != tc.expected {
t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
i, tc.expected, formatEscapes(tc.expected),
res, formatEscapes(res))
}
}
}
func TestStyleRender(t *testing.T) {
r := NewRenderer(io.Discard)
r.SetColorProfile(termenv.TrueColor)
r.SetHasDarkBackground(true)
t.Parallel()
tt := []struct {
style Style
expected string
}{
{
r.NewStyle().Foreground(Color("#5A56E0")),
"\x1b[38;2;89;86;224mhello\x1b[0m",
},
{
r.NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}),
"\x1b[38;2;89;86;224mhello\x1b[0m",
},
{
r.NewStyle().Bold(true),
"\x1b[1mhello\x1b[0m",
},
{
r.NewStyle().Italic(true),
"\x1b[3mhello\x1b[0m",
},
{
r.NewStyle().Underline(true),
"\x1b[4;4mh\x1b[0m\x1b[4;4me\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4mo\x1b[0m",
},
{
r.NewStyle().Blink(true),
"\x1b[5mhello\x1b[0m",
},
{
r.NewStyle().Faint(true),
"\x1b[2mhello\x1b[0m",
},
}
for i, tc := range tt {
s := tc.style.SetString("hello")
res := s.Render()
if res != tc.expected {
t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
i, tc.expected, formatEscapes(tc.expected),
res, formatEscapes(res))
}
}
}
func TestStyleCustomRender(t *testing.T) {
r := NewRenderer(io.Discard)
r.SetHasDarkBackground(false)
r.SetColorProfile(termenv.TrueColor)
tt := []struct {
style Style
expected string
}{
{
r.NewStyle().Foreground(Color("#5A56E0")),
"\x1b[38;2;89;86;224mhello\x1b[0m",
},
{
r.NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}),
"\x1b[38;2;255;254;18mhello\x1b[0m",
},
{
r.NewStyle().Bold(true),
"\x1b[1mhello\x1b[0m",
},
{
r.NewStyle().Italic(true),
"\x1b[3mhello\x1b[0m",
},
{
r.NewStyle().Underline(true),
"\x1b[4;4mh\x1b[0m\x1b[4;4me\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4mo\x1b[0m",
},
{
r.NewStyle().Blink(true),
"\x1b[5mhello\x1b[0m",
},
{
r.NewStyle().Faint(true),
"\x1b[2mhello\x1b[0m",
},
{
NewStyle().Faint(true).Renderer(r),
"\x1b[2mhello\x1b[0m",
},
}
for i, tc := range tt {
s := tc.style.SetString("hello")
res := s.Render()
if res != tc.expected {
t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
i, tc.expected, formatEscapes(tc.expected),
res, formatEscapes(res))
}
}
}
func TestStyleRenderer(t *testing.T) {
r := NewRenderer(io.Discard)
s1 := NewStyle().Bold(true)
s2 := s1.Renderer(r)
if s1.r == s2.r {
t.Fatalf("expected different renderers")
}
}
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)
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())
// 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[0m",
},
{
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\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
i, tc.expected, formatEscapes(tc.expected),
res, formatEscapes(res))
}
}
}
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 := 0; i < n/2; i++ {
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[0m"
if res != expected {
t.Errorf("Test #%d:\nExpected: %q\nGot: %q", i+1, expected, res)
}
}
}
func BenchmarkStyleRender(b *testing.B) {
s := NewStyle().
Bold(true).
Foreground(Color("#ffffff"))
for i := 0; i < b.N; i++ {
s.Render("Hello world")
}
}
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 interface{}) {
tb.Helper()
if !reflect.DeepEqual(a, b) {
tb.Errorf("%v != %v", a, b)
tb.FailNow()
}
}
func requireNotEqual(tb testing.TB, a, b interface{}) {
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)
}
}
charmbracelet-lipgloss-500b193/table/ 0000775 0000000 0000000 00000000000 14764354362 0017512 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/resizing.go 0000664 0000000 0000000 00000025213 14764354362 0021676 0 ustar 00root root 0000000 0000000 package table
import (
"math"
"strings"
"github.com/charmbracelet/lipgloss"
"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))
var allRows [][]string
if hasHeaders {
allRows = append([][]string{t.headers}, rows...)
} else {
allRows = rows
}
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 := t.styleFunc(rowIndex, j)
topMargin, rightMargin, bottomMargin, leftMargin := style.GetMargin()
topPadding, rightPadding, bottomPadding, leftPadding := style.GetPadding()
totalHorizontalPadding := leftMargin + rightMargin + leftPadding + rightPadding
column.xPadding = max(column.xPadding, totalHorizontalPadding)
column.fixedWidth = max(column.fixedWidth, style.GetWidth())
r.rowHeights[i] = max(r.rowHeights[i], style.GetHeight())
totalVerticalPadding := topMargin + bottomMargin + topPadding + bottomPadding
r.yPaddings[i][j] = totalVerticalPadding
}
}
// 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()
}
// 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
}
// 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()
}
return r.shrinkTableWidth()
}
// 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, rowHeights []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]++
}
rowHeights = r.expandRowHeigths(colWidths)
return
}
// shrinkTableWidth shrinks the table width.
func (r *resizer) shrinkTableWidth() (colWidths, rowHeights []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)
return colWidths, r.expandRowHeigths(colWidths)
}
// expandRowHeigths expands the row heights.
func (r *resizer) expandRowHeigths(colWidths []int) (rowHeights []int) {
rowHeights = r.defaultRowHeights()
if !r.wrap {
return rowHeights
}
for i, row := range r.allRows {
for j, cell := range row {
height := r.detectContentHeight(cell, colWidths[j]-r.xPaddingForCol(j)) + r.xPaddingForCell(i, j)
if height > rowHeights[i] {
rowHeights[i] = height
}
}
}
return
}
// 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]
}
if rowHeights[i] < 1 {
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
}
// xPaddingForCell returns the horizontal padding for a cell.
func (r *resizer) xPaddingForCell(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 (r.columnCount() * r.borderPerCell()) + r.extraBorder()
}
// borderPerCell returns number of border chars per cell.
func (r *resizer) borderPerCell() int {
if r.borderColumn {
return 1
}
return 0
}
// extraBorder returns the number of the extra border char at the end of the table.
func (r *resizer) extraBorder() int {
if r.borderColumn {
return 1
}
return 0
}
// 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
}
charmbracelet-lipgloss-500b193/table/rows.go 0000664 0000000 0000000 00000005267 14764354362 0021045 0 ustar 00root root 0000000 0000000 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 := 0; i < m.data.Rows(); i++ {
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 := 0; i < m.data.Rows(); i++ {
if m.filter(i) {
j++
}
}
return j
}
// dataToMatrix converts an object that implements the Data interface to a table.
func dataToMatrix(data Data) (rows [][]string) {
numRows := data.Rows()
numCols := data.Columns()
rows = make([][]string, numRows)
for i := 0; i < numRows; i++ {
rows[i] = make([]string, numCols)
for j := 0; j < numCols; j++ {
rows[i][j] = data.At(i, j)
}
}
return
}
charmbracelet-lipgloss-500b193/table/table.go 0000664 0000000 0000000 00000030022 14764354362 0021125 0 ustar 00root root 0000000 0000000 package table
import (
"strings"
"github.com/charmbracelet/lipgloss"
"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 {
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
offset int
wrap bool
// widths tracks the width of each column.
widths []int
// heights tracks the height of each row.
heights []int
}
// New returns a new Table that can be modified through different
// attributes.
//
// By default, a table has no border, no styling, and no rows.
func New() *Table {
return &Table{
styleFunc: DefaultStyles,
border: lipgloss.RoundedBorder(),
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
}
// 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 lipgloss.NewStyle()
}
return t.styleFunc(row, col)
}
// Data sets the table data.
func (t *Table) Data(data Data) *Table {
t.data = data
return t
}
// 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
}
// 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
return t
}
// Width sets the table width, this auto-sizes the columns to fit the width by
// either expanding or contracting the widths of each column as a best effort
// approach.
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
}
// Offset sets the table rendering offset.
//
// Warning: you may declare Offset only after setting Rows. Otherwise it will be
// ignored.
func (t *Table) Offset(o int) *Table {
t.offset = o
return t
}
// Wrap dictates whether or not the table content should wrap.
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())
sb.WriteString("\n")
}
var bottom string
if t.borderBottom {
bottom = t.constructBottomBorder()
}
// If there are no data rows render nothing.
if t.data.Rows() > 0 {
switch {
case t.useManualHeight:
// The height of the top border. Subtract 1 for the newline.
topHeight := lipgloss.Height(sb.String()) - 1
availableLines := t.height - (topHeight + lipgloss.Height(bottom))
// if the height is larger than the number of rows, use the number
// of rows.
if availableLines > t.data.Rows() {
availableLines = t.data.Rows()
}
sb.WriteString(t.constructRows(availableLines))
default:
for r := t.offset; r < t.data.Rows(); r++ {
sb.WriteString(t.constructRow(r, false))
}
}
}
sb.WriteString(bottom)
return lipgloss.NewStyle().
MaxHeight(t.computeHeight()).
MaxWidth(t.width).
Render(sb.String())
}
// 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 := 0; i < len(t.widths); i++ {
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 := 0; i < len(t.widths); i++ {
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
if t.borderLeft {
s.WriteString(t.borderStyle.Render(t.border.Left))
}
for i, header := range t.headers {
s.WriteString(t.style(HeaderRow, i).
MaxHeight(1).
Width(t.widths[i]).
MaxWidth(t.widths[i]).
Render(ansi.Truncate(header, t.widths[i], "…")))
if i < len(t.headers)-1 && t.borderColumn {
s.WriteString(t.borderStyle.Render(t.border.Left))
}
}
if t.borderHeader {
if t.borderRight {
s.WriteString(t.borderStyle.Render(t.border.Right))
}
s.WriteString("\n")
if t.borderLeft {
s.WriteString(t.borderStyle.Render(t.border.MiddleLeft))
}
for i := 0; i < len(t.headers); i++ {
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))
}
}
if t.borderRight && !t.borderHeader {
s.WriteString(t.borderStyle.Render(t.border.Right))
}
return s.String()
}
func (t *Table) constructRows(availableLines int) string {
var sb strings.Builder
// The number of rows to render after removing the offset.
offsetRowCount := t.data.Rows() - t.offset
// The number of rows to render. We always render at least one row.
rowsToRender := availableLines
rowsToRender = max(rowsToRender, 1)
// Check if we need to render an overflow row.
needsOverflow := rowsToRender < offsetRowCount
// only use the offset as the starting value if there is overflow.
rowIdx := t.offset
if !needsOverflow {
// if there is no overflow, just render to the height of the table
// check there's enough content to fill the table
rowIdx = t.data.Rows() - rowsToRender
}
for rowsToRender > 0 && rowIdx < t.data.Rows() {
// Whenever the height is too small to render all rows, the bottom row will be an overflow row (ellipsis).
isOverflow := needsOverflow && rowsToRender == 1
sb.WriteString(t.constructRow(rowIdx, isOverflow))
rowIdx++
rowsToRender--
}
return sb.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
hasHeaders := len(t.headers) > 0
height := t.heights[index+btoi(hasHeaders)]
if isOverflow {
height = 1
}
var cells []string
left := strings.Repeat(t.borderStyle.Render(t.border.Left)+"\n", height)
if t.borderLeft {
cells = append(cells, left)
}
for c := 0; c < t.data.Columns(); c++ {
cellWidth := t.widths[c]
cell := "…"
if !isOverflow {
cell = t.data.At(index, c)
}
cellStyle := t.style(index, c)
if !t.wrap {
length := (cellWidth * height) - cellStyle.GetHorizontalPadding()
cell = ansi.Truncate(cell, length, "…")
}
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 && index < t.data.Rows()-1 {
s.WriteString(t.borderStyle.Render(t.border.MiddleLeft))
for i := 0; i < len(t.widths); i++ {
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))
}
}
s.WriteString(t.borderStyle.Render(t.border.MiddleRight) + "\n")
}
return s.String()
}
charmbracelet-lipgloss-500b193/table/table_test.go 0000664 0000000 0000000 00000257670 14764354362 0022210 0 ustar 00root root 0000000 0000000 package table
import (
"fmt"
"strings"
"testing"
"unicode"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/exp/golden"
"github.com/muesli/termenv"
)
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?")
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼──────────────┼───────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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?")
expected := strings.TrimSpace(`
┌──────────┬───────────────────────────────┬─────────────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼───────────────────────────────┼─────────────────┤
│ Chinese │ 您好 │ 你好 │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Здравствуйте │ Привет │
│ Spanish │ Hola │ ¿Qué tal? │
│ English │ You look absolutely fabulous. │ How's it going? │
└──────────┴───────────────────────────────┴─────────────────┘
`)
if got := ansi.Strip(table.String()); got != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, got)
}
}
func TestTableEmpty(t *testing.T) {
table := New().
Border(lipgloss.NormalBorder()).
StyleFunc(TableStyle).
Headers("LANGUAGE", "FORMAL", "INFORMAL")
expected := strings.TrimSpace(`
┌──────────┬────────┬──────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼────────┼──────────┤
└──────────┴────────┴──────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, table.String())
}
}
func TestTableOffset(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?").
Offset(1)
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼──────────────┼───────────┤
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
╔══════════╦══════════════╦═══════════╗
║ LANGUAGE ║ FORMAL ║ INFORMAL ║
╠══════════╬══════════════╬═══════════╣
║ Chinese ║ Nǐn hǎo ║ Nǐ hǎo ║
║ French ║ Bonjour ║ Salut ║
║ Japanese ║ こんにちは ║ やあ ║
║ Russian ║ Zdravstvuyte ║ Privet ║
║ Spanish ║ Hola ║ ¿Qué tal? ║
╚══════════╩══════════════╩═══════════╝
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼──────────────┼───────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ │
├──────────┼──────────────┼───────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┬────────┬────────┐
│ LANGUAGE │ FORMAL │ │ │ │
├──────────┼──────────────┼───────────┼────────┼────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │ │ │
│ French │ Bonjour │ Salut │ Salut │ │
│ Japanese │ こんにちは │ やあ │ │ │
│ Russian │ Zdravstvuyte │ Privet │ Privet │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │ │ │
└──────────┴──────────────┴───────────┴────────┴────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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?")
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┐
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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?")
expected := strings.TrimSpace(`
┌───────────────────────────────────┐
│ Chinese Nǐn hǎo Nǐ hǎo │
│ French Bonjour Salut │
│ Japanese こんにちは やあ │
│ Russian Zdravstvuyte Privet │
│ Spanish Hola ¿Qué tal? │
└───────────────────────────────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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?")
expected := strings.TrimSpace(`
┌───────────────────────────────────┐
│ LANGUAGE FORMAL INFORMAL │
├───────────────────────────────────┤
│ Chinese Nǐn hǎo Nǐ hǎo │
│ French Bonjour Salut │
│ Japanese こんにちは やあ │
│ Russian Zdravstvuyte Privet │
│ Spanish Hola ¿Qué tal? │
└───────────────────────────────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
┌───────────────────────────────────────────────────┐
│ LANGUAGE FORMAL │
├───────────────────────────────────────────────────┤
│ Chinese Nǐn hǎo Nǐ hǎo │
│ French Bonjour Salut Salut │
│ Japanese こんにちは やあ │
│ Russian Zdravstvuyte Privet Privet Privet │
│ Spanish Hola ¿Qué tal? │
└───────────────────────────────────────────────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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)
expected := strings.TrimPrefix(`
LANGUAGE │ FORMAL │ INFORMAL
──────────┼──────────────┼───────────
Chinese │ Nǐn hǎo │ Nǐ hǎo
French │ Bonjour │ Salut
Japanese │ こんにちは │ やあ
Russian │ Zdravstvuyte │ Privet
Spanish │ Hola │ ¿Qué tal? `, "\n")
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", debug(expected), debug(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)
expected := strings.TrimPrefix(`
LANGUAGE │ FORMAL │ INFORMAL
Chinese │ Nǐn hǎo │ Nǐ hǎo
French │ Bonjour │ Salut
Japanese │ こんにちは │ やあ
Russian │ Zdravstvuyte │ Privet
Spanish │ Hola │ ¿Qué tal? `, "\n")
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", debug(expected), debug(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)
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼──────────────┼───────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
├──────────┼──────────────┼───────────┤
│ French │ Bonjour │ Salut │
├──────────┼──────────────┼───────────┤
│ Japanese │ こんにちは │ やあ │
├──────────┼──────────────┼───────────┤
│ Russian │ Zdravstvuyte │ Privet │
├──────────┼──────────────┼───────────┤
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
┌──────────────────┬─────────────────────────┐
│ 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. │
│ │ │
└──────────────────┴─────────────────────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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.`)
expected := strings.TrimSpace(`
┌──────────────────┬─────────────────────────┐
│ 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. │
│ │ │
└──────────────────┴─────────────────────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
┌──────────────────────────┬─────────────────────────┬─────────────────────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────────────────────┼─────────────────────────┼─────────────────────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────────────────────┴─────────────────────────┴─────────────────────────┘
`)
if lipgloss.Width(table.String()) != 80 {
t.Fatalf("expected table width to be 80, got %d", lipgloss.Width(table.String()))
}
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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?"},
}
table := New().
Width(30).
StyleFunc(TableStyle).
Border(lipgloss.NormalBorder()).
Headers("LANGUAGE", "FORMAL", "INFORMAL").
Rows(rows...)
expected := strings.TrimSpace(`
┌────────┬─────────┬─────────┐
│ LANGUA │ FORMAL │ INFORMA │
├────────┼─────────┼─────────┤
│ Chines │ Nǐn hǎo │ Nǐ hǎo │
│ e │ │ │
│ French │ Bonjour │ Salut │
│ Japane │ こんに │ やあ │
│ se │ ちは │ │
│ Russia │ Zdravst │ Privet │
│ n │ vuyte │ │
│ Spanis │ Hola │ ¿Qué │
│ h │ │ tal? │
└────────┴─────────┴─────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
┌──────┬─────┬──────────┐
│ Name │ Age │ Location │
├──────┼─────┼──────────┤
│ Kini │ 40 │ New York │
│ Eli │ 30 │ London │
│ Iris │ 20 │ Paris │
└──────┴─────┴──────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
┏━━━━┳━━━━━┳━━━━━┓
┃ LA ┃ FOR ┃ INF ┃
┣━━━━╋━━━━━╋━━━━━┫
┃ C… ┃ 您… ┃ 你… ┃
┃ J… ┃ こ… ┃ や… ┃
┃ A… ┃ أه… ┃ أه… ┃
┃ R… ┃ Зд… ┃ Пр… ┃
┃ S… ┃ Ho… ┃ ¿Q… ┃
┃ E… ┃ Yo… ┃ Ho… ┃
┗━━━━┻━━━━━┻━━━━━┛
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
┌
│
├
│
│
│
│
│
└
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
──────────────────────────────
LANGUAGE FORMAL INFORMAL
──────────────────────────────
Chinese Nǐn hǎo Nǐ hǎo
French Bonjour Salut
Japanese こんにち やあ
は
Russian Zdravstv Privet
uyte
Spanish Hola ¿Qué
tal?
──────────────────────────────
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, table.String())
}
}
func TestTableWidthShrinkNoBorders(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...)
expected := strings.TrimSpace(`
──────────────────────────────
LANGUAGE FORMAL INFORMAL
──────────────────────────────
Chinese Nǐn hǎo Nǐ hǎo
French Bonjour Salut
Japanese こんにち やあ
は
Russian Zdravstv Privet
uyte
Spanish Hola ¿Qué
tal?
──────────────────────────────
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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)
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼──────────────┼───────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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)
expected := strings.TrimSpace(`
┌──────────┬─────────┬──────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼─────────┼──────────┤
│ French │ Bonjour │ Salut │
└──────────┴─────────┴──────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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...)
expected := strings.TrimSpace(`
┌───────────┬────────┬──────┐
│ Fruit │ Color │ Code │
├───────────┼────────┼──────┤
│ Apple │ Red │ 31 │
│ Lime │ Green │ 32 │
│ Banana │ Yellow │ 33 │
│ Blueberry │ Blue │ 34 │
└───────────┴────────┴──────┘
`)
if stripString(table.String()) != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, stripString(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?")
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼──────────────┼───────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, 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?")
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼──────────────┼───────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, table.String())
}
}
func TestTableHeightShrink(t *testing.T) {
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?")
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼──────────────┼───────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ … │ … │ … │
└──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, table.String())
}
}
func TestTableHeightMinimum(t *testing.T) {
table := New().
Height(0).
Border(lipgloss.NormalBorder()).
StyleFunc(TableStyle).
Headers("ID", "LANGUAGE", "FORMAL", "INFORMAL").
Row("1", "Chinese", "Nǐn hǎo", "Nǐ hǎo").
Row("2", "French", "Bonjour", "Salut").
Row("3", "Japanese", "こんにちは", "やあ").
Row("4", "Russian", "Zdravstvuyte", "Privet").
Row("5", "Spanish", "Hola", "¿Qué tal?")
expected := strings.TrimSpace(`
┌────┬──────────┬──────────────┬───────────┐
│ ID │ LANGUAGE │ FORMAL │ INFORMAL │
├────┼──────────┼──────────────┼───────────┤
│ … │ … │ … │ … │
└────┴──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, table.String())
}
}
func TestTableHeightMinimumShowData(t *testing.T) {
table := New().
Height(0).
Border(lipgloss.NormalBorder()).
StyleFunc(TableStyle).
Headers("LANGUAGE", "FORMAL", "INFORMAL").
Row("Chinese", "Nǐn hǎo", "Nǐ hǎo")
expected := strings.TrimSpace(`
┌──────────┬─────────┬──────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼─────────┼──────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
└──────────┴─────────┴──────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, table.String())
}
}
func TestTableHeightWithOffset(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?").
Offset(1)
expected := strings.TrimSpace(`
┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼──────────────┼───────────┤
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘
`)
if table.String() != expected {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, table.String())
}
}
func TestStyleFunc(t *testing.T) {
lipgloss.SetColorProfile(termenv.TrueColor)
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_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())
got := table.String()
want := `┌──┬────┬──┬──┐
│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│
└──┴────┴──┴──┘`
if got != want {
t.Logf("detailed view...\ngot:\n%q\nwant:\n%q", got, want)
t.Fatalf("got:\n%s\nwant:\n%s", got, want)
}
}
func TestTableShrinkWithOffset(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"},
}
table := New().
Rows(rows...).
Offset(80).
Height(45)
got := lipgloss.Height(table.String())
if got != table.height {
t.Fatalf("expected the height to be %d with an offset of %d. got: table with height %d\n%s", table.height, table.offset, got, table.String())
}
}
func debug(s string) string {
return strings.ReplaceAll(s, " ", ".")
}
func stripString(str string) string {
s := ansi.Strip(str)
ss := strings.Split(s, "\n")
var lines []string
for _, l := range ss {
trim := strings.TrimRightFunc(l, unicode.IsSpace)
lines = append(lines, trim)
}
return strings.Join(lines, "\n")
}
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()))
})
}
}
// 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()))
}
charmbracelet-lipgloss-500b193/table/testdata/ 0000775 0000000 0000000 00000000000 14764354362 0021323 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestBorderStyles/ 0000775 0000000 0000000 00000000000 14764354362 0024604 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestBorderStyles/ASCIIBorder.golden 0000664 0000000 0000000 00000000564 14764354362 0027771 0 ustar 00root root 0000000 0000000 +----------+--------------+-----------+
| LANGUAGE | FORMAL | INFORMAL |
+----------+--------------+-----------+
| Chinese | Nǐn hǎo | Nǐ hǎo |
| French | Bonjour | Salut |
| Japanese | こんにちは | やあ |
| Russian | Zdravstvuyte | Privet |
| Spanish | Hola | ¿Qué tal? |
+----------+--------------+-----------+ charmbracelet-lipgloss-500b193/table/testdata/TestBorderStyles/BlockBorder.golden 0000664 0000000 0000000 00000001216 14764354362 0030166 0 ustar 00root root 0000000 0000000 ███████████████████████████████████████
█ LANGUAGE █ FORMAL █ INFORMAL █
███████████████████████████████████████
█ Chinese █ Nǐn hǎo █ Nǐ hǎo █
█ French █ Bonjour █ Salut █
█ Japanese █ こんにちは █ やあ █
█ Russian █ Zdravstvuyte █ Privet █
█ Spanish █ Hola █ ¿Qué tal? █
███████████████████████████████████████ charmbracelet-lipgloss-500b193/table/testdata/TestBorderStyles/HiddenBorder.golden 0000664 0000000 0000000 00000000564 14764354362 0030334 0 ustar 00root root 0000000 0000000
LANGUAGE FORMAL INFORMAL
Chinese Nǐn hǎo Nǐ hǎo
French Bonjour Salut
Japanese こんにちは やあ
Russian Zdravstvuyte Privet
Spanish Hola ¿Qué tal?
charmbracelet-lipgloss-500b193/table/testdata/TestBorderStyles/MarkdownBorder.golden 0000664 0000000 0000000 00000000444 14764354362 0030720 0 ustar 00root root 0000000 0000000 | LANGUAGE | FORMAL | INFORMAL |
|----------|--------------|-----------|
| Chinese | Nǐn hǎo | Nǐ hǎo |
| French | Bonjour | Salut |
| Japanese | こんにちは | やあ |
| Russian | Zdravstvuyte | Privet |
| Spanish | Hola | ¿Qué tal? | charmbracelet-lipgloss-500b193/table/testdata/TestBorderStyles/NormalBorder.golden 0000664 0000000 0000000 00000001216 14764354362 0030364 0 ustar 00root root 0000000 0000000 ┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼──────────────┼───────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘ charmbracelet-lipgloss-500b193/table/testdata/TestBorderStyles/RoundedBorder.golden 0000664 0000000 0000000 00000001216 14764354362 0030534 0 ustar 00root root 0000000 0000000 ╭──────────┬──────────────┬───────────╮
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼──────────────┼───────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
╰──────────┴──────────────┴───────────╯ charmbracelet-lipgloss-500b193/table/testdata/TestBorderStyles/ThickBorder.golden 0000664 0000000 0000000 00000001216 14764354362 0030176 0 ustar 00root root 0000000 0000000 ┏━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ LANGUAGE ┃ FORMAL ┃ INFORMAL ┃
┣━━━━━━━━━━╋━━━━━━━━━━━━━━╋━━━━━━━━━━━┫
┃ Chinese ┃ Nǐn hǎo ┃ Nǐ hǎo ┃
┃ French ┃ Bonjour ┃ Salut ┃
┃ Japanese ┃ こんにちは ┃ やあ ┃
┃ Russian ┃ Zdravstvuyte ┃ Privet ┃
┃ Spanish ┃ Hola ┃ ¿Qué tal? ┃
┗━━━━━━━━━━┻━━━━━━━━━━━━━━┻━━━━━━━━━━━┛ charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping/ 0000775 0000000 0000000 00000000000 14764354362 0025305 5 ustar 00root root 0000000 0000000 LongHeaderContentLongAndShortRows.golden 0000664 0000000 0000000 00000002632 14764354362 0035124 0 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping ╭─────────────┬────────────────────────────────────────────────┬───────────────╮
│ Destination │ Why are you going on this trip? Is it a hot or │ 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 │ │ $$$ │
╰─────────────┴────────────────────────────────────────────────┴───────────────╯ charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping/LongRowContent.golden 0000664 0000000 0000000 00000002672 14764354362 0031430 0 ustar 00root root 0000000 0000000 ╭─────────┬────────────────────────────────────────┬──────┬──────────┬─────────╮
│ 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. │ │ │ │
╰─────────┴────────────────────────────────────────┴──────┴──────────┴─────────╯ charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping/LongRowContentNoWrap.golden 0000664 0000000 0000000 00000001616 14764354362 0032554 0 ustar 00root root 0000000 0000000 ╭─────────┬────────────────────────────────────────┬──────┬──────────┬─────────╮
│ Name │ Description │ Type │ Required │ Default │
├─────────┼────────────────────────────────────────┼──────┼──────────┼─────────┤
│ command │ A command to be executed inside the c… │ yes │ hello │ yep │
╰─────────┴────────────────────────────────────────┴──────┴──────────┴─────────╯ LongRowContentNoWrapCustomMargins.golden 0000664 0000000 0000000 00000001616 14764354362 0035211 0 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping ╭───────────┬───────────────────────────────────────┬───────┬─────────┬────────╮
│ Name │ Description │ Typ │ Requi │ Defa │
├───────────┼───────────────────────────────────────┼───────┼─────────┼────────┤
│ command │ A command to be executed inside th… │ yes │ hello │ yep │
╰───────────┴───────────────────────────────────────┴───────┴─────────┴────────╯ LongRowContentNoWrapNoMargins.golden 0000664 0000000 0000000 00000001616 14764354362 0034313 0 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping ╭───────┬────────────────────────────────────────────────┬────┬────────┬───────╮
│ Name │ Description │Type│Required│Default│
├───────┼────────────────────────────────────────────────┼────┼────────┼───────┤
│command│A command to be executed inside the container t…│yes │hello │yep │
╰───────┴────────────────────────────────────────────────┴────┴────────┴───────╯ charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping/LongTextDifferentLanguages.golden 0000664 0000000 0000000 00000015352 14764354362 0033727 0 ustar 00root root 0000000 0000000 ╭──────────────┬───────────────┬───────────────┬───────────────┬───────────────╮
│ 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稿は │ │ │ │
│ │ かラせ江利ス │ │ │ │
│ │ 宏丸霊ミ考整 │ │ │ │
│ │ ス静将ず業巨 │ │ │ │
│ │ 職ノラホ収嗅 │ │ │ │
│ │ ざな。 │ │ │ │
╰──────────────┴───────────────┴───────────────┴───────────────┴───────────────╯ charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping/MissingRowContent.golden 0000664 0000000 0000000 00000002672 14764354362 0032142 0 ustar 00root root 0000000 0000000 ╭─────────┬────────────────────────────────────────┬──────┬──────────┬─────────╮
│ 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. │ │ │ │
╰─────────┴────────────────────────────────────────┴──────┴──────────┴─────────╯ charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_ColumnWidth/ 0000775 0000000 0000000 00000000000 14764354362 0027622 5 ustar 00root root 0000000 0000000 LongHeaderContentLongAndShortRows.golden 0000664 0000000 0000000 00000003120 14764354362 0037432 0 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_ColumnWidth ╭─────────────────────────────────────────┬──────────────────────────────┬─────╮
│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 │ │$$$ │
╰─────────────────────────────────────────┴──────────────────────────────┴─────╯ charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_ColumnWidth/LongRowContent.golden 0000664 0000000 0000000 00000003164 14764354362 0033742 0 ustar 00root root 0000000 0000000 ╭─────────────┬──────────────────────────────┬─────┬─────────────┬─────────────╮
│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.│ │ │ │
╰─────────────┴──────────────────────────────┴─────┴─────────────┴─────────────╯ LongTextDifferentLanguages.golden 0000664 0000000 0000000 00000021350 14764354362 0036160 0 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_ColumnWidth ╭─────────────┬──────────────────────────────┬─────┬─────────────┬─────────────╮
│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. │ │ومن. │前作臓 │ │
│ │ │أفاق │ │ │
│ │ │للسيط│ │ │
│ │ │رة │ │ │
│ │ │التار│ │ │
│ │ │يخ، │ │ │
│ │ │مع │ │ │
│ │ │بحث, │ │ │
│ │ │كلّ │ │ │
│ │ │اتّجة │ │ │
│ │ │القوى│ │ │
│ │ │مع. │ │ │
│ │ │فبعد │ │ │
│ │ │ايطال│ │ │
│ │ │يا، │ │ │
│ │ │تم │ │ │
│ │ │حتى, │ │ │
│ │ │لكل │ │ │
│ │ │تم │ │ │
│ │ │جسيمة│ │ │
│ │ │الإحت│ │ │
│ │ │فاظ │ │ │
│ │ │وباست│ │ │
│ │ │ثناء,│ │ │
│ │ │عل │ │ │
│ │ │فرنسا│ │ │
│ │ │وانته│ │ │
│ │ │اءً │ │ │
│ │ │الإقت│ │ │
│ │ │صادية│ │ │
│ │ │عرض. │ │ │
│ │ │ونتج │ │ │
│ │ │دأبوا│ │ │
│ │ │إحكام│ │ │
│ │ │بال │ │ │
│ │ │إذ. │ │ │
│ │ │لغات │ │ │
│ │ │عملية│ │ │
│ │ │وتم │ │ │
│ │ │مع, │ │ │
│ │ │وصل │ │ │
│ │ │بداية│ │ │
│ │ │وبغطا│ │ │
│ │ │ء │ │ │
│ │ │البري│ │ │
│ │ │ة بل,│ │ │
│ │ │أي │ │ │
│ │ │قررت │ │ │
│ │ │بلاده│ │ │
│ │ │فكانت│ │ │
│ │ │حدى │ │ │
│ │ │ │ │ │
╰─────────────┴──────────────────────────────┴─────┴─────────────┴─────────────╯ MissingRowContent.golden 0000664 0000000 0000000 00000003164 14764354362 0034375 0 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_ColumnWidth ╭─────────────┬──────────────────────────────┬─────┬─────────────┬─────────────╮
│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.│ │ │ │
╰─────────────┴──────────────────────────────┴─────┴─────────────┴─────────────╯ charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_WithMargins/ 0000775 0000000 0000000 00000000000 14764354362 0027621 5 ustar 00root root 0000000 0000000 LongHeaderContentLongAndShortRows.golden 0000664 0000000 0000000 00000003114 14764354362 0037434 0 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_WithMargins ╭───────────────────┬───────────────────────────────────────┬──────────────────╮
│ Destination │ Why are you going on this trip? Is │ 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 │ │ $$$ │
╰───────────────────┴───────────────────────────────────────┴──────────────────╯ charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_WithMargins/LongRowContent.golden 0000664 0000000 0000000 00000004671 14764354362 0033745 0 ustar 00root root 0000000 0000000 ╭───────────────┬────────────────────────┬───────────┬─────────────┬───────────╮
│ Name │ Description │ Type │ Required │ Default│
├───────────────┼────────────────────────┼───────────┼─────────────┼───────────┤
│ 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. │ │ │ │
╰───────────────┴────────────────────────┴───────────┴─────────────┴───────────╯ LongTextDifferentLanguages.golden 0000664 0000000 0000000 00000027345 14764354362 0036171 0 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_WithMargins ╭──────────────┬───────────────┬───────────────┬───────────────┬───────────────╮
│ 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 │ │ │ │
│ │ 稿はか │ │ │ │
│ │ ラせ江 │ │ │ │
│ │ 利ス宏 │ │ │ │
│ │ 丸霊ミ │ │ │ │
│ │ 考整ス │ │ │ │
│ │ 静将ず │ │ │ │
│ │ 業巨職 │ │ │ │
│ │ ノラホ │ │ │ │
│ │ 収嗅ざ │ │ │ │
│ │ な。 │ │ │ │
╰──────────────┴───────────────┴───────────────┴───────────────┴───────────────╯ MissingRowContent.golden 0000664 0000000 0000000 00000003613 14764354362 0034373 0 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_WithMargins ╭───────────────┬────────────────────────────────┬───────────┬────────┬────────╮
│ Name │ Description │ Type │ Requ│ Defa│
├───────────────┼────────────────────────────────┼───────────┼────────┼────────┤
│ 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. │ │ │ │
╰───────────────┴────────────────────────────────┴───────────┴────────┴────────╯ charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_WithPadding/ 0000775 0000000 0000000 00000000000 14764354362 0027567 5 ustar 00root root 0000000 0000000 LongHeaderContentLongAndShortRows.golden 0000664 0000000 0000000 00000002632 14764354362 0037406 0 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_WithPadding ╭─────────────┬────────────────────────────────────────────────┬───────────────╮
│ Destination │ Why are you going on this trip? Is it a hot or │ 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 │ │ $$$ │
╰─────────────┴────────────────────────────────────────────────┴───────────────╯ charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_WithPadding/LongRowContent.golden 0000664 0000000 0000000 00000002672 14764354362 0033712 0 ustar 00root root 0000000 0000000 ╭─────────┬────────────────────────────────────────┬──────┬──────────┬─────────╮
│ 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. │ │ │ │
╰─────────┴────────────────────────────────────────┴──────┴──────────┴─────────╯ LongTextDifferentLanguages.golden 0000664 0000000 0000000 00000014002 14764354362 0036121 0 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_WithPadding ╭───────┬────────────────┬─────────────────┬─────────────────┬─────────────────╮
│ Hello │ 你好 │ مرحبًا │ 안녕하세요 │ │
├───────┼────────────────┼─────────────────┼─────────────────┼─────────────────┤
│ │ 耐許ヱヨカハ調 │ شيء قد للحكومة │ 版応道潟部中幕 │ 각급 │
│ │ 出あゆ監件び理 │ والكوري │ 爆営報門案名見 │ 선거관리위원회 │
│ │ 別よン國給災レ │ الأوروبيّون, │ 壌府。博健必権 │ 의 │
│ │ ホチ権輝モエフ │ بوابة تعديل │ 次覧編仕断青場 │ 조직·직무범위 │
│ │ 会割もフ響3現 │ واعتلاء ضرب بـ. │ 内凄新東深簿代 │ 기타 필요한 │
│ │ エツ文時しだび │ إذ أسر اتّجة │ 供供。守聞書神 │ 사항은 법률로 │
│ │ ほ経機ムイメフ │ اعلان, ٣٠ │ 秀同浜東波恋闘 │ 정한다. │
│ │ 敗文ヨク現義な │ اكتوبر العصبة │ 秀。未格打好作 │ 임시회의 회기는 │
│ │ さド請情ゆじょ │ استمرار ومن. │ 器来利阪持西焦 │ 30일을 초과할 │
│ │ て憶主管州けで │ أفاق للسيطرة │ 朝三女。権幽問 │ 수 없다. 국가는 │
│ │ ふく。排ゃわつ │ التاريخ، مع │ 季負娘購合旧資 │ 여자의 복지와 │
│ │ げ美刊ヱミ出見 │ بحث, كلّ اتّجة │ 健載員式活陸。 │ 권익의 향상을 │
│ │ ツ南者オ抜豆ハ │ القوى مع. فبعد │ 未倍校朝遺続術 │ 위하여 │
│ │ トロネ論索モネ │ ايطاليا، تم │ 吉迎暮広知角亡 │ 노력하여야 │
│ │ ニイ任償スヲ話 │ حتى, لكل تم │ 志不説空住。法 │ 한다. 국군의 │
│ │ 破リヤヨ秒止口 │ جسيمة الإحتفاظ │ 省当死年勝絡聞 │ 조직과 편성은 │
│ │ イセソス止央の │ وباستثناء, عل │ 方北投健。室分 │ 법률로 정한다. │
│ │ さ食周健でてつ │ فرنسا وانتهاءً │ 性山天態意画詳 │ │
│ │ だ官送ト読聴遊 │ الإقتصادية عرض. │ 知浅方裁。変激 │ │
│ │ 容ひるべ。際ぐ │ ونتج دأبوا │ 伝阜中野品省載 │ │
│ │ ドらづ市居ネム │ إحكام بال إذ. │ 嗅闘額端反。中 │ │
│ │ ヤ研校35岩6繹 │ لغات عملية وتم │ 必台際造事寄民 │ │
│ │ ごわク報拐イ革 │ مع, وصل بداية │ 経能前作臓 │ │
│ │ 深52球ゃレスご │ وبغطاء البرية │ │ │
│ │ 究東スラ衝3間 │ بل, أي قررت │ │ │
│ │ ラ録占たス。 │ بلاده فكانت حدى │ │ │
│ │ 禁にンご忘康ざ │ │ │ │
│ │ ほぎル騰般ねど │ │ │ │
│ │ 事超スんいう真 │ │ │ │
│ │ 表何カモ自浩ヲ │ │ │ │
│ │ シミ図客線るふ │ │ │ │
│ │ 静王ぱーま写村 │ │ │ │
│ │ 月掛焼詐面ぞゃ │ │ │ │
│ │ 。昇強ごントほ │ │ │ │
│ │ 価保キ族85岡モ │ │ │ │
│ │ テ恋困ひりこな │ │ │ │
│ │ 刊並せご出来ぼ │ │ │ │
│ │ ぎむう点目ヲウ │ │ │ │
│ │ 止環公ニレ事応 │ │ │ │
│ │ タス必書タメム │ │ │ │
│ │ ノ当84無信升ち │ │ │ │
│ │ ひょ。価ーぐ中 │ │ │ │
│ │ 客テサ告覧ヨト │ │ │ │
│ │ ハ極整ラ得95稿 │ │ │ │
│ │ はかラせ江利ス │ │ │ │
│ │ 宏丸霊ミ考整ス │ │ │ │
│ │ 静将ず業巨職ノ │ │ │ │
│ │ ラホ収嗅ざな。 │ │ │ │
╰───────┴────────────────┴─────────────────┴─────────────────┴─────────────────╯ MissingRowContent.golden 0000664 0000000 0000000 00000002672 14764354362 0034345 0 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestContentWrapping_WithPadding ╭─────────┬────────────────────────────────────────┬──────┬──────────┬─────────╮
│ 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. │ │ │ │
╰─────────┴────────────────────────────────────────┴──────┴──────────┴─────────╯ charmbracelet-lipgloss-500b193/table/testdata/TestStyleFunc/ 0000775 0000000 0000000 00000000000 14764354362 0024077 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/table/testdata/TestStyleFunc/MarginAndPaddingSet.golden 0000664 0000000 0000000 00000007232 14764354362 0031100 0 ustar 00root root 0000000 0000000 ┌────────────┬────────────────┬─────────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├────────────┼────────────────┼─────────────┤
│ │ │ │
│ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │
│ [48;2;135;75;252m [0m[48;2;135;75;252m [0m[48;2;135;75;252mChinese[0m[48;2;135;75;252m [0m │ [48;2;135;75;252m [0m[48;2;135;75;252m [0m[48;2;135;75;252mNǐn hǎo[0m[48;2;135;75;252m [0m │ [48;2;135;75;252m [0m[48;2;135;75;252m [0m[48;2;135;75;252mNǐ hǎo[0m[48;2;135;75;252m [0m │
│ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │
│ │ │ │
│ │ │ │
│ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │
│ [48;2;135;75;252m [0m[48;2;135;75;252m [0m[48;2;135;75;252mFrench[0m[48;2;135;75;252m [0m │ [48;2;135;75;252m [0m[48;2;135;75;252m [0m[48;2;135;75;252mBonjour[0m[48;2;135;75;252m [0m │ [48;2;135;75;252m [0m[48;2;135;75;252m [0m[48;2;135;75;252mSalut[0m[48;2;135;75;252m [0m │
│ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │
│ │ │ │
│ │ │ │
│ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │
│ [48;2;135;75;252m [0m[48;2;135;75;252mJapanese[0m[48;2;135;75;252m [0m │ [48;2;135;75;252m [0m[48;2;135;75;252m [0m[48;2;135;75;252mこんにちは[0m[48;2;135;75;252m [0m │ [48;2;135;75;252m [0m[48;2;135;75;252m [0m[48;2;135;75;252mやあ[0m[48;2;135;75;252m [0m │
│ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │
│ │ │ │
│ │ │ │
│ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │
│ [48;2;135;75;252m [0m[48;2;135;75;252m [0m[48;2;135;75;252mRussian[0m[48;2;135;75;252m [0m │ [48;2;135;75;252m [0m[48;2;135;75;252mZdravstvuyte[0m[48;2;135;75;252m [0m │ [48;2;135;75;252m [0m[48;2;135;75;252m [0m[48;2;135;75;252mPrivet[0m[48;2;135;75;252m [0m │
│ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │
│ │ │ │
│ │ │ │
│ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │
│ [48;2;135;75;252m [0m[48;2;135;75;252m [0m[48;2;135;75;252mSpanish[0m[48;2;135;75;252m [0m │ [48;2;135;75;252m [0m[48;2;135;75;252m [0m[48;2;135;75;252mHola[0m[48;2;135;75;252m [0m │ [48;2;135;75;252m [0m[48;2;135;75;252m¿Qué tal?[0m[48;2;135;75;252m [0m │
│ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │ [48;2;135;75;252m [0m │
│ │ │ │
└────────────┴────────────────┴─────────────┘ charmbracelet-lipgloss-500b193/table/testdata/TestStyleFunc/RightAlignedTextWithMargins.golden 0000664 0000000 0000000 00000001216 14764354362 0032654 0 ustar 00root root 0000000 0000000 ┌──────────┬──────────────┬───────────┐
│ LANGUAGE │ FORMAL │ INFORMAL │
├──────────┼──────────────┼───────────┤
│ Chinese │ Nǐn hǎo │ Nǐ hǎo │
│ French │ Bonjour │ Salut │
│ Japanese │ こんにちは │ やあ │
│ Russian │ Zdravstvuyte │ Privet │
│ Spanish │ Hola │ ¿Qué tal? │
└──────────┴──────────────┴───────────┘ charmbracelet-lipgloss-500b193/table/testdata/TestTableOverFlowNoWrap.golden 0000664 0000000 0000000 00000002046 14764354362 0027221 0 ustar 00root root 0000000 0000000 ╭──────────────┬───────────────┬───────────────┬───────────────┬───────────────╮
│ Hello │ 你好 │ مرحبًا │ 안녕하세요 │ │
├──────────────┼───────────────┼───────────────┼───────────────┼───────────────┤
│ Lorem ipsum… │ 耐許ヱヨカハ… │ شيء قد للحكو… │ 각급 선거관… │ 版応道潟部中… │
│ … │ … │ … │ … │ … │
╰──────────────┴───────────────┴───────────────┴───────────────┴───────────────╯ charmbracelet-lipgloss-500b193/table/testdata/TestWrapPreStyledContent.golden 0000664 0000000 0000000 00000002524 14764354362 0027460 0 ustar 00root root 0000000 0000000 ╭─────────┬────────────────┬────────────────────────────────┬─────────────┬────╮
│Package │Version │Link │ │ │
├─────────┼────────────────┼────────────────────────────────┼─────────────┼────┤
│sourcegit│0.19 │[38;2;48;187;113mhttps://aur.archlinux.org/packag[m│ │ │
│ │ │[38;2;48;187;113mes/sourcegit-bin[0m │ │ │
│ │ │ │ │ │
│Welcome │いらっしゃいませ│مرحباً │환영 │欢迎│
│Goodbye │さようなら │مع السلامة │안녕히 가세요│再见│
╰─────────┴────────────────┴────────────────────────────────┴─────────────┴────╯ charmbracelet-lipgloss-500b193/table/testdata/TestWrapStyleFuncContent.golden 0000664 0000000 0000000 00000001700 14764354362 0027454 0 ustar 00root root 0000000 0000000 ╭─────────┬────────────────┬───────────────────────────────╮
│Package │Version │Link │
├─────────┼────────────────┼───────────────────────────────┤
│sourcegit│0.19 │[38;2;48;187;113mhttps://aur.archlinux.org/packa[0m│
│ │ │[38;2;48;187;113mges/sourcegit-bin[0m │
│Welcome │いらっしゃいませ│مرحباً │
│Goodbye │さようなら │مع السلامة │
╰─────────┴────────────────┴───────────────────────────────╯ charmbracelet-lipgloss-500b193/table/util.go 0000664 0000000 0000000 00000001455 14764354362 0021023 0 ustar 00root root 0000000 0000000 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
}
// max returns the greater of two integers.
func max(a, b int) int { //nolint:predeclared
if a > b {
return a
}
return b
}
// min returns the smaller of two integers.
func min(a, b int) int { //nolint:predeclared
if a < b {
return a
}
return b
}
// 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]
}
charmbracelet-lipgloss-500b193/tree/ 0000775 0000000 0000000 00000000000 14764354362 0017362 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/tree/children.go 0000664 0000000 0000000 00000004267 14764354362 0021512 0 ustar 00root root 0000000 0000000 package tree
// 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 = append(n[:index], n[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 := 0; i < m.data.Length(); i++ {
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 := 0; i < m.data.Length(); i++ {
if m.filter(i) {
j++
}
}
return j
}
charmbracelet-lipgloss-500b193/tree/enumerator.go 0000664 0000000 0000000 00000003326 14764354362 0022076 0 ustar 00root root 0000000 0000000 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 "│ "
}
charmbracelet-lipgloss-500b193/tree/example_test.go 0000664 0000000 0000000 00000005034 14764354362 0022405 0 ustar 00root root 0000000 0000000 package tree_test
import (
"fmt"
"github.com/charmbracelet/lipgloss/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())
}
charmbracelet-lipgloss-500b193/tree/renderer.go 0000664 0000000 0000000 00000006764 14764354362 0021534 0 ustar 00root root 0000000 0000000 package tree
import (
"strings"
"github.com/charmbracelet/lipgloss"
)
// 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
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)
},
itemFunc: func(Children, int) lipgloss.Style {
return lipgloss.NewStyle()
},
},
enumerator: DefaultEnumerator,
indenter: DefaultIndenter,
}
}
type renderer struct {
style Style
enumerator Enumerator
indenter Indenter
}
// render is responsible for actually rendering the tree.
func (r *renderer) render(node Node, root bool, prefix string) string {
if node.Hidden() {
return ""
}
var strs []string
var maxLen int
children := node.Children()
enumerator := r.enumerator
indenter := r.indenter
// print the root node name if its not empty.
if name := node.Value(); name != "" && root {
strs = append(strs, r.style.root.Render(name))
}
for i := 0; i < children.Length(); i++ {
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 := 0; i < children.Length(); i++ {
child := children.At(i)
if child.Hidden() {
continue
}
indent := indenter(children, i)
nodePrefix := enumerator(children, i)
enumStyle := r.style.enumeratorFunc(children, i)
itemStyle := r.style.itemFunc(children, i)
nodePrefix = enumStyle.Render(nodePrefix)
if l := maxLen - lipgloss.Width(nodePrefix); l > 0 {
nodePrefix = strings.Repeat(" ", l) + nodePrefix
}
item := itemStyle.Render(child.Value())
multineLinePrefix := 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,
enumStyle.Render(indent),
)
}
for lipgloss.Height(nodePrefix) > lipgloss.Height(multineLinePrefix) {
multineLinePrefix = lipgloss.JoinVertical(
lipgloss.Left,
multineLinePrefix,
prefix,
)
}
strs = append(
strs,
lipgloss.JoinHorizontal(
lipgloss.Top,
multineLinePrefix,
nodePrefix,
item,
),
)
if children.Length() > 0 {
// here we see if the child has a custom renderer, which means the
// user set a custom enumerator, style, etc.
// if it has one, we'll use it to render itself.
// otherwise, we keep using the current renderer.
renderer := r
switch child := child.(type) {
case *Tree:
if child.r != nil {
renderer = child.r
}
}
if s := renderer.render(
child,
false,
prefix+enumStyle.Render(indent),
); s != "" {
strs = append(strs, s)
}
}
}
return strings.Join(strs, "\n")
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
charmbracelet-lipgloss-500b193/tree/testdata/ 0000775 0000000 0000000 00000000000 14764354362 0021173 5 ustar 00root root 0000000 0000000 charmbracelet-lipgloss-500b193/tree/testdata/TestRootStyle.golden 0000664 0000000 0000000 00000000137 14764354362 0025172 0 ustar 00root root 0000000 0000000 [48;2;89;86;224mRoot[0m
├── [48;2;4;181;117mFoo[0m
└── [48;2;4;181;117mBaz[0m charmbracelet-lipgloss-500b193/tree/tree.go 0000664 0000000 0000000 00000021004 14764354362 0020645 0 ustar 00root root 0000000 0000000 // 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"
"github.com/charmbracelet/lipgloss"
)
// 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.
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.
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 it's 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 := 0; i < item.Length(); i++ {
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 := 0; i < item.Children().Length(); i++ {
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
}
// 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
}
// 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),
}
}
charmbracelet-lipgloss-500b193/tree/tree_test.go 0000664 0000000 0000000 00000031226 14764354362 0021713 0 ustar 00root root 0000000 0000000 package tree_test
import (
"strings"
"testing"
"unicode"
"github.com/aymanbagabas/go-udiff"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/list"
"github.com/charmbracelet/lipgloss/table"
"github.com/charmbracelet/lipgloss/tree"
"github.com/charmbracelet/x/exp/golden"
"github.com/muesli/termenv"
)
func TestTree(t *testing.T) {
tr := tree.New().
Child(
"Foo",
tree.Root("Bar").
Child(
"Qux",
tree.Root("Quux").
Child(
"Foo",
"Bar",
),
"Quuux",
),
"Baz",
)
want := `
├── Foo
├── Bar
│ ├── Qux
│ ├── Quux
│ │ ├── Foo
│ │ └── Bar
│ └── Quuux
└── Baz
`
assertEqual(t, want, tr.String())
tr.Enumerator(tree.RoundedEnumerator)
want = `
├── Foo
├── Bar
│ ├── Qux
│ ├── Quux
│ │ ├── Foo
│ │ ╰── Bar
│ ╰── Quuux
╰── Baz
`
assertEqual(t, want, 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",
)
want := `
├── Foo
├── Bar
│ ├── Qux
│ └── Quuux
└── Baz
`
assertEqual(t, want, 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)
want := ``
assertEqual(t, want, tree.String())
}
func TestTreeRoot(t *testing.T) {
tree := tree.New().
Root("Root").
Child(
"Foo",
tree.Root("Bar").
Child("Qux", "Quuux"),
"Baz",
)
want := `
Root
├── Foo
├── Bar
│ ├── Qux
│ └── Quuux
└── Baz
`
assertEqual(t, want, tree.String())
}
func TestTreeStartsWithSubtree(t *testing.T) {
tree := tree.New().
Child(
tree.New().
Root("Bar").
Child("Qux", "Quuux"),
"Baz",
)
want := `
├── Bar
│ ├── Qux
│ └── Quuux
└── Baz
`
assertEqual(t, want, 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",
)
want := `
├── Bar
├── Foo
│ ├── Qux
│ ├── Qux
│ ├── Qux
│ ├── Qux
│ ├── Qux
│ ├── Quux
│ ├── Quux
│ ├── Quux
│ ├── Quux
│ └── Quux
└── Baz
`
assertEqual(t, want, tree.String())
}
func TestTreeLastNodeIsSubTree(t *testing.T) {
tree := tree.New().
Child(
"Foo",
tree.Root("Bar").
Child("Qux",
tree.Root("Quux").Child("Foo", "Bar"),
"Quuux",
),
)
want := `
├── Foo
└── Bar
├── Qux
├── Quux
│ ├── Foo
│ └── Bar
└── Quuux
`
assertEqual(t, want, tree.String())
}
func TestTreeNil(t *testing.T) {
tree := tree.New().
Child(
nil,
tree.Root("Bar").
Child(
"Qux",
tree.Root("Quux").
Child("Bar"),
"Quuux",
),
"Baz",
)
want := `
├── Bar
│ ├── Qux
│ ├── Quux
│ │ └── Bar
│ └── Quuux
└── Baz
`
assertEqual(t, want, 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)).
Enumerator(func(tree.Children, int) string {
return "->"
}).
Indenter(func(tree.Children, int) string {
return "->"
})
want := `
-> Foo
-> Bar
-> -> Qux
-> -> Quux
-> -> -> Foo
-> -> -> Bar
-> -> Quuux
-> Baz
`
assertEqual(t, want, 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",
)
want := `
Big
Root
Node
├── Foo
├── Bar
│ ├── Line 1
│ │ Line 2
│ │ Line 3
│ │ Line 4
│ ├── Quux
│ │ ├── Foo
│ │ └── Bar
│ └── Quuux
└── Baz
Line 2
`
assertEqual(t, want, 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",
)
want := `
The Root Node™
├── Parent
│ + ├── * child 1
│ + └── * child 2
└── Baz
`
assertEqual(t, want, 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]
})
want := `
The Root Node™
I Foo
II Foo
III Foo
IV Foo
V Foo
`
assertEqual(t, want, tree.String())
}
func TestTreeStyleNilFuncs(t *testing.T) {
tree := tree.New().
Root("Silly").
Child("Willy ", "Nilly").
ItemStyleFunc(nil).
EnumeratorStyleFunc(nil)
want := `
Silly
├──Willy
└──Nilly
`
assertEqual(t, want, 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 "-"
})
want := `
Root
> Foo
- Baz
`
assertEqual(t, want, tree.String())
}
func TestRootStyle(t *testing.T) {
lipgloss.SetColorProfile(termenv.TrueColor)
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(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)
want := `
Root
├── Foo
├── Bar
└── Baz
`
assertEqual(t, want, 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",
)
want := `
├── Foo
├── Bar
│ ├── Baz
│ ├── Baz
│ ├── ╭─────────┬────────╮
│ │ │ Foo │ Bar │
│ │ ├─────────┼────────┤
│ │ │ Qux │ Baz │
│ │ │ Qux │ Baz │
│ │ ╰─────────┴────────╯
│ └── Baz
└── Qux
`
assertEqual(t, want, tree.String())
}
func TestAddItemWithAndWithoutRoot(t *testing.T) {
t1 := tree.New().
Child(
"Foo",
"Bar",
tree.New().
Child("Baz"),
"Qux",
)
t2 := tree.New().
Child(
"Foo",
tree.New().
Root("Bar").
Child("Baz"),
"Qux",
)
want := `
├── Foo
├── Bar
│ └── Baz
└── Qux
`
assertEqual(t, want, t1.String())
assertEqual(t, want, 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))
want := `
├── 1. A
│ 2. B
│ 3. C
└── A. 1
B. 2
C. 3
`
assertEqual(t, want, 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")
want := `
Foo Document
The Foo Files
│ Bar Document
│ The Bar Files
Baz Document
The Baz Files
`
assertEqual(t, want, 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")
want := `
├── Foo
├── Bar
├── Baz
│ Foo Document
│ The Foo Files
│
│ │ Bar Document
│ │ The Bar Files
│
│ Baz Document
│ The Baz Files
│
└── Qux
`
assertEqual(t, want, 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")
want := `
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
`
assertEqual(t, want, tree.String())
}
func TestTypes(t *testing.T) {
tree := tree.New().
Child(0).
Child(true).
Child([]any{"Foo", "Bar"}).
Child([]string{"Qux", "Quux", "Quuux"})
want := `
├── 0
├── true
├── Foo
├── Bar
├── Qux
├── Quux
└── Quuux
`
assertEqual(t, want, tree.String())
}
// assertEqual verifies the strings are equal, assuming its terminal output.
func assertEqual(tb testing.TB, want, got string) {
tb.Helper()
want = trimSpace(want)
got = trimSpace(got)
diff := udiff.Unified("want", "got", want, got)
if diff != "" {
tb.Fatalf("\nwant:\n\n%s\n\ngot:\n\n%s\n\ndiff:\n\n%s\n\n", want, got, 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")
}
charmbracelet-lipgloss-500b193/unset.go 0000664 0000000 0000000 00000017770 14764354362 0020124 0 ustar 00root root 0000000 0000000 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 {
s.unset(underlineKey)
return s
}
// 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)
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
}
// 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
}
// UnsetString sets the underlying string value to the empty string.
func (s Style) UnsetString() Style {
s.value = ""
return s
}
charmbracelet-lipgloss-500b193/whitespace.go 0000664 0000000 0000000 00000003711 14764354362 0021110 0 ustar 00root root 0000000 0000000 package lipgloss
import (
"strings"
"github.com/charmbracelet/x/ansi"
"github.com/muesli/termenv"
)
// whitespace is a whitespace renderer.
type whitespace struct {
re *Renderer
style termenv.Style
chars string
}
// newWhitespace creates a new whitespace renderer. The order of the options
// matters, if you're using WithWhitespaceRenderer, make sure it comes first as
// other options might depend on it.
func newWhitespace(r *Renderer, opts ...WhitespaceOption) *whitespace {
w := &whitespace{
re: r,
style: r.ColorProfile().String(),
}
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])
j++
if j >= len(r) {
j = 0
}
i += ansi.StringWidth(string(r[j]))
}
// 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.Styled(b.String())
}
// WhitespaceOption sets a styling rule for rendering whitespace.
type WhitespaceOption func(*whitespace)
// WithWhitespaceForeground sets the color of the characters in the whitespace.
func WithWhitespaceForeground(c TerminalColor) WhitespaceOption {
return func(w *whitespace) {
w.style = w.style.Foreground(c.color(w.re))
}
}
// WithWhitespaceBackground sets the background color of the whitespace.
func WithWhitespaceBackground(c TerminalColor) WhitespaceOption {
return func(w *whitespace) {
w.style = w.style.Background(c.color(w.re))
}
}
// WithWhitespaceChars sets the characters to be rendered in the whitespace.
func WithWhitespaceChars(s string) WhitespaceOption {
return func(w *whitespace) {
w.chars = s
}
}