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 74a5b96
Show file tree
Hide file tree
Showing 7 changed files with 739 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 "•"
}
191 changes: 191 additions & 0 deletions list/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Package list defines an API to build lists.
//
// 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
// primitive type and other Lists.
//
// 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(_ int) string { return "•" },
enumeratorStyleFunc: func(_ int) lipgloss.Style {
return lipgloss.NewStyle().MarginRight(1)
},
}
}

// Item appends the given item to the list.
func (l List) Item(item Item) List {
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(_ int) string {
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 {
if i >= 0 && i < len(l.items) {
return l.items[i]
}
return nil
}

const indent = 2

// 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 + indent
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())
}
}
}
return s.String()
}

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

0 comments on commit 74a5b96

Please sign in to comment.