Repository: pterm/pterm (go)

Issue:
Add `ListPrinter`
`ListPrinter` should implement `TextPrinter`. Should be able to print leveled lists.

### Example:

```
- Level one
  - Level two
- This is an example
  - level 2
    - level 3
```

**should print:**

- Level one
  - Level two
- This is an example
  - level 2
    - level 3

Pull request:
feat: add `BulletListPrinter`
### Description
Added a List printer.

### Scope
> What is affected by this pull request?

- [ ] Bug Fix
- [x] New Feature
- [ ] Documentation
- [ ] Other

### Related Issue
Fixes #43 


### 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/.golangci.yml b/.golangci.yml
index 483bb4f9..ce45fd91 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -16,6 +16,7 @@ linters-settings:
       - hugeParam
       - ptrToRefParam
       - paramTypeCombine
+      - unnamedResult
   #  maligned:
   #    suggest-new: true
   misspell:
diff --git a/_examples/bulletlist-custom/main.go b/_examples/bulletlist-custom/main.go
new file mode 100644
index 00000000..0c1cbf08
--- /dev/null
+++ b/_examples/bulletlist-custom/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+	"github.com/pterm/pterm"
+)
+
+func main() {
+	pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{
+		{Level: 0, Text: "Blue", TextStyle: pterm.NewStyle(pterm.FgBlue), BulletStyle: pterm.NewStyle(pterm.FgRed)},
+		{Level: 1, Text: "Green", TextStyle: pterm.NewStyle(pterm.FgGreen), Bullet: "-", BulletStyle: pterm.NewStyle(pterm.FgLightWhite)},
+		{Level: 2, Text: "Cyan", TextStyle: pterm.NewStyle(pterm.FgCyan), Bullet: ">", BulletStyle: pterm.NewStyle(pterm.FgYellow)},
+	}).Render()
+}
diff --git a/_examples/bulletlist/main.go b/_examples/bulletlist/main.go
new file mode 100644
index 00000000..3faa7075
--- /dev/null
+++ b/_examples/bulletlist/main.go
@@ -0,0 +1,16 @@
+package main
+
+import "github.com/pterm/pterm"
+
+func main() {
+	pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{
+		{Level: 0, Text: "Level 0"},
+		{Level: 1, Text: "Level 1"},
+		{Level: 2, Text: "Level 2"},
+	}).Render()
+
+	pterm.NewListFromString(`0
+ 1
+  2
+   3`, " ").Render()
+}
diff --git a/bulletlist_printer.go b/bulletlist_printer.go
new file mode 100644
index 00000000..d38979da
--- /dev/null
+++ b/bulletlist_printer.go
@@ -0,0 +1,149 @@
+package pterm
+
+import (
+	"strings"
+
+	"github.com/pterm/pterm/internal"
+)
+
+// NewListFromStrings returns a BulletList with Text using the NewListItemFromString method.
+func NewListFromStrings(s []string, padding string) BulletList {
+	var lis []BulletListItem
+	for _, line := range s {
+		lis = append(lis, NewListItemFromString(line, padding))
+	}
+	return *DefaultBulletList.WithItems(lis)
+}
+
+// NewListItemFromString returns a ListItem with a Text. The padding is counted in the Text to define the Level of the ListItem.
+func NewListItemFromString(text string, padding string) BulletListItem {
+	s, l := internal.RemoveAndCountPrefix(text, padding)
+	return BulletListItem{
+		Level: l,
+		Text:  s,
+	}
+}
+
+// DefaultListItem contains standards, which can be used to print a ListItem.
+var DefaultListItem = BulletListItem{
+	Bullet:      "•",
+	TextStyle:   &ThemeDefault.ListTextStyle,
+	BulletStyle: &ThemeDefault.ListBulletStyle,
+}
+
+// BulletListItem is able to render a ListItem.
+type BulletListItem struct {
+	Level       int
+	Text        string
+	TextStyle   *Style
+	Bullet      string
+	BulletStyle *Style
+}
+
+// WithLevel returns a new ListItem 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.
+func (p BulletListItem) WithText(text string) BulletListItem {
+	p.Text = text
+	return p
+}
+
+// WithTextStyle returns a new ListItem 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.
+func (p BulletListItem) WithBullet(bullet string) BulletListItem {
+	p.Bullet = bullet
+	return p
+}
+
+// WithBulletStyle returns a new ListItem with a specific BulletStyle.
+func (p BulletListItem) WithBulletStyle(style *Style) BulletListItem {
+	p.BulletStyle = style
+	return p
+}
+
+// Render renders the ListItem as a string.
+func (p BulletListItem) Render() {
+	Println(p.Srender())
+}
+
+// Srender renders the ListItem as a string.
+func (p BulletListItem) Srender() string {
+	if p.TextStyle == nil {
+		p.TextStyle = NewStyle()
+	}
+	if p.BulletStyle == nil {
+		p.BulletStyle = &ThemeDefault.ListBulletStyle
+	}
+	return strings.Repeat(" ", p.Level) + p.BulletStyle.Sprint(p.Bullet) + " " + p.TextStyle.Sprint(p.Text)
+}
+
+// NewListFromString returns a BulletList with Text using the NewListItemFromString method, splitting after return (\n).
+func NewListFromString(s string, padding string) BulletList {
+	return NewListFromStrings(strings.Split(s, "\n"), padding)
+}
+
+// DefaultBulletList contains standards, which can be used to print a BulletList.
+var DefaultBulletList = BulletList{
+	Bullet:      "•",
+	TextStyle:   &ThemeDefault.ListTextStyle,
+	BulletStyle: &ThemeDefault.ListBulletStyle,
+}
+
+// BulletList is able to render a list.
+type BulletList struct {
+	Items       []BulletListItem
+	TextStyle   *Style
+	Bullet      string
+	BulletStyle *Style
+}
+
+// WithItems returns a new list with specific Items.
+func (l BulletList) WithItems(items []BulletListItem) *BulletList {
+	l.Items = append(l.Items, items...)
+	return &l
+}
+
+// WithTextStyle returns a new list with a specific text style.
+func (l BulletList) WithTextStyle(style *Style) *BulletList {
+	l.TextStyle = style
+	return &l
+}
+
+// WithBullet returns a new list with a specific bullet.
+func (l BulletList) WithBullet(bullet string) *BulletList {
+	l.Bullet = bullet
+	return &l
+}
+
+// WithBulletStyle returns a new list with a specific bullet style.
+func (l BulletList) WithBulletStyle(style *Style) *BulletList {
+	l.BulletStyle = style
+	return &l
+}
+
+// Render prints the list to the terminal.
+func (l BulletList) Render() {
+	Println(l.Srender())
+}
+
+// Srender renders the list as a string.
+func (l BulletList) Srender() string {
+	var ret string
+	for _, item := range l.Items {
+		if item.Bullet == "" {
+			ret += item.WithBullet(l.Bullet).Srender() + "\n"
+		} else {
+			ret += item.WithBullet(item.Bullet).Srender() + "\n"
+		}
+	}
+	return ret
+}
diff --git a/internal/remove_and_count_prefix.go b/internal/remove_and_count_prefix.go
new file mode 100644
index 00000000..5c770317
--- /dev/null
+++ b/internal/remove_and_count_prefix.go
@@ -0,0 +1,11 @@
+package internal
+
+import (
+	"strings"
+)
+
+func RemoveAndCountPrefix(input, subString string) (string, int) {
+	inputLength := len(input)
+	input = strings.TrimLeft(input, subString)
+	return input, inputLength - len(input)
+}
diff --git a/theme.go b/theme.go
index 30f473cf..823d16e3 100644
--- a/theme.go
+++ b/theme.go
@@ -30,6 +30,8 @@ var (
 		TableHeaderStyle:        Style{FgLightCyan},
 		TableSeparatorStyle:     Style{FgGray},
 		SectionStyle:            Style{Bold, FgYellow},
+		ListTextStyle:           Style{FgWhite},
+		ListBulletStyle:         Style{FgGray},
 	}
 )
 
