Skip to content

Commit

Permalink
feat: list renderer
Browse files Browse the repository at this point in the history
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)
```

<p align="center">
<img width="600" alt="image" src="https://github.com/charmbracelet/lipgloss/assets/42545625/0dc9f440-0748-4151-a3b0-7dcf29dfcdb0">
</p>

-->

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.

<p align="center">
<img width="600" alt="List example" src="https://github.com/charmbracelet/lipgloss/assets/42545625/360494f1-57fb-4e13-bc19-0006efe01561">
</p>

In addition to the predefined enumerators (`Arabic`, `Alphabet`, `Roman`, `Bullet`, `Tree`),
you may also define your own custom enumerator:

```go
var DuckDuckGooseEnumerator Enumerator = func(l *List, i int) string {
    if l.At(i) == "Goose" {
        return "Honk →"
    }
    return ""
}
```

Use it in a list:

```go
l := list.New("Duck", "Duck", "Duck", "Duck", "Goose", "Duck", "Duck")
l.Enumerator(DuckDuckGooseEnumerator)
```

Print the list:

<p align="center">
<img width="600" alt="image" src="https://github.com/charmbracelet/lipgloss/assets/42545625/157aaf30-140d-4948-9bb4-dfba46e5b87e">
</p>

If you need, you can also build lists incrementally:

```go
l := list.New()

