Skip to content
18 changes: 18 additions & 0 deletions tree/children.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package tree

import "slices"

// Children is the interface that wraps the basic methods of a tree model.
type Children interface {
// At returns the content item of the given index.
Expand All @@ -18,6 +20,22 @@ func (n NodeChildren) Append(child Node) NodeChildren {
return n
}

// Insert inserts a child to the list at the given index.
func (n NodeChildren) Insert(index int, child Node) NodeChildren {
if index < 0 || len(n) < index+1 {
return n
}
return slices.Insert(n, index, child)
}

// Replace swaps the child at the given index with the given child.
func (n NodeChildren) Replace(index int, child Node) NodeChildren {
if index < 0 || len(n) < index+1 {
return n
}
return slices.Replace(n, index, index+1, child)
}

// 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 {
Expand Down
153 changes: 153 additions & 0 deletions tree/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package tree_test

import (
"fmt"

"github.com/charmbracelet/lipgloss/tree"
"github.com/charmbracelet/x/ansi"
)

func ExampleLeaf_Insert() {
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)
// Adds a new Tree Node to a Leaf (Mac).
t.Replace(3, t.Children().At(3).Insert(0, "Glow Play Cushion Blush"))
fmt.Println(ansi.Strip(t.String()))
// Output:
//⁜ Makeup
//├── Glossier
//├── Fenty Beauty
//│ ├── Gloss Bomb Universal Lip Luminizer
//│ ╰── Hot Cheeks Velour Blushlighter
//├── Nyx
//├── Mac
//│ ╰── Glow Play Cushion Blush
//╰── Milk
}

func ExampleLeaf_Replace() {
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)
// Add Glow Play Cushion Blush to Mac Leaf.
t.Replace(3, t.Children().At(3).Replace(0, "Glow Play Cushion Blush"))
fmt.Println(ansi.Strip(t.String()))
// Output:
//⁜ Makeup
//├── Glossier
//├── Fenty Beauty
//│ ├── Gloss Bomb Universal Lip Luminizer
//│ ╰── Hot Cheeks Velour Blushlighter
//├── Nyx
//├── Mac
//│ ╰── Glow Play Cushion Blush
//╰── Milk
}

// Tree Examples

func ExampleTree_Replace() {
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)
// Add a Tree as a Child of "Glossier". At this stage "Glossier" is a Leaf,
// so we re-assign the value of "Glossier" in the "Makeup" Tree to its new
// Tree value returned from Child().
t.Replace(0, t.Children().At(0).Child(
tree.Root("Apparel").Child("Pink Hoodie", "Baseball Cap"),
))

// Add a Leaf as a Child of "Glossier". At this stage "Glossier" is a Tree,
// so we don't need to use [Tree.Replace] on the parent tree.
t.Children().At(0).Child("Makeup")
fmt.Println(ansi.Strip(t.String()))
// Output:
// ⁜ Makeup
// ├── Glossier
// │ ├── Apparel
// │ │ ├── Pink Hoodie
// │ │ ╰── Baseball Cap
// │ ╰── Makeup
// ├── Fenty Beauty
// │ ├── Gloss Bomb Universal Lip Luminizer
// │ ╰── Hot Cheeks Velour Blushlighter
// ├── Nyx
// ├── Mac
// ╰── Milk
//
}

func ExampleTree_Insert() {
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)
// Adds a new Tree Node after Fenty Beauty.
t.Insert(2, tree.Root("Lancôme").Child("Juicy Tubes Lip Gloss", "Lash Idôle", "Teint Idôle Highlighter"))

// Adds a new Tree Node in Fenty Beauty
t.Replace(1, t.Children().At(1).Insert(0, "Blurring Skin Tint"))

// Adds a new Tree Node to a Leaf (Mac)
t.Replace(4, t.Children().At(4).Insert(0, "Glow Play Cushion Blush"))
fmt.Println(ansi.Strip(t.String()))
// Output:
//⁜ Makeup
//├── Glossier
//├── Fenty Beauty
//│ ├── Blurring Skin Tint
//│ ├── Gloss Bomb Universal Lip Luminizer
//│ ╰── Hot Cheeks Velour Blushlighter
//├── Lancôme
//│ ├── Juicy Tubes Lip Gloss
//│ ├── Lash Idôle
//│ ╰── Teint Idôle Highlighter
//├── Nyx
//├── Mac
//│ ╰── Glow Play Cushion Blush
//╰── Milk
}
12 changes: 12 additions & 0 deletions tree/testdata/TestInheritedStyles.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
⁜ Makeup
├── Glossier
│  ├── Apparel
│  │  ├── Pink Hoodie
│  │  ╰── Baseball Cap
│  ╰── Makeup
├── Fenty Beauty
│  ├── Gloss Bomb Universal Lip Luminizer
│  ╰── Hot Cheeks Velour Blushlighter
├── Nyx
├── Mac
╰── Milk
97 changes: 92 additions & 5 deletions tree/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
Value() string
Children() Children
Hidden() bool
Child(...any) *Tree
SetChildren(...any) *Tree
Insert(int, any) *Tree
Replace(int, any) *Tree
}

