mirror of
https://github.com/charmbracelet/lipgloss.git
synced 2024-10-05 18:28:00 +03:00
feb42a9be4
* feat: support any type for list Root func Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * feat: move tree to root Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * feat: more cleanup Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * docs(examples): bring back tree examples * docs(examples): add tree toggle example * docs(examples): fix toggle tree hierarchy; as bashbunni metadata * fix(tree): stylize whitespace when vertically joining tree elements * docs: get started with trees * feat: add RootStyle with example * test: add test for RootStyle func * docs(godoc): replace list mentions in tree package * refactor(examples): use Root shorthand instead of tree.New where applicable --------- Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Co-authored-by: bashbunni <bunni@bashbunni.dev> Co-authored-by: Christian Rocha <christian@rocha.is>
732 lines
13 KiB
Go
732 lines
13 KiB
Go
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")
|
|
}
|