Repository: pterm/pterm (go)

Issue:
Add `TreeListPrinter`
Add a tree list printer like `BulletListPrinter`.
Style: like `tree` in cmd 

Pull request:
Feat/add tree list
### Description
Added a tree list printer.

### Scope
> What is affected by this pull request?

- [ ] Bug Fix
- [x] New Feature
- [x] Documentation
- [ ] Other

### Related Issue
Fixes #89 


### To-Do Checklist
- [x] I tested my changes
- [x] I have commented every method that I created/changed
- [x] I updated the examples to fit with my changes
- [x] I have added tests for my newly created methods


Patch:
diff --git a/README.md b/README.md
index 1469efac..9380dcf6 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,7 @@
 - [Section](#section)
 - [Spinner](#spinner)
 - [Header](#header)
+- [Tree](#tree)
 - [Paragraph](#paragraph)
 - [Print With Color](#print-with-color)
   - [Print With RGB Color](#print-color-rgb)
diff --git a/_examples/tree/main.go b/_examples/tree/main.go
new file mode 100644
index 00000000..a2ee3a65
--- /dev/null
+++ b/_examples/tree/main.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+	"github.com/pterm/pterm"
+)
+
+func main() {
+	// You can use a LeveledList here, for easy generation.
+	leveledList := pterm.LeveledList{
+		pterm.LeveledListItem{Level: 0, Text: "C:"},
+		pterm.LeveledListItem{Level: 1, Text: "User"},
+		pterm.LeveledListItem{Level: 1, Text: "log"},
+		pterm.LeveledListItem{Level: 1, Text: "Programs"},
+		pterm.LeveledListItem{Level: 1, Text: "Programs(x86)"},
+		pterm.LeveledListItem{Level: 1, Text: "dev"},
+		pterm.LeveledListItem{Level: 0, Text: "D:"},
+		pterm.LeveledListItem{Level: 0, Text: "E:"},
+		pterm.LeveledListItem{Level: 1, Text: "Movies"},
+		pterm.LeveledListItem{Level: 1, Text: "Music"},
+		pterm.LeveledListItem{Level: 2, Text: "LinkinPark"},
+		pterm.LeveledListItem{Level: 1, Text: "Games"},
+		pterm.LeveledListItem{Level: 2, Text: "Shooter"},
+		pterm.LeveledListItem{Level: 3, Text: "CallOfDuty"},
+		pterm.LeveledListItem{Level: 3, Text: "CS:GO"},
+		pterm.LeveledListItem{Level: 3, Text: "Battlefield"},
+		pterm.LeveledListItem{Level: 4, Text: "Battlefield 1"},
+		pterm.LeveledListItem{Level: 4, Text: "Battlefield 2"},
+		pterm.LeveledListItem{Level: 0, Text: "F:"},
+		pterm.LeveledListItem{Level: 1, Text: "dev"},
+		pterm.LeveledListItem{Level: 2, Text: "dops"},
+		pterm.LeveledListItem{Level: 2, Text: "PTerm"},
+	}
+
+	// Generate tree from LeveledList.
+	root := pterm.NewTreeFromLeveledList(leveledList)
+
+	// Render Tree
+	pterm.DefaultTree.WithRoot(root).Render()
+}
diff --git a/bulletlist_printer.go b/bulletlist_printer.go
index e0089ea9..38b02324 100644
--- a/bulletlist_printer.go
+++ b/bulletlist_printer.go
@@ -15,7 +15,7 @@ func NewBulletListFromStrings(s []string, padding string) BulletList {
 	return *DefaultBulletList.WithItems(lis)
 }
 
-// NewBulletListItemFromString returns a ListItem with a Text. The padding is counted in the Text to define the Level of the ListItem.
+// NewBulletListItemFromString returns a BulletListItem with a Text. The padding is counted in the Text to define the Level of the ListItem.
 func NewBulletListItemFromString(text string, padding string) BulletListItem {
 	s, l := internal.RemoveAndCountPrefix(text, padding)
 	return BulletListItem{
@@ -40,42 +40,42 @@ type BulletListItem struct {
 	BulletStyle *Style
 }
 
-// WithLevel returns a new ListItem with a specific Level.
+// WithLevel returns a new BulletListItem with a specific Level.
 func (p BulletListItem) WithLevel(level int) BulletListItem {
 	p.Level = level
 	return p
 }
 
-// WithText returns a new ListItem with a specific Text.
+// WithText returns a new BulletListItem with a specific Text.
 func (p BulletListItem) WithText(text string) BulletListItem {
 	p.Text = text
 	return p
 }
 
-// WithTextStyle returns a new ListItem with a specific TextStyle.
+// WithTextStyle returns a new BulletListItem with a specific TextStyle.
 func (p BulletListItem) WithTextStyle(style *Style) BulletListItem {
 	p.TextStyle = style
 	return p
 }
 
-// WithBullet returns a new ListItem with a specific Prefix.
+// WithBullet returns a new BulletListItem with a specific Prefix.
 func (p BulletListItem) WithBullet(bullet string) BulletListItem {
 	p.Bullet = bullet
 	return p
 }
 
-// WithBulletStyle returns a new ListItem with a specific BulletStyle.
+// WithBulletStyle returns a new BulletListItem with a specific BulletStyle.
 func (p BulletListItem) WithBulletStyle(style *Style) BulletListItem {
 	p.BulletStyle = style
 	return p
 }
 
-// Render renders the ListItem as a string.
+// Render renders the BulletListItem as a string.
 func (p BulletListItem) Render() {
 	Println(p.Srender())
 }
 
-// Srender renders the ListItem as a string.
+// Srender renders the BulletListItem as a string.
 func (p BulletListItem) Srender() string {
 	if p.TextStyle == nil {
 		p.TextStyle = NewStyle()
diff --git a/theme.go b/theme.go
index 8f71952e..979e4367 100644
--- a/theme.go
+++ b/theme.go
@@ -32,6 +32,8 @@ var (
 		SectionStyle:            Style{Bold, FgYellow},
 		ListTextStyle:           Style{FgWhite},
 		ListBulletStyle:         Style{FgGray},
+		TreeStyle:               Style{FgGray},
+		TreeTextStyle:           Style{FgDefault},
 		LetterStyle:             Style{FgDefault},
 		DebugMessageStyle:       Style{FgGray},
 		DebugPrefixStyle:        Style{FgBlack, BgGray},
@@ -70,6 +72,8 @@ type Theme struct {
 	SectionStyle            Style
 	ListTextStyle           Style
 	ListBulletStyle         Style
+	TreeStyle               Style
+	TreeTextStyle           Style
 	LetterStyle             Style
 	DebugMessageStyle       Style
 	DebugPrefixStyle        Style
@@ -194,3 +198,15 @@ func (t Theme) WithDebugPrefixStyle(style Style) Theme {
 	t.DebugPrefixStyle = style
 	return t
 }
+
+// WithTreeStyle returns a new theme with overridden value.
+func (t Theme) WithTreeStyle(style Style) Theme {
+	t.TreeStyle = style
+	return t
+}
+
+// WithTreeTextStyle returns a new theme with overridden value.
+func (t Theme) WithTreeTextStyle(style Style) Theme {
+	t.TreeTextStyle = style
+	return t
+}
diff --git a/tree_printer.go b/tree_printer.go
new file mode 100644
index 00000000..879db7e1
--- /dev/null
+++ b/tree_printer.go
@@ -0,0 +1,162 @@
+package pterm
+
+// TreeNode is used as items in a Tree.
+type TreeNode struct {
+	Children []TreeNode
+	Text     string
+}
+
+// LeveledList is a list, which contains multiple LeveledListItem.
+type LeveledList []LeveledListItem
+
+// LeveledListItem combines a text with a specific level.
+// The level is the indent, which would normally be seen in a BulletList.
+type LeveledListItem struct {
+	Level int
+	Text  string
+}
+
+// DefaultTree contains standards, which can be used to render a Tree.
+var DefaultTree = Tree{
+	TreeStyle:            &ThemeDefault.TreeStyle,
+	TextStyle:            &ThemeDefault.TreeTextStyle,
+	TopRightCornerString: "└",
+	HorizontalString:     "─",
+	TopRightDownString:   "├",
+	VerticalString:       "│",
+	RightDownLeftString:  "┬",
+}
+
+// Tree is able to render a list.
+type Tree struct {
+	Root                 TreeNode
+	TreeStyle            *Style
+	TextStyle            *Style
+	TopRightCornerString string
+	TopRightDownString   string
+	HorizontalString     string
+	VerticalString       string
+	RightDownLeftString  string
+}
+
+// WithTreeStyle returns a new list with a specific tree style.
+func (p Tree) WithTreeStyle(style *Style) *Tree {
+	p.TreeStyle = style
+	return &p
+}
+
+// WithTextStyle returns a new list with a specific text style.
+func (p Tree) WithTextStyle(style *Style) *Tree {
+	p.TextStyle = style
+	return &p
+}
+
+// WithTopRightCornerString returns a new list with a specific TopRightCornerString.
+func (p Tree) WithTopRightCornerString(s string) *Tree {
+	p.TopRightCornerString = s
+	return &p
+}
+
+// WithTopRightDownStringOngoing returns a new list with a specific TopRightDownString.
+func (p Tree) WithTopRightDownStringOngoing(s string) *Tree {
+	p.TopRightDownString = s
+	return &p
+}
+
+// WithHorizontalString returns a new list with a specific HorizontalString.
+func (p Tree) WithHorizontalString(s string) *Tree {
+	p.HorizontalString = s
+	return &p
+}
+
+// WithVerticalString returns a new list with a specific VerticalString.
+func (p Tree) WithVerticalString(s string) *Tree {
+	p.VerticalString = s
+	return &p
+}
+
+// WithRoot returns a new list with a specific Root.
+func (p Tree) WithRoot(root TreeNode) *Tree {
+	p.Root = root
+	return &p
+}
+
+// Render prints the list to the terminal.
+func (p Tree) Render() {
+	Println(p.Srender())
+}
+
+// Srender renders the list as a string.
+func (p Tree) Srender() string {
+	if p.TreeStyle == nil {
+		p.TreeStyle = NewStyle()
+	}
+	if p.TextStyle == nil {
+		p.TextStyle = NewStyle()
+	}
+
+	return walkOverTree(p.Root.Children, p, "")
+}
+
+// walkOverTree is a recursive function,
+// which analyzes a Tree and connects the items with specific characters.
+// Returns Tree as string.
+func walkOverTree(list []TreeNode, p Tree, prefix string) string {
+	var ret string
+	for i, item := range list {
+		if len(list) > i+1 { // if not last in list
+			if len(item.Children) == 0 { // if there are no children
+				ret += prefix + p.TreeStyle.Sprint(p.TopRightDownString) + p.TreeStyle.Sprint(p.HorizontalString) + p.TextStyle.Sprint(item.Text) + "\n"
+			} else { // if there are children
+				ret += prefix + p.TreeStyle.Sprint(p.TopRightDownString) + p.TreeStyle.Sprint(p.RightDownLeftString) + p.TextStyle.Sprint(item.Text) + "\n"
+				ret += walkOverTree(item.Children, p, prefix+p.TreeStyle.Sprint(p.VerticalString))
+			}
+		} else if len(list) == i+1 { // if last in list
+			if len(item.Children) == 0 { // if there are no children
+				ret += prefix + p.TreeStyle.Sprint(p.TopRightCornerString) + p.TreeStyle.Sprint(p.HorizontalString) + p.TextStyle.Sprint(item.Text) + "\n"
+			} else { // if there are children
+				ret += prefix + p.TreeStyle.Sprint(p.TopRightCornerString) + p.TreeStyle.Sprint(p.RightDownLeftString) + p.TextStyle.Sprint(item.Text) + "\n"
+				ret += walkOverTree(item.Children, p, prefix+" ")
+			}
+		}
+	}
+	return ret
+}
+
+// NewTreeFromLeveledList converts a TreeItems list to a TreeNode and returns it.
+func NewTreeFromLeveledList(leveledListItems LeveledList) TreeNode {
+	if len(leveledListItems) == 0 {
+		return TreeNode{}
+	}
+
+	root := &TreeNode{
+		Children: []TreeNode{},
+		Text:     leveledListItems[0].Text,
+	}
+
+	for i, record := range leveledListItems {
+		last := root
+
+		if record.Level < 0 {
+			record.Level = 0
+			leveledListItems[i].Level = 0
+		}
+
+		if len(leveledListItems)-1 != i {
+			if leveledListItems[i+1].Level-1 > record.Level {
+				leveledListItems[i+1].Level = record.Level + 1
+			}
+		}
+
+		for i := 0; i < record.Level; i++ {
+			lastIndex := len(last.Children) - 1
+			last = &last.Children[lastIndex]
+		}
+		last.Children = append(last.Children, TreeNode{
+			Children: []TreeNode{},
+			Text:     record.Text,
+		})
+	}
+
+	return *root
+}


Test patch:
diff --git a/theme_test.go b/theme_test.go
index f6100b33..cb056fd5 100644
--- a/theme_test.go
+++ b/theme_test.go
@@ -164,3 +164,19 @@ func TestTheme_WithDebugPrefixStyle(t *testing.T) {
 
 	assert.Equal(t, s, p2.DebugPrefixStyle)
 }
+
+func TestTheme_WithTreeStyle(t *testing.T) {
+	s := Style{FgRed, BgBlue, Bold}
+	p := Theme{}
+	p2 := p.WithTreeStyle(s)
+
+	assert.Equal(t, s, p2.TreeStyle)
+}
+
+func TestTheme_WithTreeTextStyle(t *testing.T) {
+	s := Style{FgRed, BgBlue, Bold}
+	p := Theme{}
+	p2 := p.WithTreeTextStyle(s)
+
+	assert.Equal(t, s, p2.TreeTextStyle)
+}
diff --git a/tree_printer_test.go b/tree_printer_test.go
new file mode 100644
index 00000000..b3e681ab
--- /dev/null
+++ b/tree_printer_test.go
@@ -0,0 +1,184 @@
+package pterm
+
+import (
+	"fmt"
+	"io"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/pterm/pterm/internal"
+)
+
+func TestTreePrinterNilPrint(t *testing.T) {
+	proxyToDevNull()
+	Tree{}.Render()
+	Tree{}.WithRoot(NewTreeFromLeveledList(LeveledList{LeveledListItem{Text: "Hello, World!", Level: 0}})).Render()
+}
+
+func TestTree_Render(t *testing.T) {
+	internal.TestPrintContains(t, func(w io.Writer, a interface{}) {
+		DefaultTree.WithRoot(NewTreeFromLeveledList([]LeveledListItem{
+			{Level: 0, Text: fmt.Sprint(a)},
+			{Level: 0, Text: "0.0"},
+			{Level: 1, Text: "0.1"},
+			{Level: 1, Text: "0.2"},
+			{Level: 0, Text: "1.0"},
+			{Level: 0, Text: "2.0"},
+			{Level: 1, Text: "2.1"},
+			{Level: 1, Text: "2.2"},
+			{Level: 2, Text: "2.2.1"},
+			{Level: 1, Text: "2.3"},
+		})).Render()
+	})
+}
+
+func TestTree_NewTreeFromLeveledList(t *testing.T) {
+	p := DefaultTree
+	p2 := p.WithRoot(NewTreeFromLeveledList(LeveledList{
+		LeveledListItem{Level: 0, Text: "0.0"},
+		LeveledListItem{Level: 1, Text: "0.1"},
+		LeveledListItem{Level: 1, Text: "0.2"},
+		LeveledListItem{Level: 0, Text: "1.0"},
+		LeveledListItem{Level: 0, Text: "2.0"},
+		LeveledListItem{Level: 1, Text: "2.1"},
+		LeveledListItem{Level: 1, Text: "2.2"},
+		LeveledListItem{Level: 2, Text: "2.2.1"},
+		LeveledListItem{Level: 1, Text: "2.3"}}))
+
+	assert.Equal(t, NewTreeFromLeveledList(LeveledList{
+		LeveledListItem{Level: 0, Text: "0.0"},
+		LeveledListItem{Level: 1, Text: "0.1"},
+		LeveledListItem{Level: 1, Text: "0.2"},
+		LeveledListItem{Level: 0, Text: "1.0"},
+		LeveledListItem{Level: 0, Text: "2.0"},
+		LeveledListItem{Level: 1, Text: "2.1"},
+		LeveledListItem{Level: 1, Text: "2.2"},
+		LeveledListItem{Level: 2, Text: "2.2.1"},
+		LeveledListItem{Level: 1, Text: "2.3"}}), p2.Root)
+	assert.Empty(t, p.Root)
+}
+
+func TestTree_NewTreeFromLeveledListLevelInvalidIncrease(t *testing.T) {
+	p := DefaultTree
+	p2 := p.WithRoot(NewTreeFromLeveledList(LeveledList{
+		LeveledListItem{Level: 0, Text: "0.0"},
+		LeveledListItem{Level: 1, Text: "0.1"},
+		LeveledListItem{Level: 1, Text: "0.2"},
+		LeveledListItem{Level: 0, Text: "1.0"},
+		LeveledListItem{Level: 0, Text: "2.0"},
+		LeveledListItem{Level: 1, Text: "2.1"},
+		LeveledListItem{Level: 1, Text: "2.2"},
+		LeveledListItem{Level: 2, Text: "2.2.1"},
+		LeveledListItem{Level: 10, Text: "2.3"}}))
+
+	assert.Equal(t, NewTreeFromLeveledList(LeveledList{
+		LeveledListItem{Level: 0, Text: "0.0"},
+		LeveledListItem{Level: 1, Text: "0.1"},
+		LeveledListItem{Level: 1, Text: "0.2"},
+		LeveledListItem{Level: 0, Text: "1.0"},
+		LeveledListItem{Level: 0, Text: "2.0"},
+		LeveledListItem{Level: 1, Text: "2.1"},
+		LeveledListItem{Level: 1, Text: "2.2"},
+		LeveledListItem{Level: 2, Text: "2.2.1"},
+		LeveledListItem{Level: 3, Text: "2.3"}}), p2.Root)
+	assert.Empty(t, p.Root)
+}
+
+func TestTree_NewTreeFromLeveledListEmptyList(t *testing.T) {
+	p := DefaultTree
+	p2 := p.WithRoot(NewTreeFromLeveledList(LeveledList{}))
+
+	assert.Equal(t, NewTreeFromLeveledList(LeveledList{}), p2.Root)
+	assert.Empty(t, p.Root)
+}
+
+func TestTree_NewTreeFromLeveledListNegativeLevel(t *testing.T) {
+	p := DefaultTree
+	p2 := p.WithRoot(NewTreeFromLeveledList(LeveledList{
+		LeveledListItem{Level: 0, Text: "0.0"},
+		LeveledListItem{Level: 1, Text: "0.1"},
+		LeveledListItem{Level: 1, Text: "0.2"},
+		LeveledListItem{Level: 0, Text: "1.0"},
+		LeveledListItem{Level: 0, Text: "2.0"},
+		LeveledListItem{Level: 1, Text: "2.1"},
+		LeveledListItem{Level: 1, Text: "2.2"},
+		LeveledListItem{Level: 2, Text: "2.2.1"},
+		LeveledListItem{Level: -5, Text: "2.3"}}))
+
+	assert.Equal(t, NewTreeFromLeveledList(LeveledList{
+		LeveledListItem{Level: 0, Text: "0.0"},
+		LeveledListItem{Level: 1, Text: "0.1"},
+		LeveledListItem{Level: 1, Text: "0.2"},
+		LeveledListItem{Level: 0, Text: "1.0"},
+		LeveledListItem{Level: 0, Text: "2.0"},
+		LeveledListItem{Level: 1, Text: "2.1"},
+		LeveledListItem{Level: 1, Text: "2.2"},
+		LeveledListItem{Level: 2, Text: "2.2.1"},
+		LeveledListItem{Level: 0, Text: "2.3"}}), p2.Root)
+	assert.Empty(t, p.Root)
+}
+
+func TestTree_WithHorizontalString(t *testing.T) {
+	p := Tree{}
+	p2 := p.WithHorizontalString("-")
+
+	assert.Equal(t, "-", p2.HorizontalString)
+	assert.Empty(t, p.HorizontalString)
+}
+
+func TestTree_WithRoot(t *testing.T) {
+	p := Tree{}
+	p2 := p.WithRoot(TreeNode{
+		Children: nil,
+		Text:     "Hello, World!",
+	})
+
+	assert.Equal(t, TreeNode{
+		Children: nil,
+		Text:     "Hello, World!",
+	}, p2.Root)
+	assert.Empty(t, p.Root)
+}
+
+func TestTree_WithTreeStyle(t *testing.T) {
+	p := Tree{}
+	s := NewStyle(FgRed, BgRed, Bold)
+	p2 := p.WithTreeStyle(s)
+
+	assert.Equal(t, s, p2.TreeStyle)
+	assert.Empty(t, p.TreeStyle)
+}
+
+func TestTree_WithTextStyle(t *testing.T) {
+	p := Tree{}
+	s := NewStyle(FgRed, BgRed, Bold)
+	p2 := p.WithTextStyle(s)
+
+	assert.Equal(t, s, p2.TextStyle)
+	assert.Empty(t, p.TextStyle)
+}
+
+func TestTree_WithTopRightCornerString(t *testing.T) {
+	p := Tree{}
+	p2 := p.WithTopRightCornerString("-")
+
+	assert.Equal(t, "-", p2.TopRightCornerString)
+	assert.Empty(t, p.TopRightCornerString)
+}
+
+func TestTree_WithTopRightDownStringOngoing(t *testing.T) {
+	p := Tree{}
+	p2 := p.WithTopRightDownStringOngoing("-")
+
+	assert.Equal(t, "-", p2.TopRightDownString)
+	assert.Empty(t, p.TopRightDownString)
+}
+
+func TestTree_WithVerticalString(t *testing.T) {
+	p := Tree{}
+	p2 := p.WithVerticalString("-")
+
+	assert.Equal(t, "-", p2.VerticalString)
+	assert.Empty(t, p.VerticalString)
+}