// Leaf is a node without children.
Expand All @@ -56,6 +60,35 @@
return s.value
}

// Child adds a child to this Tree, converting a Leaf to a Tree in the process.
func (s *Leaf) Child(children ...any) *Tree {
t := &Tree{
value: s.value,
hidden: s.hidden,
children: NodeChildren(nil),
}
return t.Child(children)
}

// SetChildren turns the Leaf into a Tree with the given children.
func (s *Leaf) SetChildren(children ...any) *Tree {
return s.Child(children)
}

// Replace turns the Leaf into a Tree with the given child. Because of the type
// change, you'll need to reassign the result of this function to the value with
// [Tree.Replace].
func (s *Leaf) Replace(_ int, child any) *Tree {
return s.Child(child)
}

// Insert turns the Leaf into a Tree with the given child. Because of the type
// change, you'll need to reassign the result of this function to the value with
// [Tree.Replace].
func (s *Leaf) Insert(_ int, child any) *Tree {
return s.Child(child)
}

// Hidden returns whether a Leaf node is hidden.
func (s Leaf) Hidden() bool {
return s.hidden
Expand Down Expand Up @@ -118,7 +151,64 @@
return t.ensureRenderer().render(t, true, "")
}

// Child adds a child to this tree.
// SetChildren overwrites a Tree's Children.
func (t *Tree) SetChildren(children ...any) *Tree {
t.children = NodeChildren(nil)
return t.Child(children)
}

// Replace swaps the child at the given index with the given child.
func (t *Tree) Replace(index int, child any) *Tree {
nodes := t.anyToNode(child)
t.children = t.children.(NodeChildren).Replace(index, nodes[0])
return t
}

// Insert child at the given index.
func (t *Tree) Insert(index int, child any) *Tree {
nodes := t.anyToNode(child)
t.children = t.children.(NodeChildren).Insert(index, nodes[0])
return t
}

// TODO probably don't need this to be an []any

Check failure on line 174 in tree/tree.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (macos-latest)

Comment should end in a period (godot)

Check failure on line 174 in tree/tree.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (macos-latest)

Comment should end in a period (godot)
func (t *Tree) anyToNode(children ...any) []Node {
var nodes []Node
for _, child := range children {
switch item := child.(type) {
case *Tree:
child, _ := child.(*Tree)
nodes = append(nodes, child)
case Children:
for i := 0; i < item.Length(); i++ {
nodes = append(nodes, item.At(i))
}
case Node:
nodes = append(nodes, item)
case fmt.Stringer:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful to support encoding.TextMarshaler as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have this in other type checks in tree, maybe something we can add down the line

s := Leaf{value: item.String()}
nodes = append(nodes, &s)
case string:
s := Leaf{value: item}
nodes = append(nodes, &s)
case []any:
return t.anyToNode(item...)
case []string:
ss := make([]any, 0, len(item))
for _, s := range item {
ss = append(ss, s)
}
return t.anyToNode(ss...)
case nil:
continue
default:
return t.anyToNode(fmt.Sprintf("%v", item))
}
}
return nodes
}

// 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).
Expand Down Expand Up @@ -147,7 +237,7 @@
t.children = t.children.(NodeChildren).Append(item)
case fmt.Stringer:
s := Leaf{value: item.String()}
t.children = t.children.(NodeChildren).Append(s)
t.children = t.children.(NodeChildren).Append(&s)
case string:
s := Leaf{value: item}
t.children = t.children.(NodeChildren).Append(&s)
Expand Down Expand Up @@ -180,9 +270,6 @@
parent.Child(item.children.At(i))
}
return parent, j
case Leaf:
item.value = parent.Value()
return item, j
case *Leaf:
item.value = parent.Value()
return item, j
Expand Down
32 changes: 32 additions & 0 deletions tree/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,38 @@ func TestRootStyle(t *testing.T) {
golden.RequireEqual(t, []byte(tree.String()))
}

func TestInheritedStyles(t *testing.T) {
enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1)
rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35"))
itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212"))

tr := 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)
// Add a Tree as a Child of "Glossier"
tr.Replace(0, tr.Children().At(0).Child(
tree.Root("Apparel").Child("Pink Hoodie", "Baseball Cap"),
))

// Add a Leaf as a Child of "Glossier"
tr.Children().At(0).Child("Makeup")
golden.RequireEqual(t, []byte(tr.String()))
}

func TestAt(t *testing.T) {
data := tree.NewStringData("Foo", "Bar")

Expand Down
Loading