diff --git a/examples/go.mod b/examples/go.mod index 54cd487e56..30c87a0448 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -47,3 +47,7 @@ require ( golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.24.0 // indirect ) + +replace charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251114160003-3248589b24c9 => ../../lipgloss + +replace charm.land/bubbles/v2 v2.0.0-beta.1.0.20251110211018-84a82dfeeed8 => ../../bubbles diff --git a/examples/go.sum b/examples/go.sum index 5f0706a2bb..0532d55117 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,7 +1,3 @@ -charm.land/bubbles/v2 v2.0.0-beta.1.0.20251110211018-84a82dfeeed8 h1:2bzaNBvZHgpLjxlQTDjoeY0YGjQtMflWiDZMEbviiRE= -charm.land/bubbles/v2 v2.0.0-beta.1.0.20251110211018-84a82dfeeed8/go.mod h1:5AbN6cEd/47gkEf8TgiQ2O3RZ5QxMS14l9W+7F9fPC4= -charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251114160003-3248589b24c9 h1:FSmPSuQzHfyzens1NukU5wP76ttNcEa8MZQIZM77RbQ= -charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251114160003-3248589b24c9/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= diff --git a/examples/tree-default/main.go b/examples/tree-default/main.go new file mode 100644 index 0000000000..ca5d824c82 --- /dev/null +++ b/examples/tree-default/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "os" + + "charm.land/bubbles/v2/tree" + tea "charm.land/bubbletea/v2" +) + +type model struct { + tree tree.Model +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyPressMsg: + switch msg.String() { + case "q", "ctrl+c": + return m, tea.Quit + } + } + m.tree, cmd = m.tree.Update(msg) + return m, cmd +} + +func (m model) View() tea.View { + return tea.NewView(m.tree.View()) +} + +func main() { + t := tree.New(tree.Root("~/charm"). + Child( + "ayman", + tree.Root("bash"). + Child( + tree.Root("tools"). + Child("zsh", + "doom-emacs", + ), + ), + tree.Root("carlos"). + Child( + tree.Root("emotes"). + Child( + "chefkiss.png", + "kekw.png", + ), + ), + "maas", + ), 70, 13) + + if _, err := tea.NewProgram(model{tree: t}).Run(); err != nil { + fmt.Println("Oh no:", err) + os.Exit(1) + } +} diff --git a/examples/tree-file-system/main.go b/examples/tree-file-system/main.go new file mode 100644 index 0000000000..6d36e07d3f --- /dev/null +++ b/examples/tree-file-system/main.go @@ -0,0 +1,142 @@ +package main + +import ( + "fmt" + "os" + + "charm.land/bubbles/v2/key" + "charm.land/bubbles/v2/tree" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + ltree "charm.land/lipgloss/v2/tree" + "github.com/charmbracelet/x/ansi" +) + +type model struct { + tree tree.Model + choice *tree.Node +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyPressMsg: + switch msg.String() { + case "e": + m.choice = m.tree.NodeAtCurrentOffset() + return m, tea.Quit + case "q", "ctrl+c": + return m, tea.Quit + } + } + m.tree, cmd = m.tree.Update(msg) + m.updateStyles() + + return m, cmd +} + +func (m *model) updateStyles() { + dimmed := lipgloss.Color("239") + base := lipgloss.NewStyle() + m.tree.SetStyles(tree.Styles{ + TreeStyle: base. + Padding(1). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("236")). + BorderBackground(base.GetBackground()), + RootNodeStyle: base, + NodeStyle: base, + ParentNodeStyle: base, + OpenIndicatorStyle: base, + SelectedNodeStyle: base.Bold(true).Background(lipgloss.Color("8")), + HelpStyle: base.MarginTop(1), + EnumeratorStyle: base.Foreground(dimmed), + IndenterStyle: base.Foreground(dimmed), + }) +} + +func (m model) View() tea.View { + return tea.NewView(m.tree.View()) +} + +type file struct { + name string + color string +} + +func (f file) String() string { + return "⌯ " + lipgloss.NewStyle().Foreground(lipgloss.Color(f.color)).Render(f.name) +} + +type dir struct { + name string +} + +func (d dir) String() string { + return lipgloss.NewStyle().Foreground(lipgloss.Color("4")).Render(d.name) +} + +const ( + width = 50 + height = 21 + enumeratorWidth = 3 +) + +func main() { + t := tree.New( + tree.Root(dir{"charmbracelet/lipgloss"}). + Indenter(func(_ ltree.Children, _ int) string { + return "│ " + }). + Enumerator(func(_ ltree.Children, _ int) string { + return "│ " + }). + Child( + tree.Root(dir{"tree"}). + Child(file{"tree.go", "6"}). + Child(file{"renderer.go", "6"}), + ). + Child( + tree.Root(dir{"table"}). + Child( + tree.Root(dir{"utils"}). + Child(file{"utils.go", "6"}), + ), + ). + Child(tree.Root(dir{"list"}).Child(lipgloss.NewStyle().Faint(true).Render("(empty)"))). + Child(file{"README.md", "3"}). + Child(file{"go.mod", "255"}). + Child(file{"go.sum", "255"}). + Child(file{".gitignore", "255"}), + width, + height, + ) + t.SetCursorCharacter("") + t.SetOpenCharacter("📂") + t.SetClosedCharacter("📁") + kb := []key.Binding{ + key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "select")), + } + t.SetAdditionalShortHelpKeys(func() []key.Binding { + return kb + }) + t.SetAdditionalFullHelpKeys(func() []key.Binding { + return kb + }) + + p := tea.NewProgram(model{tree: t}) + m, err := p.Run() + if err != nil { + fmt.Println("Oh no:", err) + os.Exit(1) + } + + // Assert the final tea.Model to our local model and print the choice. + if m, ok := m.(model); ok && m.choice != nil { + fmt.Printf("---\nYou chose %s!\n", ansi.Strip(m.choice.Value())) + } +} diff --git a/examples/tree-long/main.go b/examples/tree-long/main.go new file mode 100644 index 0000000000..4b8aaa36a4 --- /dev/null +++ b/examples/tree-long/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "os" + "time" + + "charm.land/bubbles/v2/tree" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" +) + +type model struct { + tree tree.Model +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyPressMsg: + switch msg.String() { + case "q", "ctrl+c": + return m, tea.Quit + } + } + m.tree, cmd = m.tree.Update(msg) + return m, cmd +} + +func (m model) View() tea.View { + return tea.NewView(m.tree.View()) +} + +func main() { + root := tree.Root("🛂 Passport expiration date") + thisYear := time.Now().Year() + for year := thisYear; year < thisYear+10; year++ { + yRoot := tree.Root(fmt.Sprintf("%d", year)).Close() + for month := 1; month <= 12; month++ { + mRoot := tree.Root(time.Month(month).String()).Close().RootStyle( + lipgloss.NewStyle().Foreground(lipgloss.Color("1"))) + for day := 1; day < daysIn(time.Month(month), year); day++ { + mRoot.Child(fmt.Sprintf("%d", day)) + } + yRoot.Child(mRoot) + } + root.Child(yRoot) + } + + t := tree.New(root, 80, 30) + + if _, err := tea.NewProgram(model{tree: t}).Run(); err != nil { + fmt.Println("Oh no:", err) + os.Exit(1) + } +} + +func daysIn(m time.Month, year int) int { + return time.Date(year, m+1, 0, 0, 0, 0, 0, time.UTC).Day() +} diff --git a/examples/tree-toc/main.go b/examples/tree-toc/main.go new file mode 100644 index 0000000000..1a3556dd12 --- /dev/null +++ b/examples/tree-toc/main.go @@ -0,0 +1,121 @@ +package main + +import ( + "fmt" + "os" + + "charm.land/bubbles/v2/tree" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + ltree "charm.land/lipgloss/v2/tree" +) + +type model struct { + tree tree.Model +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyPressMsg: + switch msg.String() { + case "q", "ctrl+c": + return m, tea.Quit + } + } + m.tree, cmd = m.tree.Update(msg) + m.updateStyles() + + return m, cmd +} + +func (m *model) updateStyles() { + m.tree.SetStyles(tree.Styles{ + RootNodeStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("205")), + SelectedNodeStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#EE6FF8")).Bold(true), + CursorStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#EE6FF8")).Bold(true), + HelpStyle: lipgloss.NewStyle().MarginTop(1), + }) +} + +func (m model) View() tea.View { + pageNumbers := make([]string, len(m.tree.AllNodes())) + for i, node := range m.tree.AllNodes() { + v := node.GivenValue() + if page, ok := v.(page); ok { + num := fmt.Sprintf("%d", page.page) + if i == m.tree.YOffset() { + num = lipgloss.NewStyle().Foreground(lipgloss.Color("#EE6FF8")).Bold(true).Render(num) + } else { + num = lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Render(num) + } + pageNumbers[i] = num + } + } + v := lipgloss.NewStyle().Padding(1).Render( + lipgloss.JoinHorizontal( + lipgloss.Top, + m.tree.View(), + lipgloss.JoinVertical(lipgloss.Left, pageNumbers...)), + ) + + return tea.NewView(v) +} + +const ( + width = 60 + height = 12 +) + +func enumerator(_ ltree.Children, _ int) string { + return " " +} + +func indenter(_ ltree.Children, _ int) string { + return " " +} + +type page struct { + title string + page int +} + +func (p page) String() string { + return p.title +} + +func main() { + t := tree.New( + tree.Root(page{"Go Mistakes", 1}). + Enumerator(enumerator). + Indenter(indenter). + Child( + tree.Root(page{"Code and Project Organization", 2}). + Child(page{"Unintended variable shadowing", 12}). + Child(page{"Unnecessary nested code", 22}), + ). + Child( + tree.Root(page{"Data Types", 23}). + Child(page{"Creating confusion with octal literals", 28}). + Child(page{"Neglecting integer overflows", 52}), + ). + Child( + tree.Root(page{"Strings", 53}). + Child(page{"Not understaing the concept of rune", 59}). + Child(page{"Misusing trim functions", 61}), + ), + width, + height, + ) + t.SetClosedCharacter("📘") + t.SetOpenCharacter("📖") + + if _, err := tea.NewProgram(model{tree: t}).Run(); err != nil { + fmt.Println("Oh no:", err) + os.Exit(1) + } +}