@@ -63,6 +65,8 @@ type Theme struct {
 	TableHeaderStyle        Style
 	TableSeparatorStyle     Style
 	SectionStyle            Style
+	ListTextStyle           Style
+	ListBulletStyle         Style
 }
 
 // WithPrimaryStyle returns a new theme with overridden value.
@@ -154,3 +158,15 @@ func (t Theme) WithDescriptionPrefixStyle(style Style) Theme {
 	t.DescriptionPrefixStyle = style
 	return t
 }
+
+// WithListTextStyle returns a new theme with overridden value.
+func (t Theme) WithListTextStyle(style Style) Theme {
+	t.ListTextStyle = style
+	return t
+}
+
+// WithListBulletStyle returns a new theme with overridden value.
+func (t Theme) WithListBulletStyle(style Style) Theme {
+	t.ListBulletStyle = style
+	return t
+}


Test patch:
diff --git a/bulletlist_printer_test.go b/bulletlist_printer_test.go
new file mode 100644
index 00000000..c2aaf81b
--- /dev/null
+++ b/bulletlist_printer_test.go
@@ -0,0 +1,160 @@
+package pterm
+
+import (
+	"fmt"
+	"io"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/pterm/pterm/internal"
+)
+
+func TestListPrinterNilPrint(t *testing.T) {
+	p := BulletList{}
+	p.Render()
+}
+
+func TestBulletList_Render(t *testing.T) {
+	internal.TestPrintContains(t, func(w io.Writer, a interface{}) {
+		DefaultBulletList.WithItems([]BulletListItem{
+			{Level: 0, Text: fmt.Sprint(a)},
+		}).Render()
+	})
+}
+
+func TestBulletList_RenderWithBullet(t *testing.T) {
+	internal.TestPrintContains(t, func(w io.Writer, a interface{}) {
+		DefaultBulletList.WithItems([]BulletListItem{
+			{
+				Level:  0,
+				Text:   fmt.Sprint(a),
+				Bullet: "-",
+			},
+		}).Render()
+	})
+}
+
+func TestBulletList_Srender(t *testing.T) {
+	internal.TestSprintContains(t, func(a interface{}) string {
+		return DefaultBulletList.WithItems([]BulletListItem{
+			{Level: 0, Text: fmt.Sprint(a)},
+		}).Srender()
+	})
+}
+
+func TestBulletList_WithBullet(t *testing.T) {
+	p := BulletList{}
+	p2 := p.WithBullet("-")
+
+	assert.Equal(t, "-", p2.Bullet)
+	assert.Empty(t, p.Bullet)
+}
+
+func TestBulletList_WithBulletStyle(t *testing.T) {
+	p := BulletList{}
+	s := NewStyle(FgRed, BgRed, Bold)
+	p2 := p.WithBulletStyle(s)
+
+	assert.Equal(t, s, p2.BulletStyle)
+	assert.Empty(t, p.BulletStyle)
+}
+
+func TestBulletList_WithItems(t *testing.T) {
+	p := BulletList{}
+	li := []BulletListItem{{
+		Level:       0,
+		Text:        "test",
+		TextStyle:   nil,
+		Bullet:      "+",
+		BulletStyle: nil,
+	}}
+	p2 := p.WithItems(li)
+
+	assert.Equal(t, li, p2.Items)
+	assert.Empty(t, p.Items)
+}
+
+func TestBulletList_WithTextStyle(t *testing.T) {
+	p := BulletList{}
+	s := NewStyle(FgRed, BgRed, Bold)
+	p2 := p.WithTextStyle(s)
+
+	assert.Equal(t, s, p2.TextStyle)
+	assert.Empty(t, p.TextStyle)
+}
+
+func TestListItem_Render(t *testing.T) {
+	internal.TestPrintContains(t, func(w io.Writer, a interface{}) {
+		DefaultListItem.WithText(fmt.Sprint(a)).Render()
+	})
+}
+
+func TestListItem_Srender(t *testing.T) {
+	internal.TestSprintContains(t, func(a interface{}) string {
+		return DefaultListItem.WithText(fmt.Sprint(a)).Srender()
+	})
+}
+
+func TestListItem_WithBullet(t *testing.T) {
+	p := BulletListItem{}
+	p2 := p.WithBullet("-")
+
+	assert.Equal(t, "-", p2.Bullet)
+	assert.Empty(t, p.Bullet)
+}
+
+func TestListItem_WithBulletStyle(t *testing.T) {
+	p := BulletListItem{}
+	s := NewStyle(FgRed, BgRed, Bold)
+	p2 := p.WithBulletStyle(s)
+
+	assert.Equal(t, s, p2.BulletStyle)
+	assert.Empty(t, p.BulletStyle)
+}
+
+func TestListItem_WithLevel(t *testing.T) {
+	p := BulletListItem{}
+	p2 := p.WithLevel(1)
+
+	assert.Equal(t, 1, p2.Level)
+	assert.Empty(t, p.Level)
+}
+
+func TestListItem_WithText(t *testing.T) {
+	p := BulletListItem{}
+	p2 := p.WithText("test")
+
+	assert.Equal(t, "test", p2.Text)
+	assert.Empty(t, p.Text)
+}
+
+func TestListItem_WithTextStyle(t *testing.T) {
+	p := BulletListItem{}
+	s := NewStyle(FgRed, BgRed, Bold)
+	p2 := p.WithTextStyle(s)
+
+	assert.Equal(t, s, p2.TextStyle)
+	assert.Empty(t, p.TextStyle)
+}
+
+func TestNewListFromString(t *testing.T) {
+	p := *DefaultBulletList.WithItems([]BulletListItem{
+		{Level: 0, Text: "0"},
+		{Level: 1, Text: "1"},
+		{Level: 2, Text: "2"},
+		{Level: 3, Text: "3"},
+		{Level: 4, Text: "4"},
+		{Level: 5, Text: "5"},
+	})
+
+	s := `0
+ 1
+  2
+   3
+    4
+     5`
+	p2 := NewListFromString(s, " ")
+
+	assert.Equal(t, p, p2)
+}
diff --git a/theme_test.go b/theme_test.go
index d7f02373..3bdf6a65 100644
--- a/theme_test.go
+++ b/theme_test.go
@@ -124,3 +124,19 @@ func TestTheme_WithWarningPrefixStyle(t *testing.T) {
 
 	assert.Equal(t, s, p2.WarningPrefixStyle)
 }
+
+func TestTheme_WithListBulletStyle(t *testing.T) {
+	s := Style{FgRed, BgBlue, Bold}
+	p := Theme{}
+	p2 := p.WithListBulletStyle(s)
+
+	assert.Equal(t, s, p2.ListBulletStyle)
+}
+
+func TestTheme_WithListTextStyle(t *testing.T) {
+	s := Style{FgRed, BgBlue, Bold}
+	p := Theme{}
+	p2 := p.WithListTextStyle(s)
+
+	assert.Equal(t, s, p2.ListTextStyle)
+}