for i := 0; i < repeat; i++ {
    l.Item("Lip Gloss")
}
```
  • Loading branch information
maaslalani committed May 15, 2024
1 parent ce5323e commit 1a82cdc
Show file tree
Hide file tree
Showing 7 changed files with 747 additions and 0 deletions.
23 changes: 23 additions & 0 deletions examples/list/duckduckgoose/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"fmt"

"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/list"
)

func main() {
l := list.New("Duck", "Duck", "Duck", "Duck", "Goose", "Duck", "Duck")

var DuckDuckGooseEnumerator = func(i int) string {
if l.At(i) == "Goose" {
return "Honk →"
}
return ""
}

l = l.Enumerator(DuckDuckGooseEnumerator).EnumeratorStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("48")).MarginRight(1))

fmt.Println(l)
}
26 changes: 26 additions & 0 deletions examples/list/makeup/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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("212")).MarginRight(1)

l := list.New(
"Glossier",
"Claire’s Boutique",
"Nyx",
"Mac",
"Milk",
).
Enumerator(list.Roman).
EnumeratorStyle(enumeratorStyle).
ItemStyle(itemStyle)

fmt.Println(l.String())
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ retract v0.7.0 // v0.7.0 introduces a bug that causes some apps to freeze.
go 1.18

require (
github.com/MakeNowJust/heredoc v1.0.0
github.com/charmbracelet/x/exp/term v0.0.0-20240408110044-525ba71bb562
github.com/muesli/termenv v0.15.2
github.com/rivo/uniseg v0.4.7
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
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/charmbracelet/x/exp/term v0.0.0-20240408110044-525ba71bb562 h1:jCSNgVpyc16IspmSdrUTio2lY33YojCN4tKOyQxWIg4=
Expand Down
72 changes: 72 additions & 0 deletions list/enumerator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package list

import (
"fmt"
"strings"
)

// Enumerator defines a function that returns the correct prefix for the list
// element at the given index.
type Enumerator func(i int) string

const abcLen = 26

// Alphabet is the enumeration for alphabetical listing.
//
// a. Foo
// b. Bar
// c. Baz
// d. Qux.
func Alphabet(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)
}

// Arabic is the enumeration for arabic numerals listing.
//
// 1. Foo
// 2. Bar
// 3. Baz
// 4. Qux.
func Arabic(i int) string {
return fmt.Sprintf("%d.", i+1)
}

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}
)

// Roman is the enumeration for roman numerals listing.
//
// / I. Foo
// / II. Bar
// / III. Baz
// / IV. Qux.
func Roman(i int) string {
var 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.
//
// • Foo
// • Bar
// • Baz
// • Qux.
func Bullet(_ int) string {
return "•"
}
188 changes: 188 additions & 0 deletions list/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// package list defines an API to build lists.

Check failure on line 1 in list/list.go

View workflow job for this annotation

GitHub Actions / lint

package-comments: package comment should be of the form "Package list ..." (revive)

Check failure on line 1 in list/list.go

View workflow job for this annotation

GitHub Actions / lint

package-comments: package comment should be of the form "Package list ..." (revive)
//
// A list is a enumerated collection of items. Items are rendered vertically
// stacked on top of each other. Like the following:
//
// list.New("A", "B", "C").
// (list.Bullet).
// String()
//
// • A
// • B
// • C
package list

import (
"fmt"
"strings"

"github.com/charmbracelet/lipgloss"
)

// Item is a list item. It allows the list to print it's elements, a list can
// contain nested lists.
type Item interface {
any | List
}

// List is the representation of a lipgloss List.
//
// It can be printed to display a human-readable list.
//
// fmt.Println(list.New("A", "B", "C"))
//
// • A
// • B
// • C
type List struct {
indent int
separator lipgloss.Style
items []Item
itemStyleFunc StyleFunc
enumerator Enumerator
enumeratorStyleFunc StyleFunc
}

// StyleFunc defines a list style function that returns the correct style for
// the list element at the given index.
type StyleFunc func(i int) lipgloss.Style

// New returns a new list given the list items.
// List items may be of any primative type and other Lists.

Check failure on line 51 in list/list.go

View workflow job for this annotation

GitHub Actions / lint-soft

`primative` is a misspelling of `primitive` (misspell)

Check failure on line 51 in list/list.go

View workflow job for this annotation

GitHub Actions / lint-soft

`primative` is a misspelling of `primitive` (misspell)
//
// list.New(
// "Foo",
// "Bar",
// NewBaz{},
// list.New(
// "Qux",
// "Quux",
// ),
// )
func New(items ...Item) List {
return List{
items: items,
separator: lipgloss.NewStyle().SetString("\n"),
enumerator: func(i int) string {

Check failure on line 66 in list/list.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'i' seems to be unused, consider removing or renaming it as _ (revive)

Check failure on line 66 in list/list.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'i' seems to be unused, consider removing or renaming it as _ (revive)
return "•"
},
enumeratorStyleFunc: func(i int) lipgloss.Style {

Check failure on line 69 in list/list.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'i' seems to be unused, consider removing or renaming it as _ (revive)

Check failure on line 69 in list/list.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'i' seems to be unused, consider removing or renaming it as _ (revive)
return lipgloss.NewStyle().MarginRight(1)
},
}
}

func (l List) Item(item Item) List {

Check failure on line 75 in list/list.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported method List.Item should have comment or be unexported (revive)

Check failure on line 75 in list/list.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported method List.Item should have comment or be unexported (revive)
l.items = append(l.items, item)
return l
}

// Separator sets the list separator style, which separates list items.
// The default separator is a newline.
func (l List) Separator(s lipgloss.Style) List {
l.separator = s
return l
}

// Prefix sets a static enumerator.
//
// list.New(...).Prefix(s) is equivalent to:
//
// list.New(...).Enumerator(func(_ int) string {
// return s
// })
func (l List) Prefix(s string) List {
l.enumerator = func(i int) string {

Check failure on line 95 in list/list.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'i' seems to be unused, consider removing or renaming it as _ (revive)

Check failure on line 95 in list/list.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'i' seems to be unused, consider removing or renaming it as _ (revive)
return s
}
return l
}

// Enumerator sets list enumeration function. The function will be called with
// the index of the item in the list.
func (l List) Enumerator(e Enumerator) List {
l.enumerator = e
return l
}

// EnumeratorStyleFunc sets the style function for the list enumeration. The
// function will be called with the index of the item in the list.
func (l List) EnumeratorStyleFunc(f StyleFunc) List {
l.enumeratorStyleFunc = f
return l
}

// ItemStyleFunc sets the style function for the list enumeration. The
// function will be called with the index of the item in the list.
func (l List) ItemStyleFunc(f StyleFunc) List {
l.itemStyleFunc = f
return l
}

// EnumeratorStyle sets the style for the list enumeration. The function will be
// called with the index of the item in the list.
func (l List) EnumeratorStyle(style lipgloss.Style) List {
l.enumeratorStyleFunc = func(_ int) lipgloss.Style { return style }
return l
}

// ItemStyle sets the style function for the list enumeration. The
// function will be called with the index of the item in the list.
func (l List) ItemStyle(style lipgloss.Style) List {
l.itemStyleFunc = func(_ int) lipgloss.Style { return style }
return l
}

// At returns the item at index i.
func (l List) At(i int) Item {
return l.items[i]
}

// String returns a string representation of the list.
func (l List) String() string {
var s strings.Builder

// find the longest enumerator value of this list.
last := len(l.items) - 1
var maxLen int
for i := 0; i <= last; i++ {
enum := l.enumeratorStyleFunc(i).Render(l.enumerator(i))
maxLen = max(lipgloss.Width(enum), maxLen)
}

for i, item := range l.items {
switch item := item.(type) {
case List:
item.indent = l.indent + 2

Check failure on line 156 in list/list.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 2, in <operation> detected (gomnd)

Check failure on line 156 in list/list.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 2, in <operation> detected (gomnd)
s.WriteString(item.String())
if i != last {
s.WriteString(l.separator.String())
}
default:
indent := strings.Repeat(" ", l.indent)
enumerator := l.enumeratorStyleFunc(i).
Width(maxLen - 1).
Align(lipgloss.Right).
Render(l.enumerator(i))
listItem := lipgloss.JoinHorizontal(
lipgloss.Top,
indent,
enumerator,
fmt.Sprintf("%v", item),
)
s.WriteString(listItem)
if i != last {
s.WriteString(l.separator.String())
}
}

}

Check failure on line 179 in list/list.go

View workflow job for this annotation

GitHub Actions / lint

unnecessary trailing newline (whitespace)

Check failure on line 179 in list/list.go

View workflow job for this annotation

GitHub Actions / lint

unnecessary trailing newline (whitespace)
return s.String()
}

func max(a, b int) int {
if a > b {
return a
}
return b
}

0 comments on commit 1a82cdc

Please sign in to comment.