From 90c9e447b23c648c28bbb169ea4c1a80b86f65b4 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Fri, 11 Oct 2024 21:18:43 +0300 Subject: [PATCH 01/29] feat: tree bubble - initial skeleton --- tree/tree.go | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 tree/tree.go diff --git a/tree/tree.go b/tree/tree.go new file mode 100644 index 000000000..68a6c5342 --- /dev/null +++ b/tree/tree.go @@ -0,0 +1,124 @@ +package tree + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + ltree "github.com/charmbracelet/lipgloss/tree" + + "github.com/charmbracelet/bubbles/key" +) + +// Styles contains style definitions for this tree component. By default, these +// values are generated by DefaultStyles. +type Styles struct { + SelectedNode lipgloss.Style + SelectionCursor lipgloss.Style +} + +// DefaultStyles returns a set of default style definitions for this tree +// component. +func DefaultStyles() (s Styles) { + s.SelectedNode = lipgloss.NewStyle(). + Background(lipgloss.Color("62")). + Foreground(lipgloss.Color("230")) + s.SelectionCursor = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) + + return s +} + +// KeyMap is the key bindings for different actions within the tree. +type KeyMap struct { + Down key.Binding + Up key.Binding +} + +// DefaultKeyMap is the default set of key bindings for navigating and acting +// upon the tree. +var DefaultKeyMap = KeyMap{ + Down: key.NewBinding(key.WithKeys("down", "j", "ctrl+n"), key.WithHelp("down", "next line")), + Up: key.NewBinding(key.WithKeys("up", "k", "ctrl+p"), key.WithHelp("up", "previous line")), +} + +// Model is the Bubble Tea model for this tree element. +type Model struct { + tree *Tree + // KeyMap encodes the keybindings recognized by the widget. + KeyMap KeyMap + + // Styles sets the styling for the tree + Styles Styles + + // selectedValue is the value of the selected node. + selectedValue string +} + +// Leaf is a node without children. +type Leaf struct { + ltree.Leaf +} + +// Tree is a node with children +type Tree struct { + *ltree.Tree +} + +// New creates a new model with default settings. +func New() Model { + t := ltree.New() + m := Model{ + tree: &Tree{t}, + KeyMap: DefaultKeyMap, + Styles: DefaultStyles(), + selectedValue: t.Value(), + } + t.ItemStyleFunc(m.selectedNodeStyle()).RootStyle(m.nodeStyle(t)) + return m +} + +// Root sets the root value of this tree. +func (m *Model) Root(root any) *Model { + m.tree.Root(root) + return m +} + +// Root sets the root value of this tree. +func (m *Model) Child(child any) *Model { + m.tree.Child(child) + return m +} + +func (m Model) selectedNodeStyle() func(children ltree.Children, i int) lipgloss.Style { + return func(children ltree.Children, i int) lipgloss.Style { + child := children.At(i) + return m.nodeStyle(child) + } +} + +func (m Model) nodeStyle(node ltree.Node) lipgloss.Style { + st := lipgloss.NewStyle().MaxHeight(1) + if node.Value() == m.selectedValue { + st = m.Styles.SelectedNode + } + return st +} + +// Update is the Bubble Tea update loop. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg.(type) { + case tea.KeyMsg: + return m, tea.Quit + } + + return m, tea.Batch(cmds...) +} + +// View renders the component. +func (m Model) View() string { + return lipgloss.JoinHorizontal( + lipgloss.Top, + m.Styles.SelectionCursor.Render("> "), + m.tree.String(), + ) +} From 8d1cde72193f1741c9a8429b7a4fdcde2790aecf Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 12 Oct 2024 20:44:35 +0300 Subject: [PATCH 02/29] feat: selected node --- tree/tree.go | 196 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 162 insertions(+), 34 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index 68a6c5342..c38607bc4 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -1,6 +1,8 @@ package tree import ( + "fmt" + tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ltree "github.com/charmbracelet/lipgloss/tree" @@ -30,6 +32,7 @@ func DefaultStyles() (s Styles) { type KeyMap struct { Down key.Binding Up key.Binding + Quit key.Binding } // DefaultKeyMap is the default set of key bindings for navigating and acting @@ -37,78 +40,184 @@ type KeyMap struct { var DefaultKeyMap = KeyMap{ Down: key.NewBinding(key.WithKeys("down", "j", "ctrl+n"), key.WithHelp("down", "next line")), Up: key.NewBinding(key.WithKeys("up", "k", "ctrl+p"), key.WithHelp("up", "previous line")), + Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")), } // Model is the Bubble Tea model for this tree element. type Model struct { - tree *Tree + root *Item // KeyMap encodes the keybindings recognized by the widget. KeyMap KeyMap // Styles sets the styling for the tree Styles Styles - // selectedValue is the value of the selected node. - selectedValue string + // yOffset is the vertical offset of the selected node. + yOffset int +} + +// Tree is a node with children +type Item struct { + tree *ltree.Tree + yOffset int + + // TODO: move to lipgloss.Tree? + size int } -// Leaf is a node without children. -type Leaf struct { - ltree.Leaf +func (t *Item) String() string { + return t.tree.String() } -// Tree is a node with children -type Tree struct { - *ltree.Tree +// nolint +func (t *Item) Value() string { + return t.tree.Value() +} + +// nolint +func (t *Item) Children() ltree.Children { + return t.tree.Children() +} + +// nolint +func (t *Item) Hidden() bool { + return t.tree.Hidden() +} + +// nolint +// TODO: add ItemStyleFunc to the Node interface? +func (t *Item) ItemStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Item { + t.tree.ItemStyleFunc(f) + return t +} + +// nolint +func (t *Item) RootStyle(style lipgloss.Style) *Item { + t.tree.RootStyle(style) + return t +} + +// nolint +func (t *Item) Child(child any) *Item { + item := new(Item) + item.tree = ltree.Root(child) + switch child := child.(type) { + case *Item: + t.size = t.size + child.size + t.tree.Child(child) + default: + item.size = 1 + t.size = t.size + item.size + t.tree.Child(item) + } + + return t +} + +// nolint +func Root(root any) *Item { + t := new(Item) + t.size = 1 + t.tree = ltree.Root(root) + return t +} + +func updateStyles(t *Item, itemStyleFunc ltree.StyleFunc) { + t.ItemStyleFunc(itemStyleFunc) + children := t.tree.Children() + for i := 0; i < children.Length(); i++ { + child := children.At(i) + if child, ok := child.(*Item); ok { + child.ItemStyleFunc(itemStyleFunc) + updateStyles(child, itemStyleFunc) + } + } } // New creates a new model with default settings. -func New() Model { - t := ltree.New() +func New(t *Item) Model { m := Model{ - tree: &Tree{t}, - KeyMap: DefaultKeyMap, - Styles: DefaultStyles(), - selectedValue: t.Value(), + root: t, + KeyMap: DefaultKeyMap, + Styles: DefaultStyles(), } - t.ItemStyleFunc(m.selectedNodeStyle()).RootStyle(m.nodeStyle(t)) + t.size = calcAttributes(t) + m.updateStyles() return m } -// Root sets the root value of this tree. -func (m *Model) Root(root any) *Model { - m.tree.Root(root) - return m +func calcAttributes(t *Item) int { + size := 1 + children := t.tree.Children() + for i := 0; i < children.Length(); i++ { + child := children.At(i) + if child, ok := child.(*Item); ok { + size = size + child.size + } + } + + setYOffsets(t) + return size } -// Root sets the root value of this tree. -func (m *Model) Child(child any) *Model { - m.tree.Child(child) - return m +func setYOffsets(t *Item) { + children := t.tree.Children() + for i := 0; i < children.Length(); i++ { + child := children.At(i) + if child, ok := child.(*Item); ok { + child.yOffset = t.yOffset + i + 1 + setYOffsets(child) + } + } } -func (m Model) selectedNodeStyle() func(children ltree.Children, i int) lipgloss.Style { +// YOffset returns the vertical offset of the selected node. +// Useful for scrolling to the selected node using a viewport. +func (m *Model) YOffset() int { + return m.yOffset +} + +func (m *Model) selectedNodeStyle() ltree.StyleFunc { return func(children ltree.Children, i int) lipgloss.Style { child := children.At(i) return m.nodeStyle(child) } } -func (m Model) nodeStyle(node ltree.Node) lipgloss.Style { - st := lipgloss.NewStyle().MaxHeight(1) - if node.Value() == m.selectedValue { - st = m.Styles.SelectedNode +func (m *Model) nodeStyle(node ltree.Node) lipgloss.Style { + switch node := node.(type) { + case *Item: + if node.yOffset == m.yOffset { + return m.Styles.SelectedNode + } } - return st + return lipgloss.NewStyle().Foreground(lipgloss.Color("1")).MaxHeight(1) +} + +func (m *Model) updateStyles() { + // TODO: add RootStyleFunc to the Node interface? + m.root.RootStyle(m.nodeStyle(m.root)) + updateStyles(m.root, m.selectedNodeStyle()) } // Update is the Bubble Tea update loop. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmds []tea.Cmd - switch msg.(type) { + switch msg := msg.(type) { case tea.KeyMsg: - return m, tea.Quit + switch { + case key.Matches(msg, m.KeyMap.Down): + // TODO: check boundaries + m.yOffset = m.yOffset + 1 + m.updateStyles() + case key.Matches(msg, m.KeyMap.Up): + // TODO: check boundaries + m.yOffset = m.yOffset - 1 + m.updateStyles() + case key.Matches(msg, m.KeyMap.Quit): + return m, tea.Quit + } } return m, tea.Batch(cmds...) @@ -116,9 +225,28 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { // View renders the component. func (m Model) View() string { - return lipgloss.JoinHorizontal( + s := fmt.Sprintf("yOffset: %d\n", m.yOffset) + + debug := printDebugInfo(m.root) + + t := lipgloss.JoinHorizontal( lipgloss.Top, + lipgloss.NewStyle().Faint(true).MarginRight(1).Render(debug), m.Styles.SelectionCursor.Render("> "), - m.tree.String(), + m.root.String(), ) + return lipgloss.JoinVertical(lipgloss.Left, s, t) +} + +func printDebugInfo(t *Item) string { + debug := fmt.Sprintf("size=%d yOffset=%d", t.size, t.yOffset) + children := t.Children() + for i := 0; i < children.Length(); i++ { + child := children.At(i) + if child, ok := child.(*Item); ok { + debug = debug + "\n" + printDebugInfo(child) + } + } + + return debug } From 17001609be8c202b205c0afbe35568648f2743f9 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sun, 13 Oct 2024 20:50:44 +0300 Subject: [PATCH 03/29] fix: handle offset boundaries --- tree/tree.go | 63 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index c38607bc4..17f77ec30 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -91,6 +91,13 @@ func (t *Item) ItemStyleFunc(f func(children ltree.Children, i int) lipgloss.Sty return t } +// nolint +// TODO: add ItemStyleFunc to the Node interface? +func (t *Item) EnumeratorStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Item { + t.tree.EnumeratorStyleFunc(f) + return t +} + // nolint func (t *Item) RootStyle(style lipgloss.Style) *Item { t.tree.RootStyle(style) @@ -124,14 +131,6 @@ func Root(root any) *Item { func updateStyles(t *Item, itemStyleFunc ltree.StyleFunc) { t.ItemStyleFunc(itemStyleFunc) - children := t.tree.Children() - for i := 0; i < children.Length(); i++ { - child := children.At(i) - if child, ok := child.(*Item); ok { - child.ItemStyleFunc(itemStyleFunc) - updateStyles(child, itemStyleFunc) - } - } } // New creates a new model with default settings. @@ -147,26 +146,28 @@ func New(t *Item) Model { } func calcAttributes(t *Item) int { - size := 1 + rootSize := 1 children := t.tree.Children() for i := 0; i < children.Length(); i++ { child := children.At(i) if child, ok := child.(*Item); ok { - size = size + child.size + rootSize = rootSize + child.size } } setYOffsets(t) - return size + return rootSize } func setYOffsets(t *Item) { children := t.tree.Children() + above := 0 for i := 0; i < children.Length(); i++ { child := children.At(i) if child, ok := child.(*Item); ok { - child.yOffset = t.yOffset + i + 1 + child.yOffset = t.yOffset + above + i + 1 setYOffsets(child) + above += child.size - 1 } } } @@ -208,18 +209,16 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { case tea.KeyMsg: switch { case key.Matches(msg, m.KeyMap.Down): - // TODO: check boundaries - m.yOffset = m.yOffset + 1 - m.updateStyles() + m.yOffset = min(m.root.size-1, m.yOffset+1) case key.Matches(msg, m.KeyMap.Up): - // TODO: check boundaries - m.yOffset = m.yOffset - 1 - m.updateStyles() + m.yOffset = max(0, m.yOffset-1) case key.Matches(msg, m.KeyMap.Quit): return m, tea.Quit } } + // not sure why, but I think m.yOffset is captured in the closure, so we need to update the styles + m.updateStyles() return m, tea.Batch(cmds...) } @@ -227,17 +226,29 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func (m Model) View() string { s := fmt.Sprintf("yOffset: %d\n", m.yOffset) + // TODO: remove debug := printDebugInfo(m.root) + cursor := "" + for i := 0; i < m.root.size; i++ { + if i == m.yOffset { + cursor = cursor + m.Styles.SelectedNode.Render("> ") + } else { + cursor = cursor + " " + } + cursor = cursor + "\n" + } + t := lipgloss.JoinHorizontal( lipgloss.Top, lipgloss.NewStyle().Faint(true).MarginRight(1).Render(debug), - m.Styles.SelectionCursor.Render("> "), + cursor, m.root.String(), ) return lipgloss.JoinVertical(lipgloss.Left, s, t) } +// TODO: remove func printDebugInfo(t *Item) string { debug := fmt.Sprintf("size=%d yOffset=%d", t.size, t.yOffset) children := t.Children() @@ -250,3 +261,17 @@ func printDebugInfo(t *Item) string { return debug } + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} From 070c0e7ce375e3abf319e8c6deecd1f52c9aed90 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sun, 13 Oct 2024 22:34:17 +0300 Subject: [PATCH 04/29] feat: expand/collapse --- tree/tree.go | 127 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 104 insertions(+), 23 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index 17f77ec30..fc15518c3 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -30,17 +30,19 @@ func DefaultStyles() (s Styles) { // KeyMap is the key bindings for different actions within the tree. type KeyMap struct { - Down key.Binding - Up key.Binding - Quit key.Binding + Down key.Binding + Up key.Binding + Toggle key.Binding + Quit key.Binding } // DefaultKeyMap is the default set of key bindings for navigating and acting // upon the tree. var DefaultKeyMap = KeyMap{ - Down: key.NewBinding(key.WithKeys("down", "j", "ctrl+n"), key.WithHelp("down", "next line")), - Up: key.NewBinding(key.WithKeys("up", "k", "ctrl+p"), key.WithHelp("up", "previous line")), - Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")), + Down: key.NewBinding(key.WithKeys("down", "j", "ctrl+n"), key.WithHelp("down", "next line")), + Up: key.NewBinding(key.WithKeys("up", "k", "ctrl+p"), key.WithHelp("up", "previous line")), + Toggle: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "toggle")), + Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")), } // Model is the Bubble Tea model for this tree element. @@ -58,20 +60,40 @@ type Model struct { // Tree is a node with children type Item struct { - tree *ltree.Tree - yOffset int + name string + + // tree is used as the renderer layer + tree *ltree.Tree + yOffset int + expandable bool + open bool // TODO: move to lipgloss.Tree? size int } +// Use to print the Item's tree func (t *Item) String() string { - return t.tree.String() + if t.tree == nil { + return t.name + } + // t.tree != nil only for the root node, why? + if t.open { + return "▼ " + t.tree.String() + } + return "▶ " + t.tree.String() } -// nolint +// Returns the name of the Item +// Value is not called on the root node, why? func (t *Item) Value() string { - return t.tree.Value() + if t.expandable { + if t.open { + return "▼ " + t.name + } + return "▶ " + t.name + } + return t.name } // nolint @@ -104,17 +126,37 @@ func (t *Item) RootStyle(style lipgloss.Style) *Item { return t } +type expandable struct { + name string + open bool +} + +func (e expandable) String() string { + if e.open { + return "▼ " + e.name + } + return "▶ " + e.name +} + // nolint func (t *Item) Child(child any) *Item { item := new(Item) - item.tree = ltree.Root(child) + item.tree = ltree.Root(item) switch child := child.(type) { case *Item: + item.name = child.name t.size = t.size + child.size + t.open = t.size > 1 t.tree.Child(child) default: + item.name = child.(string) item.size = 1 + item.open = false t.size = t.size + item.size + t.open = t.size > 1 + // leaf exists so we can have a custom string method - so only for rendering the name + // otherwise we need have a Node + // t.tree.Child(leaf{child.(string)}) t.tree.Child(item) } @@ -124,8 +166,11 @@ func (t *Item) Child(child any) *Item { // nolint func Root(root any) *Item { t := new(Item) + t.name = root.(string) t.size = 1 - t.tree = ltree.Root(root) + t.open = true + t.expandable = true + t.tree = ltree.Root(t) return t } @@ -140,23 +185,25 @@ func New(t *Item) Model { KeyMap: DefaultKeyMap, Styles: DefaultStyles(), } - t.size = calcAttributes(t) + setAttributes(t) m.updateStyles() return m } -func calcAttributes(t *Item) int { - rootSize := 1 +func setAttributes(t *Item) { + setSizes(t) + setYOffsets(t) +} + +func setSizes(t *Item) int { children := t.tree.Children() + size := 1 + children.Length() for i := 0; i < children.Length(); i++ { child := children.At(i) - if child, ok := child.(*Item); ok { - rootSize = rootSize + child.size - } + size = size + setSizes(child.(*Item)) - 1 } - - setYOffsets(t) - return rootSize + t.size = size + return size } func setYOffsets(t *Item) { @@ -212,6 +259,21 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { m.yOffset = min(m.root.size-1, m.yOffset+1) case key.Matches(msg, m.KeyMap.Up): m.yOffset = max(0, m.yOffset-1) + case key.Matches(msg, m.KeyMap.Toggle): + node := findNode(m.root, m.yOffset) + if node == nil { + break + } + node.open = !node.open + if node.open { + node.tree.Offset(0, 0) + } else { + node.tree.Offset(0, node.tree.Children().Length()) + } + setAttributes(m.root) + m.yOffset = node.yOffset + // TODO: re-calcualte the size and yOffsets + // calcAttributes(m.root) case key.Matches(msg, m.KeyMap.Quit): return m, tea.Quit } @@ -232,7 +294,7 @@ func (m Model) View() string { cursor := "" for i := 0; i < m.root.size; i++ { if i == m.yOffset { - cursor = cursor + m.Styles.SelectedNode.Render("> ") + cursor = cursor + m.Styles.SelectedNode.Render("👉 ") } else { cursor = cursor + " " } @@ -275,3 +337,22 @@ func max(a, b int) int { } return b } + +func findNode(t *Item, yOffset int) *Item { + if t.yOffset == yOffset { + return t + } + + children := t.tree.Children() + for i := 0; i < children.Length(); i++ { + child := children.At(i) + if child, ok := child.(*Item); ok { + found := findNode(child, yOffset) + if found != nil { + return found + } + } + } + + return nil +} From 42e170e6d2897d1924c99e7e7427a650c6b68065 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Mon, 14 Oct 2024 19:37:43 +0300 Subject: [PATCH 05/29] chore: clean up --- tree/tree.go | 282 +++++++++++++++++++++++++++------------------------ 1 file changed, 151 insertions(+), 131 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index fc15518c3..ee13a99ab 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -14,6 +14,7 @@ import ( // values are generated by DefaultStyles. type Styles struct { SelectedNode lipgloss.Style + ItemStyle lipgloss.Style SelectionCursor lipgloss.Style } @@ -22,8 +23,10 @@ type Styles struct { func DefaultStyles() (s Styles) { s.SelectedNode = lipgloss.NewStyle(). Background(lipgloss.Color("62")). - Foreground(lipgloss.Color("230")) + Foreground(lipgloss.Color("18")). + Bold(true) s.SelectionCursor = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) + s.ItemStyle = lipgloss.NewStyle() return s } @@ -58,126 +61,136 @@ type Model struct { yOffset int } -// Tree is a node with children +// Item is a a node in the tree +// Item implements lipgloss's tree.Node type Item struct { - name string - // tree is used as the renderer layer - tree *ltree.Tree - yOffset int - expandable bool - open bool + tree *ltree.Tree + + // yOffset is the vertical offset of the selected node. + yOffset int + + // isRoot is true for every Item which was added with tree.Root + isRoot bool + open bool + + // TODO: expose a getter for this in lipgloss + rootStyle lipgloss.Style // TODO: move to lipgloss.Tree? size int } -// Use to print the Item's tree +// Used to print the Item's tree +// NOTE: Value is not called on the root node, so we need to repeat the open/closed character +// Should this be fixed in lipgloss? func (t *Item) String() string { - if t.tree == nil { - return t.name - } - // t.tree != nil only for the root node, why? if t.open { - return "▼ " + t.tree.String() + return t.rootStyle.Render("▼ ") + t.tree.String() } - return "▶ " + t.tree.String() + return t.rootStyle.Render("▶ ") + t.tree.String() } -// Returns the name of the Item -// Value is not called on the root node, why? +// Value returns the root name of this node. func (t *Item) Value() string { - if t.expandable { + if t.isRoot { if t.open { - return "▼ " + t.name + return "▼ " + t.tree.Value() } - return "▶ " + t.name + return "▶ " + t.tree.Value() } - return t.name + return t.tree.Value() } -// nolint +// Children returns the children of an item. func (t *Item) Children() ltree.Children { return t.tree.Children() } -// nolint +// Hidden returns whether this item is hidden. func (t *Item) Hidden() bool { return t.tree.Hidden() } -// nolint -// TODO: add ItemStyleFunc to the Node interface? +// ItemStyleFunc sets the item style function. Use this for conditional styling. +// For example: +// +// t := tree.Root("root"). +// ItemStyleFunc(func(_ tree.Data, i int) lipgloss.Style { +// if selected == i { +// return lipgloss.NewStyle().Foreground(hightlightColor) +// } +// return lipgloss.NewStyle().Foreground(dimColor) +// }) func (t *Item) ItemStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Item { t.tree.ItemStyleFunc(f) return t } -// nolint -// TODO: add ItemStyleFunc to the Node interface? +// EnumeratorStyleFunc sets the enumeration style function. Use this function +// for conditional styling. +// +// t := tree.Root("root"). +// EnumeratorStyleFunc(func(_ tree.Children, i int) lipgloss.Style { +// if selected == i { +// return lipgloss.NewStyle().Foreground(hightlightColor) +// } +// return lipgloss.NewStyle().Foreground(dimColor) +// }) func (t *Item) EnumeratorStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Item { t.tree.EnumeratorStyleFunc(f) return t } -// nolint +// RootStyle sets a style for the root element. func (t *Item) RootStyle(style lipgloss.Style) *Item { t.tree.RootStyle(style) return t } -type expandable struct { - name string - open bool -} - -func (e expandable) String() string { - if e.open { - return "▼ " + e.name - } - return "▶ " + e.name -} - -// nolint +// Child adds a child to this tree. +// +// If a Child Item is passed without a root, it will be parented to it's sibling +// child (auto-nesting). +// +// tree.Root("Foo").Child(tree.Root("Bar").Child("Baz"), "Qux") +// +// ├── Foo +// ├── Bar +// │ └── Baz +// └── Qux func (t *Item) Child(child any) *Item { - item := new(Item) - item.tree = ltree.Root(item) switch child := child.(type) { case *Item: - item.name = child.name t.size = t.size + child.size t.open = t.size > 1 + child.open = child.size > 1 t.tree.Child(child) default: - item.name = child.(string) + item := new(Item) + // TODO: should I create a tree for leaf nodes? + // makes the code a bit simpler + item.tree = ltree.Root(child) item.size = 1 item.open = false t.size = t.size + item.size t.open = t.size > 1 - // leaf exists so we can have a custom string method - so only for rendering the name - // otherwise we need have a Node - // t.tree.Child(leaf{child.(string)}) t.tree.Child(item) } return t } -// nolint +// Root returns a new tree with the root set. func Root(root any) *Item { t := new(Item) - t.name = root.(string) t.size = 1 t.open = true - t.expandable = true - t.tree = ltree.Root(t) + t.isRoot = true + t.tree = ltree.Root(root) return t } -func updateStyles(t *Item, itemStyleFunc ltree.StyleFunc) { - t.ItemStyleFunc(itemStyleFunc) -} - // New creates a new model with default settings. func New(t *Item) Model { m := Model{ @@ -190,64 +203,6 @@ func New(t *Item) Model { return m } -func setAttributes(t *Item) { - setSizes(t) - setYOffsets(t) -} - -func setSizes(t *Item) int { - children := t.tree.Children() - size := 1 + children.Length() - for i := 0; i < children.Length(); i++ { - child := children.At(i) - size = size + setSizes(child.(*Item)) - 1 - } - t.size = size - return size -} - -func setYOffsets(t *Item) { - children := t.tree.Children() - above := 0 - for i := 0; i < children.Length(); i++ { - child := children.At(i) - if child, ok := child.(*Item); ok { - child.yOffset = t.yOffset + above + i + 1 - setYOffsets(child) - above += child.size - 1 - } - } -} - -// YOffset returns the vertical offset of the selected node. -// Useful for scrolling to the selected node using a viewport. -func (m *Model) YOffset() int { - return m.yOffset -} - -func (m *Model) selectedNodeStyle() ltree.StyleFunc { - return func(children ltree.Children, i int) lipgloss.Style { - child := children.At(i) - return m.nodeStyle(child) - } -} - -func (m *Model) nodeStyle(node ltree.Node) lipgloss.Style { - switch node := node.(type) { - case *Item: - if node.yOffset == m.yOffset { - return m.Styles.SelectedNode - } - } - return lipgloss.NewStyle().Foreground(lipgloss.Color("1")).MaxHeight(1) -} - -func (m *Model) updateStyles() { - // TODO: add RootStyleFunc to the Node interface? - m.root.RootStyle(m.nodeStyle(m.root)) - updateStyles(m.root, m.selectedNodeStyle()) -} - // Update is the Bubble Tea update loop. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmds []tea.Cmd @@ -272,8 +227,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } setAttributes(m.root) m.yOffset = node.yOffset - // TODO: re-calcualte the size and yOffsets - // calcAttributes(m.root) case key.Matches(msg, m.KeyMap.Quit): return m, tea.Quit } @@ -286,7 +239,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { // View renders the component. func (m Model) View() string { - s := fmt.Sprintf("yOffset: %d\n", m.yOffset) + s := fmt.Sprintf("y=%d\n", m.yOffset) // TODO: remove debug := printDebugInfo(m.root) @@ -310,34 +263,87 @@ func (m Model) View() string { return lipgloss.JoinVertical(lipgloss.Left, s, t) } -// TODO: remove -func printDebugInfo(t *Item) string { - debug := fmt.Sprintf("size=%d yOffset=%d", t.size, t.yOffset) - children := t.Children() +func setAttributes(t *Item) { + setSizes(t) + setYOffsets(t) +} + +// setSizes updates each Item's size +// Note that if a child isn't open, its size is 1 +func setSizes(t *Item) int { + children := t.tree.Children() + size := 1 + children.Length() + for i := 0; i < children.Length(); i++ { + child := children.At(i) + size = size + setSizes(child.(*Item)) - 1 + } + t.size = size + return size +} + +// setYOffsets updates each Item's yOffset based on how many items are "above" it +func setYOffsets(t *Item) { + children := t.tree.Children() + above := 0 for i := 0; i < children.Length(); i++ { child := children.At(i) if child, ok := child.(*Item); ok { - debug = debug + "\n" + printDebugInfo(child) + child.yOffset = t.yOffset + above + i + 1 + setYOffsets(child) + above += child.size - 1 } } +} - return debug +// YOffset returns the vertical offset of the selected node. +// Useful for scrolling to the selected node using a viewport. +func (m *Model) YOffset() int { + return m.yOffset } -func min(a, b int) int { - if a < b { - return a +// Since the selected node changes, we need to capture m.yOffset in the +// style function's closure again +func (m *Model) updateStyles() { + m.root.rootStyle = m.nodeStyle(m.root) + // TODO: add RootStyleFunc to the Node interface? + m.root.RootStyle(m.root.rootStyle) + m.root.ItemStyleFunc(m.selectedNodeStyle()) +} + +// selectedNodeStyle sets the node style +// and takes into account whether it's selected or not +func (m *Model) selectedNodeStyle() ltree.StyleFunc { + return func(children ltree.Children, i int) lipgloss.Style { + child := children.At(i) + return m.nodeStyle(child) } - return b } -func max(a, b int) int { - if a > b { - return a +func (m *Model) nodeStyle(node ltree.Node) lipgloss.Style { + if node, ok := node.(*Item); ok { + if node.yOffset == m.yOffset { + return m.Styles.SelectedNode + } } - return b + return m.Styles.ItemStyle +} + +// TODO: remove +func printDebugInfo(t *Item) string { + debug := fmt.Sprintf("size=%-2d y=%d", t.size, t.yOffset) + children := t.Children() + for i := 0; i < children.Length(); i++ { + child := children.At(i) + if child, ok := child.(*Item); ok { + debug = debug + "\n" + printDebugInfo(child) + } + } + + return debug } +// findNode starts a DFS search for the node with the given yOffset +// starting from the given item func findNode(t *Item, yOffset int) *Item { if t.yOffset == yOffset { return t @@ -356,3 +362,17 @@ func findNode(t *Item, yOffset int) *Item { return nil } + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} From fd2b5276c2427b94d7ccc06ce1b2a43a67111090 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Mon, 14 Oct 2024 19:39:09 +0300 Subject: [PATCH 06/29] chore: change NOTE to TODO --- tree/tree.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tree/tree.go b/tree/tree.go index ee13a99ab..816e5baa4 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -82,7 +82,7 @@ type Item struct { } // Used to print the Item's tree -// NOTE: Value is not called on the root node, so we need to repeat the open/closed character +// TODO: Value is not called on the root node, so we need to repeat the open/closed character // Should this be fixed in lipgloss? func (t *Item) String() string { if t.open { From 8c58f1fe040480075c04ace25c767b08eb24f94b Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Mon, 14 Oct 2024 19:46:56 +0300 Subject: [PATCH 07/29] chore: add todo --- tree/tree.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tree/tree.go b/tree/tree.go index 816e5baa4..20af7e1fb 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -127,6 +127,8 @@ func (t *Item) ItemStyleFunc(f func(children ltree.Children, i int) lipgloss.Sty return t } +// TODO: support IndentStyleFunc in lipgloss so we can have a full background for the item? + // EnumeratorStyleFunc sets the enumeration style function. Use this function // for conditional styling. // From 81ae05770acb09be085b5358e5fca49e67895105 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Thu, 17 Oct 2024 14:14:17 +0300 Subject: [PATCH 08/29] feat: ability to costmize open/closed characters --- tree/tree.go | 64 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index 20af7e1fb..e4f69b03a 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -57,6 +57,12 @@ type Model struct { // Styles sets the styling for the tree Styles Styles + // OpenCharacter is the character used to represent an open node. + OpenCharacter string + + // ClosedCharacter is the character used to represent a closed node. + ClosedCharacter string + // yOffset is the vertical offset of the selected node. yOffset int } @@ -77,27 +83,34 @@ type Item struct { // TODO: expose a getter for this in lipgloss rootStyle lipgloss.Style + opts *itemOptions + // TODO: move to lipgloss.Tree? size int } +type itemOptions struct { + openCharacter string + closedCharacter string +} + // Used to print the Item's tree // TODO: Value is not called on the root node, so we need to repeat the open/closed character // Should this be fixed in lipgloss? func (t *Item) String() string { if t.open { - return t.rootStyle.Render("▼ ") + t.tree.String() + return t.rootStyle.Render(t.opts.openCharacter+" ") + t.tree.String() } - return t.rootStyle.Render("▶ ") + t.tree.String() + return t.rootStyle.Render(t.opts.closedCharacter+" ") + t.tree.String() } // Value returns the root name of this node. func (t *Item) Value() string { if t.isRoot { if t.open { - return "▼ " + t.tree.Value() + return t.opts.openCharacter + " " + t.tree.Value() } - return "▶ " + t.tree.Value() + return t.opts.closedCharacter + " " + t.tree.Value() } return t.tree.Value() } @@ -196,11 +209,13 @@ func Root(root any) *Item { // New creates a new model with default settings. func New(t *Item) Model { m := Model{ - root: t, - KeyMap: DefaultKeyMap, - Styles: DefaultStyles(), + root: t, + KeyMap: DefaultKeyMap, + Styles: DefaultStyles(), + OpenCharacter: "▼", + ClosedCharacter: "▶", } - setAttributes(t) + m.setAttributes() m.updateStyles() return m } @@ -227,7 +242,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } else { node.tree.Offset(0, node.tree.Children().Length()) } - setAttributes(m.root) + m.setAttributes() m.yOffset = node.yOffset case key.Matches(msg, m.KeyMap.Quit): return m, tea.Quit @@ -265,9 +280,25 @@ func (m Model) View() string { return lipgloss.JoinVertical(lipgloss.Left, s, t) } -func setAttributes(t *Item) { - setSizes(t) - setYOffsets(t) +// FlatItems returns all items in the tree as a flat list. +func (m *Model) FlatItems() []*Item { + return m.root.FlatItems() +} + +func (m *Model) setAttributes() { + setSizes(m.root) + setYOffsets(m.root) +} + +// FlatItems returns all items in the tree as a flat list. +func (t *Item) FlatItems() []*Item { + res := []*Item{t} + children := t.tree.Children() + for i := 0; i < children.Length(); i++ { + child := children.At(i) + res = append(res, child.(*Item).FlatItems()...) + } + return res } // setSizes updates each Item's size @@ -310,6 +341,15 @@ func (m *Model) updateStyles() { // TODO: add RootStyleFunc to the Node interface? m.root.RootStyle(m.root.rootStyle) m.root.ItemStyleFunc(m.selectedNodeStyle()) + + items := m.FlatItems() + opts := &itemOptions{ + openCharacter: m.OpenCharacter, + closedCharacter: m.ClosedCharacter, + } + for _, item := range items { + item.opts = opts + } } // selectedNodeStyle sets the node style From 3b6757e26278d6f59155cdb8ff3278c6c4d669b4 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Thu, 17 Oct 2024 14:47:30 +0300 Subject: [PATCH 09/29] feat: expose Item and ItemAtCurrentOffset --- tree/tree.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tree/tree.go b/tree/tree.go index e4f69b03a..7e16536de 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -334,6 +334,16 @@ func (m *Model) YOffset() int { return m.yOffset } +// Item returns the item at the given yoffset +func (m *Model) Item(yoffset int) *Item { + return findNode(m.root, yoffset) +} + +// ItemAtCurrentOffset returns the item at the current yoffset +func (m *Model) ItemAtCurrentOffset() *Item { + return findNode(m.root, m.yOffset) +} + // Since the selected node changes, we need to capture m.yOffset in the // style function's closure again func (m *Model) updateStyles() { @@ -372,7 +382,7 @@ func (m *Model) nodeStyle(node ltree.Node) lipgloss.Style { // TODO: remove func printDebugInfo(t *Item) string { - debug := fmt.Sprintf("size=%-2d y=%d", t.size, t.yOffset) + debug := fmt.Sprintf("size=%2d y=%2d", t.size, t.yOffset) children := t.Children() for i := 0; i < children.Length(); i++ { child := children.At(i) From fc2f458c3fbd9773ac5ed91b7e10f56a0bfab497 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Thu, 17 Oct 2024 17:38:34 +0300 Subject: [PATCH 10/29] feat: help --- tree/tree.go | 118 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 105 insertions(+), 13 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index 7e16536de..910e535b4 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -6,7 +6,9 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ltree "github.com/charmbracelet/lipgloss/tree" + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" ) @@ -16,6 +18,7 @@ type Styles struct { SelectedNode lipgloss.Style ItemStyle lipgloss.Style SelectionCursor lipgloss.Style + HelpStyle lipgloss.Style } // DefaultStyles returns a set of default style definitions for this tree @@ -27,6 +30,7 @@ func DefaultStyles() (s Styles) { Bold(true) s.SelectionCursor = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) s.ItemStyle = lipgloss.NewStyle() + s.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) return s } @@ -36,32 +40,68 @@ type KeyMap struct { Down key.Binding Up key.Binding Toggle key.Binding - Quit key.Binding + + // Help toggle keybindings. + ShowFullHelp key.Binding + CloseFullHelp key.Binding + + Quit key.Binding } // DefaultKeyMap is the default set of key bindings for navigating and acting // upon the tree. var DefaultKeyMap = KeyMap{ - Down: key.NewBinding(key.WithKeys("down", "j", "ctrl+n"), key.WithHelp("down", "next line")), - Up: key.NewBinding(key.WithKeys("up", "k", "ctrl+p"), key.WithHelp("up", "previous line")), - Toggle: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "toggle")), - Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")), + Down: key.NewBinding( + key.WithKeys("down", "j", "ctrl+n"), + key.WithHelp("down", "next line"), + ), + Up: key.NewBinding( + key.WithKeys("up", "k", "ctrl+p"), + key.WithHelp("up", "previous line"), + ), + Toggle: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "toggle"), + ), + + // Toggle help. + ShowFullHelp: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "more"), + ), + CloseFullHelp: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "close help"), + ), + + Quit: key.NewBinding( + key.WithKeys("q", "ctrl+c"), + key.WithHelp("q", "quit"), + ), } // Model is the Bubble Tea model for this tree element. type Model struct { - root *Item + showHelp bool + // OpenCharacter is the character used to represent an open node. + OpenCharacter string + // ClosedCharacter is the character used to represent a closed node. + ClosedCharacter string // KeyMap encodes the keybindings recognized by the widget. KeyMap KeyMap - // Styles sets the styling for the tree Styles Styles + Help help.Model - // OpenCharacter is the character used to represent an open node. - OpenCharacter string + // Additional key mappings for the short and full help views. This allows + // you to add additional key mappings to the help menu without + // re-implementing the help component. Of course, you can also disable the + // list's help component and implement a new one if you need more + // flexibility. + AdditionalShortHelpKeys func() []key.Binding + AdditionalFullHelpKeys func() []key.Binding - // ClosedCharacter is the character used to represent a closed node. - ClosedCharacter string + root *Item // yOffset is the vertical offset of the selected node. yOffset int @@ -210,10 +250,12 @@ func Root(root any) *Item { func New(t *Item) Model { m := Model{ root: t, + showHelp: true, KeyMap: DefaultKeyMap, Styles: DefaultStyles(), OpenCharacter: "▼", ClosedCharacter: "▶", + Help: help.New(), } m.setAttributes() m.updateStyles() @@ -246,6 +288,10 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { m.yOffset = node.yOffset case key.Matches(msg, m.KeyMap.Quit): return m, tea.Quit + case key.Matches(msg, m.KeyMap.ShowFullHelp): + fallthrough + case key.Matches(msg, m.KeyMap.CloseFullHelp): + m.Help.ShowAll = !m.Help.ShowAll } } @@ -256,7 +302,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { // View renders the component. func (m Model) View() string { - s := fmt.Sprintf("y=%d\n", m.yOffset) + s := fmt.Sprintf("y=%2d current=%s\n", m.yOffset, ansi.Strip(m.ItemAtCurrentOffset().Value())) // TODO: remove debug := printDebugInfo(m.root) @@ -277,7 +323,53 @@ func (m Model) View() string { cursor, m.root.String(), ) - return lipgloss.JoinVertical(lipgloss.Left, s, t) + + var help string + if m.showHelp { + help = m.helpView() + } + + return lipgloss.JoinVertical(lipgloss.Left, s, t, help) +} + +// ShortHelp returns bindings to show in the abbreviated help view. It's part +// of the help.KeyMap interface. +func (m Model) ShortHelp() []key.Binding { + kb := []key.Binding{ + m.KeyMap.Up, + m.KeyMap.Down, + m.KeyMap.Toggle, + m.KeyMap.Quit, + } + + if m.AdditionalShortHelpKeys != nil { + kb = append(kb, m.AdditionalShortHelpKeys()...) + } + + return kb +} + +// FullHelp returns bindings to show the full help view. It's part of the +// help.KeyMap interface. +func (m Model) FullHelp() [][]key.Binding { + kb := [][]key.Binding{ + { + m.KeyMap.Up, + m.KeyMap.Down, + m.KeyMap.Toggle, + m.KeyMap.Quit, + }, + } + + if m.AdditionalFullHelpKeys != nil { + kb = append(kb, m.AdditionalFullHelpKeys()) + } + + return kb +} + +func (m Model) helpView() string { + return m.Styles.HelpStyle.Render(m.Help.View(m)) } // FlatItems returns all items in the tree as a flat list. From 75fbd52fac13e170d4aa9449fbd84f6219bfba7f Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Fri, 18 Oct 2024 21:37:14 +0300 Subject: [PATCH 11/29] WIP --- tree/tree.go | 304 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 242 insertions(+), 62 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index 910e535b4..13f5adb4b 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -2,23 +2,32 @@ package tree import ( "fmt" + "os" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ltree "github.com/charmbracelet/lipgloss/tree" - "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" ) +// StyleFunc allows the tree to be styled per item. +type StyleFunc func(children Items, i int) lipgloss.Style + // Styles contains style definitions for this tree component. By default, these // values are generated by DefaultStyles. type Styles struct { - SelectedNode lipgloss.Style - ItemStyle lipgloss.Style - SelectionCursor lipgloss.Style - HelpStyle lipgloss.Style + SelectedNode lipgloss.Style + SelectionCursor lipgloss.Style + HelpStyle lipgloss.Style + TreeStyle lipgloss.Style + selectedItemFunc StyleFunc + SelectedItemStyle lipgloss.Style + SelectedItemStyleFunc StyleFunc + itemFunc StyleFunc + ItemStyle lipgloss.Style + ItemStyleFunc StyleFunc } // DefaultStyles returns a set of default style definitions for this tree @@ -28,8 +37,14 @@ func DefaultStyles() (s Styles) { Background(lipgloss.Color("62")). Foreground(lipgloss.Color("18")). Bold(true) + s.itemFunc = func(items Items, index int) lipgloss.Style { + item := items.At(index) + if item.IsSelected() { + return s.SelectedNode + } + return lipgloss.NewStyle() + } s.SelectionCursor = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) - s.ItemStyle = lipgloss.NewStyle() s.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) return s @@ -53,15 +68,15 @@ type KeyMap struct { var DefaultKeyMap = KeyMap{ Down: key.NewBinding( key.WithKeys("down", "j", "ctrl+n"), - key.WithHelp("down", "next line"), + key.WithHelp("↓/j", "down"), ), Up: key.NewBinding( key.WithKeys("up", "k", "ctrl+p"), - key.WithHelp("up", "previous line"), + key.WithHelp("↑/k", "up"), ), Toggle: key.NewBinding( key.WithKeys("enter"), - key.WithHelp("enter", "toggle"), + key.WithHelp("⏎", "toggle"), ), // Toggle help. @@ -89,8 +104,8 @@ type Model struct { ClosedCharacter string // KeyMap encodes the keybindings recognized by the widget. KeyMap KeyMap - // Styles sets the styling for the tree - Styles Styles + // styles sets the styling for the tree + styles Styles Help help.Model // Additional key mappings for the short and full help views. This allows @@ -101,8 +116,9 @@ type Model struct { AdditionalShortHelpKeys func() []key.Binding AdditionalFullHelpKeys func() []key.Binding - root *Item - + root *Item + width int + height int // yOffset is the vertical offset of the selected node. yOffset int } @@ -116,6 +132,9 @@ type Item struct { // yOffset is the vertical offset of the selected node. yOffset int + // depth is the depth of the node in the tree + depth int + // isRoot is true for every Item which was added with tree.Root isRoot bool open bool @@ -129,9 +148,46 @@ type Item struct { size int } +// IsSelected returns whether this item is selected. +func (t *Item) IsSelected() bool { + return t.yOffset == t.opts.treeYOffset +} + +// Depth returns the depth of the node in the tree. +func (t *Item) Depth() int { + return t.depth +} + +// Size returns the number of nodes in the tree. +// Note that if a child isn't open, its size is 1 +func (t *Item) Size() int { + return t.size +} + +// YOffset returns the vertical offset of the Item +func (t *Item) YOffset() int { + return t.yOffset +} + type itemOptions struct { openCharacter string closedCharacter string + treeWidth int + treeYOffset int +} + +// TODO +const EnumeratorWidth = 4 + +// TODO +const IndenterWidth = 4 + +func (t *Item) contentWidth() int { + c := max(0, (t.depth-1)*IndenterWidth+EnumeratorWidth) + lipgloss.Width(t.tree.Value()) + if t.isRoot { + c += lipgloss.Width(t.opts.openCharacter + " ") + } + return c } // Used to print the Item's tree @@ -146,13 +202,16 @@ func (t *Item) String() string { // Value returns the root name of this node. func (t *Item) Value() string { + s := lipgloss.NewStyle() + // padding := strings.Repeat("·", max(0, t.opts.treeWidth-t.contentWidth())) + padding := "" if t.isRoot { if t.open { - return t.opts.openCharacter + " " + t.tree.Value() + return s.Render(t.opts.openCharacter + " " + t.tree.Value() + padding) } - return t.opts.closedCharacter + " " + t.tree.Value() + return s.Render(t.opts.closedCharacter + " " + t.tree.Value() + padding) } - return t.tree.Value() + return s.Render(t.tree.Value() + padding) } // Children returns the children of an item. @@ -165,6 +224,24 @@ func (t *Item) Hidden() bool { return t.tree.Hidden() } +type Items []*Item + +// Children returns the children of an item. +func (t Items) At(index int) *Item { + return t[index] +} + +// Children returns the children of an item. +func (t Items) Length() int { + return len(t) +} + +// ItemStyle sets a static style for all items. +func (t *Item) ItemStyle(s lipgloss.Style) *Item { + t.tree.ItemStyle(s) + return t +} + // ItemStyleFunc sets the item style function. Use this for conditional styling. // For example: // @@ -175,13 +252,40 @@ func (t *Item) Hidden() bool { // } // return lipgloss.NewStyle().Foreground(dimColor) // }) -func (t *Item) ItemStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Item { - t.tree.ItemStyleFunc(f) +func (t *Item) ItemStyleFunc(f StyleFunc) *Item { + t.tree.ItemStyleFunc(func(children ltree.Children, i int) lipgloss.Style { + c := make(Items, children.Length()) + // TODO: if we expose Depth and Size, we can avoid this + for i := 0; i < children.Length(); i++ { + c[i] = children.At(i).(*Item) + } + return f(c, i) + }) return t } // TODO: support IndentStyleFunc in lipgloss so we can have a full background for the item? +// TODO: should we re-export RoundedEnumerator from lipgloss? +// Enumerator sets the enumerator implementation. This can be used to change the +// way the branches indicators look. Lipgloss includes predefined enumerators +// for a classic or rounded tree. For example, you can have a rounded tree: +// +// tree.New(). +// Enumerator(ltree.RoundedEnumerator) +func (t *Item) Enumerator(enumerator ltree.Enumerator) *Item { + t.tree.Enumerator(enumerator) + return t +} + +// EnumeratorStyle sets a static style for all enumerators. +// +// Use EnumeratorStyleFunc to conditionally set styles based on the tree node. +func (t *Item) EnumeratorStyle(style lipgloss.Style) *Item { + t.tree.EnumeratorStyle(style) + return t +} + // EnumeratorStyleFunc sets the enumeration style function. Use this function // for conditional styling. // @@ -214,23 +318,25 @@ func (t *Item) RootStyle(style lipgloss.Style) *Item { // ├── Bar // │ └── Baz // └── Qux -func (t *Item) Child(child any) *Item { - switch child := child.(type) { - case *Item: - t.size = t.size + child.size - t.open = t.size > 1 - child.open = child.size > 1 - t.tree.Child(child) - default: - item := new(Item) - // TODO: should I create a tree for leaf nodes? - // makes the code a bit simpler - item.tree = ltree.Root(child) - item.size = 1 - item.open = false - t.size = t.size + item.size - t.open = t.size > 1 - t.tree.Child(item) +func (t *Item) Child(children ...any) *Item { + for _, child := range children { + switch child := child.(type) { + case *Item: + t.size = t.size + child.size + t.open = t.size > 1 + child.open = child.size > 1 + t.tree.Child(child) + default: + item := new(Item) + // TODO: should I create a tree for leaf nodes? + // makes the code a bit simpler + item.tree = ltree.Root(child) + item.size = 1 + item.open = false + t.size = t.size + item.size + t.open = t.size > 1 + t.tree.Child(item) + } } return t @@ -247,16 +353,18 @@ func Root(root any) *Item { } // New creates a new model with default settings. -func New(t *Item) Model { +func New(t *Item, width, height int) Model { m := Model{ - root: t, - showHelp: true, KeyMap: DefaultKeyMap, - Styles: DefaultStyles(), + styles: DefaultStyles(), OpenCharacter: "▼", ClosedCharacter: "▶", Help: help.New(), + + showHelp: true, + root: t, } + m.setSize(width, height) m.setAttributes() m.updateStyles() return m @@ -302,26 +410,28 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { // View renders the component. func (m Model) View() string { - s := fmt.Sprintf("y=%2d current=%s\n", m.yOffset, ansi.Strip(m.ItemAtCurrentOffset().Value())) - + var s, t, debug, cursor string // TODO: remove - debug := printDebugInfo(m.root) - - cursor := "" - for i := 0; i < m.root.size; i++ { - if i == m.yOffset { - cursor = cursor + m.Styles.SelectedNode.Render("👉 ") - } else { - cursor = cursor + " " + if os.Getenv("DEBUG") == "true" { + s += fmt.Sprintf("y=%2d\n", m.yOffset) + debug = printDebugInfo(m.root) + " " + for i := 0; i < m.root.size; i++ { + if i == m.yOffset { + cursor = cursor + "👉 " + } else { + cursor = cursor + " " + } + cursor = cursor + "\n" } - cursor = cursor + "\n" } - t := lipgloss.JoinHorizontal( + t = m.styles.TreeStyle.Render(m.root.String()) + + t = lipgloss.JoinHorizontal( lipgloss.Top, - lipgloss.NewStyle().Faint(true).MarginRight(1).Render(debug), + debug, cursor, - m.root.String(), + t, ) var help string @@ -332,6 +442,52 @@ func (m Model) View() string { return lipgloss.JoinVertical(lipgloss.Left, s, t, help) } +// SetStyles sets the styles for this component. +func (m *Model) SetStyles(styles Styles) { + if styles.ItemStyleFunc != nil { + styles.itemFunc = styles.ItemStyleFunc + } else { + styles.itemFunc = func(_ Items, _ int) lipgloss.Style { + return styles.ItemStyle + } + } + if styles.SelectedItemStyleFunc != nil { + styles.selectedItemFunc = styles.SelectedItemStyleFunc + } else { + styles.selectedItemFunc = func(_ Items, _ int) lipgloss.Style { + return styles.SelectedItemStyle + } + } + m.styles = styles + m.updateStyles() +} + +// SetShowHelp shows or hides the help view. +func (m *Model) SetShowHelp(v bool) { + m.showHelp = v +} + +// SetSize sets the width and height of this component. +func (m *Model) SetSize(width, height int) { + m.setSize(width, height) +} + +// SetWidth sets the width of this component. +func (m *Model) SetWidth(v int) { + m.setSize(v, m.height) +} + +// SetHeight sets the height of this component. +func (m *Model) SetHeight(v int) { + m.setSize(m.width, v) +} + +func (m *Model) setSize(width, height int) { + m.width = width + m.height = height + m.Help.Width = width +} + // ShortHelp returns bindings to show in the abbreviated help view. It's part // of the help.KeyMap interface. func (m Model) ShortHelp() []key.Binding { @@ -339,13 +495,14 @@ func (m Model) ShortHelp() []key.Binding { m.KeyMap.Up, m.KeyMap.Down, m.KeyMap.Toggle, - m.KeyMap.Quit, } if m.AdditionalShortHelpKeys != nil { kb = append(kb, m.AdditionalShortHelpKeys()...) } + kb = append(kb, m.KeyMap.Quit, m.KeyMap.ShowFullHelp) + return kb } @@ -357,7 +514,6 @@ func (m Model) FullHelp() [][]key.Binding { m.KeyMap.Up, m.KeyMap.Down, m.KeyMap.Toggle, - m.KeyMap.Quit, }, } @@ -365,11 +521,16 @@ func (m Model) FullHelp() [][]key.Binding { kb = append(kb, m.AdditionalFullHelpKeys()) } + kb = append(kb, []key.Binding{ + m.KeyMap.Quit, + m.KeyMap.CloseFullHelp, + }) + return kb } func (m Model) helpView() string { - return m.Styles.HelpStyle.Render(m.Help.View(m)) + return m.styles.HelpStyle.Render(m.Help.View(m)) } // FlatItems returns all items in the tree as a flat list. @@ -378,6 +539,7 @@ func (m *Model) FlatItems() []*Item { } func (m *Model) setAttributes() { + setDepths(m.root, 0) setSizes(m.root) setYOffsets(m.root) } @@ -393,6 +555,17 @@ func (t *Item) FlatItems() []*Item { return res } +// setSizes updates each Item's size +// Note that if a child isn't open, its size is 1 +func setDepths(t *Item, depth int) { + t.depth = depth + children := t.tree.Children() + for i := 0; i < children.Length(); i++ { + child := children.At(i) + setDepths(child.(*Item), depth+1) + } +} + // setSizes updates each Item's size // Note that if a child isn't open, its size is 1 func setSizes(t *Item) int { @@ -448,6 +621,8 @@ func (m *Model) updateStyles() { opts := &itemOptions{ openCharacter: m.OpenCharacter, closedCharacter: m.ClosedCharacter, + treeWidth: m.width, + treeYOffset: m.yOffset, } for _, item := range items { item.opts = opts @@ -456,25 +631,30 @@ func (m *Model) updateStyles() { // selectedNodeStyle sets the node style // and takes into account whether it's selected or not -func (m *Model) selectedNodeStyle() ltree.StyleFunc { - return func(children ltree.Children, i int) lipgloss.Style { +func (m *Model) selectedNodeStyle() StyleFunc { + return func(children Items, i int) lipgloss.Style { child := children.At(i) - return m.nodeStyle(child) + if child.yOffset == m.yOffset { + return m.styles.selectedItemFunc(children, i) + } + + return m.styles.itemFunc(children, i) } } func (m *Model) nodeStyle(node ltree.Node) lipgloss.Style { if node, ok := node.(*Item); ok { if node.yOffset == m.yOffset { - return m.Styles.SelectedNode + return m.styles.SelectedNode } } - return m.Styles.ItemStyle + + return m.styles.ItemStyle } // TODO: remove func printDebugInfo(t *Item) string { - debug := fmt.Sprintf("size=%2d y=%2d", t.size, t.yOffset) + debug := fmt.Sprintf("size=%2d y=%2d depth=%2d padding=%2d", t.size, t.yOffset, t.depth, t.contentWidth()) children := t.Children() for i := 0; i < children.Length(); i++ { child := children.At(i) From f2e8edb17e03635201fd5672fe4f2f5fea200d7b Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 19 Oct 2024 14:00:34 +0300 Subject: [PATCH 12/29] fix: rename Item -> Node and consolidate styling --- tree/tree.go | 225 ++++++++++++++++++++++++--------------------------- 1 file changed, 106 insertions(+), 119 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index 13f5adb4b..f941e204f 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -13,39 +13,38 @@ import ( ) // StyleFunc allows the tree to be styled per item. -type StyleFunc func(children Items, i int) lipgloss.Style +type StyleFunc func(children Nodes, i int) lipgloss.Style // Styles contains style definitions for this tree component. By default, these // values are generated by DefaultStyles. type Styles struct { - SelectedNode lipgloss.Style SelectionCursor lipgloss.Style HelpStyle lipgloss.Style TreeStyle lipgloss.Style - selectedItemFunc StyleFunc - SelectedItemStyle lipgloss.Style - SelectedItemStyleFunc StyleFunc - itemFunc StyleFunc - ItemStyle lipgloss.Style - ItemStyleFunc StyleFunc + selectedNodeFunc StyleFunc + SelectedNodeStyle lipgloss.Style + SelectedNodeStyleFunc StyleFunc + nodeFunc StyleFunc + NodeStyle lipgloss.Style + NodeStyleFunc StyleFunc } // DefaultStyles returns a set of default style definitions for this tree // component. func DefaultStyles() (s Styles) { - s.SelectedNode = lipgloss.NewStyle(). + s.NodeStyle = lipgloss.NewStyle() + s.SelectedNodeStyle = lipgloss.NewStyle(). Background(lipgloss.Color("62")). Foreground(lipgloss.Color("18")). Bold(true) - s.itemFunc = func(items Items, index int) lipgloss.Style { - item := items.At(index) - if item.IsSelected() { - return s.SelectedNode - } - return lipgloss.NewStyle() + s.nodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return s.NodeStyle + } + s.selectedNodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return s.SelectedNodeStyle } s.SelectionCursor = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) - s.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) + s.HelpStyle = lipgloss.NewStyle().PaddingTop(1) return s } @@ -116,16 +115,16 @@ type Model struct { AdditionalShortHelpKeys func() []key.Binding AdditionalFullHelpKeys func() []key.Binding - root *Item + root *Node width int height int // yOffset is the vertical offset of the selected node. yOffset int } -// Item is a a node in the tree -// Item implements lipgloss's tree.Node -type Item struct { +// Node is a a node in the tree +// Node implements lipgloss's tree.Node +type Node struct { // tree is used as the renderer layer tree *ltree.Tree @@ -135,7 +134,7 @@ type Item struct { // depth is the depth of the node in the tree depth int - // isRoot is true for every Item which was added with tree.Root + // isRoot is true for every Node which was added with tree.Root isRoot bool open bool @@ -149,95 +148,80 @@ type Item struct { } // IsSelected returns whether this item is selected. -func (t *Item) IsSelected() bool { +func (t *Node) IsSelected() bool { return t.yOffset == t.opts.treeYOffset } // Depth returns the depth of the node in the tree. -func (t *Item) Depth() int { +func (t *Node) Depth() int { return t.depth } // Size returns the number of nodes in the tree. // Note that if a child isn't open, its size is 1 -func (t *Item) Size() int { +func (t *Node) Size() int { return t.size } -// YOffset returns the vertical offset of the Item -func (t *Item) YOffset() int { +// YOffset returns the vertical offset of the Node +func (t *Node) YOffset() int { return t.yOffset } type itemOptions struct { openCharacter string closedCharacter string - treeWidth int treeYOffset int } -// TODO -const EnumeratorWidth = 4 - -// TODO -const IndenterWidth = 4 - -func (t *Item) contentWidth() int { - c := max(0, (t.depth-1)*IndenterWidth+EnumeratorWidth) + lipgloss.Width(t.tree.Value()) - if t.isRoot { - c += lipgloss.Width(t.opts.openCharacter + " ") - } - return c -} - -// Used to print the Item's tree +// Used to print the Node's tree // TODO: Value is not called on the root node, so we need to repeat the open/closed character // Should this be fixed in lipgloss? -func (t *Item) String() string { +func (t *Node) String() string { + s := t.rootStyle.UnsetWidth() if t.open { - return t.rootStyle.Render(t.opts.openCharacter+" ") + t.tree.String() + return s.Render(t.opts.openCharacter+" ") + t.tree.String() } - return t.rootStyle.Render(t.opts.closedCharacter+" ") + t.tree.String() + return s.Render(t.opts.closedCharacter+" ") + t.tree.String() } // Value returns the root name of this node. -func (t *Item) Value() string { +func (t *Node) Value() string { s := lipgloss.NewStyle() - // padding := strings.Repeat("·", max(0, t.opts.treeWidth-t.contentWidth())) - padding := "" if t.isRoot { if t.open { - return s.Render(t.opts.openCharacter + " " + t.tree.Value() + padding) + return s.Render(t.opts.openCharacter + " " + t.tree.Value()) } - return s.Render(t.opts.closedCharacter + " " + t.tree.Value() + padding) + return s.Render(t.opts.closedCharacter + " " + t.tree.Value()) } - return s.Render(t.tree.Value() + padding) + return s.Render(t.tree.Value()) } // Children returns the children of an item. -func (t *Item) Children() ltree.Children { +func (t *Node) Children() ltree.Children { return t.tree.Children() } // Hidden returns whether this item is hidden. -func (t *Item) Hidden() bool { +func (t *Node) Hidden() bool { return t.tree.Hidden() } -type Items []*Item +// Nodes are a list of tree nodes. +type Nodes []*Node // Children returns the children of an item. -func (t Items) At(index int) *Item { +func (t Nodes) At(index int) *Node { return t[index] } // Children returns the children of an item. -func (t Items) Length() int { +func (t Nodes) Length() int { return len(t) } // ItemStyle sets a static style for all items. -func (t *Item) ItemStyle(s lipgloss.Style) *Item { +func (t *Node) ItemStyle(s lipgloss.Style) *Node { t.tree.ItemStyle(s) return t } @@ -246,25 +230,25 @@ func (t *Item) ItemStyle(s lipgloss.Style) *Item { // For example: // // t := tree.Root("root"). -// ItemStyleFunc(func(_ tree.Data, i int) lipgloss.Style { +// ItemStyleFunc(func(_ tree.Nodes, i int) lipgloss.Style { // if selected == i { // return lipgloss.NewStyle().Foreground(hightlightColor) // } // return lipgloss.NewStyle().Foreground(dimColor) // }) -func (t *Item) ItemStyleFunc(f StyleFunc) *Item { +func (t *Node) ItemStyleFunc(f StyleFunc) *Node { t.tree.ItemStyleFunc(func(children ltree.Children, i int) lipgloss.Style { - c := make(Items, children.Length()) + c := make(Nodes, children.Length()) // TODO: if we expose Depth and Size, we can avoid this for i := 0; i < children.Length(); i++ { - c[i] = children.At(i).(*Item) + c[i] = children.At(i).(*Node) } return f(c, i) }) return t } -// TODO: support IndentStyleFunc in lipgloss so we can have a full background for the item? +// TODO: support IndentStyleFunc in lipgloss so we can have a full background for the item // TODO: should we re-export RoundedEnumerator from lipgloss? // Enumerator sets the enumerator implementation. This can be used to change the @@ -273,7 +257,7 @@ func (t *Item) ItemStyleFunc(f StyleFunc) *Item { // // tree.New(). // Enumerator(ltree.RoundedEnumerator) -func (t *Item) Enumerator(enumerator ltree.Enumerator) *Item { +func (t *Node) Enumerator(enumerator ltree.Enumerator) *Node { t.tree.Enumerator(enumerator) return t } @@ -281,7 +265,7 @@ func (t *Item) Enumerator(enumerator ltree.Enumerator) *Item { // EnumeratorStyle sets a static style for all enumerators. // // Use EnumeratorStyleFunc to conditionally set styles based on the tree node. -func (t *Item) EnumeratorStyle(style lipgloss.Style) *Item { +func (t *Node) EnumeratorStyle(style lipgloss.Style) *Node { t.tree.EnumeratorStyle(style) return t } @@ -296,20 +280,20 @@ func (t *Item) EnumeratorStyle(style lipgloss.Style) *Item { // } // return lipgloss.NewStyle().Foreground(dimColor) // }) -func (t *Item) EnumeratorStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Item { +func (t *Node) EnumeratorStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Node { t.tree.EnumeratorStyleFunc(f) return t } // RootStyle sets a style for the root element. -func (t *Item) RootStyle(style lipgloss.Style) *Item { +func (t *Node) RootStyle(style lipgloss.Style) *Node { t.tree.RootStyle(style) return t } // Child adds a child to this tree. // -// If a Child Item is passed without a root, it will be parented to it's sibling +// If a Child Node is passed without a root, it will be parented to it's sibling // child (auto-nesting). // // tree.Root("Foo").Child(tree.Root("Bar").Child("Baz"), "Qux") @@ -318,16 +302,16 @@ func (t *Item) RootStyle(style lipgloss.Style) *Item { // ├── Bar // │ └── Baz // └── Qux -func (t *Item) Child(children ...any) *Item { +func (t *Node) Child(children ...any) *Node { for _, child := range children { switch child := child.(type) { - case *Item: + case *Node: t.size = t.size + child.size t.open = t.size > 1 child.open = child.size > 1 t.tree.Child(child) default: - item := new(Item) + item := new(Node) // TODO: should I create a tree for leaf nodes? // makes the code a bit simpler item.tree = ltree.Root(child) @@ -343,8 +327,8 @@ func (t *Item) Child(children ...any) *Item { } // Root returns a new tree with the root set. -func Root(root any) *Item { - t := new(Item) +func Root(root any) *Node { + t := new(Node) t.size = 1 t.open = true t.isRoot = true @@ -353,10 +337,9 @@ func Root(root any) *Item { } // New creates a new model with default settings. -func New(t *Item, width, height int) Model { +func New(t *Node, width, height int) Model { m := Model{ KeyMap: DefaultKeyMap, - styles: DefaultStyles(), OpenCharacter: "▼", ClosedCharacter: "▶", Help: help.New(), @@ -364,6 +347,7 @@ func New(t *Item, width, height int) Model { showHelp: true, root: t, } + m.SetStyles(DefaultStyles()) m.setSize(width, height) m.setAttributes() m.updateStyles() @@ -444,18 +428,18 @@ func (m Model) View() string { // SetStyles sets the styles for this component. func (m *Model) SetStyles(styles Styles) { - if styles.ItemStyleFunc != nil { - styles.itemFunc = styles.ItemStyleFunc + if styles.NodeStyleFunc != nil { + styles.nodeFunc = styles.NodeStyleFunc } else { - styles.itemFunc = func(_ Items, _ int) lipgloss.Style { - return styles.ItemStyle + styles.nodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return styles.NodeStyle } } - if styles.SelectedItemStyleFunc != nil { - styles.selectedItemFunc = styles.SelectedItemStyleFunc + if styles.SelectedNodeStyleFunc != nil { + styles.selectedNodeFunc = styles.SelectedNodeStyleFunc } else { - styles.selectedItemFunc = func(_ Items, _ int) lipgloss.Style { - return styles.SelectedItemStyle + styles.selectedNodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return styles.SelectedNodeStyle } } m.styles = styles @@ -533,9 +517,9 @@ func (m Model) helpView() string { return m.styles.HelpStyle.Render(m.Help.View(m)) } -// FlatItems returns all items in the tree as a flat list. -func (m *Model) FlatItems() []*Item { - return m.root.FlatItems() +// FlatNodes returns all items in the tree as a flat list. +func (m *Model) FlatNodes() []*Node { + return m.root.FlatNodes() } func (m *Model) setAttributes() { @@ -544,48 +528,48 @@ func (m *Model) setAttributes() { setYOffsets(m.root) } -// FlatItems returns all items in the tree as a flat list. -func (t *Item) FlatItems() []*Item { - res := []*Item{t} +// FlatNodes returns all descendant items in as a flat list. +func (t *Node) FlatNodes() []*Node { + res := []*Node{t} children := t.tree.Children() for i := 0; i < children.Length(); i++ { child := children.At(i) - res = append(res, child.(*Item).FlatItems()...) + res = append(res, child.(*Node).FlatNodes()...) } return res } -// setSizes updates each Item's size +// setSizes updates each Node's size // Note that if a child isn't open, its size is 1 -func setDepths(t *Item, depth int) { +func setDepths(t *Node, depth int) { t.depth = depth children := t.tree.Children() for i := 0; i < children.Length(); i++ { child := children.At(i) - setDepths(child.(*Item), depth+1) + setDepths(child.(*Node), depth+1) } } -// setSizes updates each Item's size +// setSizes updates each Node's size // Note that if a child isn't open, its size is 1 -func setSizes(t *Item) int { +func setSizes(t *Node) int { children := t.tree.Children() size := 1 + children.Length() for i := 0; i < children.Length(); i++ { child := children.At(i) - size = size + setSizes(child.(*Item)) - 1 + size = size + setSizes(child.(*Node)) - 1 } t.size = size return size } -// setYOffsets updates each Item's yOffset based on how many items are "above" it -func setYOffsets(t *Item) { +// setYOffsets updates each Node's yOffset based on how many items are "above" it +func setYOffsets(t *Node) { children := t.tree.Children() above := 0 for i := 0; i < children.Length(); i++ { child := children.At(i) - if child, ok := child.(*Item); ok { + if child, ok := child.(*Node); ok { child.yOffset = t.yOffset + above + i + 1 setYOffsets(child) above += child.size - 1 @@ -599,29 +583,28 @@ func (m *Model) YOffset() int { return m.yOffset } -// Item returns the item at the given yoffset -func (m *Model) Item(yoffset int) *Item { +// Node returns the item at the given yoffset +func (m *Model) Node(yoffset int) *Node { return findNode(m.root, yoffset) } -// ItemAtCurrentOffset returns the item at the current yoffset -func (m *Model) ItemAtCurrentOffset() *Item { +// NodeAtCurrentOffset returns the item at the current yoffset +func (m *Model) NodeAtCurrentOffset() *Node { return findNode(m.root, m.yOffset) } // Since the selected node changes, we need to capture m.yOffset in the // style function's closure again func (m *Model) updateStyles() { - m.root.rootStyle = m.nodeStyle(m.root) + m.root.rootStyle = m.rootStyle() // TODO: add RootStyleFunc to the Node interface? m.root.RootStyle(m.root.rootStyle) m.root.ItemStyleFunc(m.selectedNodeStyle()) - items := m.FlatItems() + items := m.FlatNodes() opts := &itemOptions{ openCharacter: m.OpenCharacter, closedCharacter: m.ClosedCharacter, - treeWidth: m.width, treeYOffset: m.yOffset, } for _, item := range items { @@ -632,33 +615,37 @@ func (m *Model) updateStyles() { // selectedNodeStyle sets the node style // and takes into account whether it's selected or not func (m *Model) selectedNodeStyle() StyleFunc { - return func(children Items, i int) lipgloss.Style { + return func(children Nodes, i int) lipgloss.Style { child := children.At(i) if child.yOffset == m.yOffset { - return m.styles.selectedItemFunc(children, i) + return m.styles.selectedNodeFunc(children, i) } - return m.styles.itemFunc(children, i) + return m.styles.nodeFunc(children, i) } } -func (m *Model) nodeStyle(node ltree.Node) lipgloss.Style { - if node, ok := node.(*Item); ok { - if node.yOffset == m.yOffset { - return m.styles.SelectedNode - } +func (m *Model) rootStyle() lipgloss.Style { + if m.styles.nodeFunc == nil || m.styles.selectedNodeFunc == nil { + return lipgloss.NewStyle() + } + if m.root.yOffset == m.yOffset { + s := m.styles.selectedNodeFunc(Nodes{m.root}, 0) + // TODO: if we call Value on the root node in lipgloss, we wouldn't need this + return s.Width(s.GetWidth() - lipgloss.Width(m.OpenCharacter) - 1) } - return m.styles.ItemStyle + s := m.styles.nodeFunc(Nodes{m.root}, 0) + return s.Width(s.GetWidth() - lipgloss.Width(m.OpenCharacter) - 1) } // TODO: remove -func printDebugInfo(t *Item) string { - debug := fmt.Sprintf("size=%2d y=%2d depth=%2d padding=%2d", t.size, t.yOffset, t.depth, t.contentWidth()) +func printDebugInfo(t *Node) string { + debug := fmt.Sprintf("size=%2d y=%2d depth=%2d", t.size, t.yOffset, t.depth) children := t.Children() for i := 0; i < children.Length(); i++ { child := children.At(i) - if child, ok := child.(*Item); ok { + if child, ok := child.(*Node); ok { debug = debug + "\n" + printDebugInfo(child) } } @@ -668,7 +655,7 @@ func printDebugInfo(t *Item) string { // findNode starts a DFS search for the node with the given yOffset // starting from the given item -func findNode(t *Item, yOffset int) *Item { +func findNode(t *Node, yOffset int) *Node { if t.yOffset == yOffset { return t } @@ -676,7 +663,7 @@ func findNode(t *Item, yOffset int) *Item { children := t.tree.Children() for i := 0; i < children.Length(); i++ { child := children.At(i) - if child, ok := child.(*Item); ok { + if child, ok := child.(*Node); ok { found := findNode(child, yOffset) if found != nil { return found From e825b9b0ba467afa5efeab5490e21f6ae3aecdd6 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 19 Oct 2024 20:38:09 +0300 Subject: [PATCH 13/29] feat(tree): expose GivenValue --- tree/tree.go | 52 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index f941e204f..42f640711 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -138,6 +138,9 @@ type Node struct { isRoot bool open bool + // value is the root value of the node + value any + // TODO: expose a getter for this in lipgloss rootStyle lipgloss.Style @@ -197,6 +200,11 @@ func (t *Node) Value() string { return s.Render(t.tree.Value()) } +// GivenValue returns the value passed to the node. +func (t *Node) GivenValue() any { + return t.value +} + // Children returns the children of an item. func (t *Node) Children() ltree.Children { return t.tree.Children() @@ -262,6 +270,32 @@ func (t *Node) Enumerator(enumerator ltree.Enumerator) *Node { return t } +// Indenter sets the indenter implementation. This is used to change the way +// the tree is indented. The default indentor places a border connecting sibling +// elements and no border for the last child. +// +// └── Foo +// └── Bar +// └── Baz +// └── Qux +// └── Quux +// +// You can define your own indenter. +// +// func ArrowIndenter(children tree.Children, index int) string { +// return "→ " +// } +// +// → Foo +// → → Bar +// → → → Baz +// → → → → Qux +// → → → → → Quux +func (t *Node) Indenter(indenter ltree.Indenter) *Node { + t.tree.Indenter(indenter) + return t +} + // EnumeratorStyle sets a static style for all enumerators. // // Use EnumeratorStyleFunc to conditionally set styles based on the tree node. @@ -317,6 +351,7 @@ func (t *Node) Child(children ...any) *Node { item.tree = ltree.Root(child) item.size = 1 item.open = false + item.value = child t.size = t.size + item.size t.open = t.size > 1 t.tree.Child(item) @@ -331,6 +366,7 @@ func Root(root any) *Node { t := new(Node) t.size = 1 t.open = true + t.value = root t.isRoot = true t.tree = ltree.Root(root) return t @@ -394,11 +430,11 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { // View renders the component. func (m Model) View() string { - var s, t, debug, cursor string + var treeView, leftDebugView, cursor string // TODO: remove if os.Getenv("DEBUG") == "true" { - s += fmt.Sprintf("y=%2d\n", m.yOffset) - debug = printDebugInfo(m.root) + " " + // topDebugView += fmt.Sprintf("y=%2d\n", m.yOffset) + leftDebugView = printDebugInfo(m.root) + " " for i := 0; i < m.root.size; i++ { if i == m.yOffset { cursor = cursor + "👉 " @@ -409,13 +445,13 @@ func (m Model) View() string { } } - t = m.styles.TreeStyle.Render(m.root.String()) + treeView = m.styles.TreeStyle.Render(m.root.String()) - t = lipgloss.JoinHorizontal( + treeView = lipgloss.JoinHorizontal( lipgloss.Top, - debug, + leftDebugView, cursor, - t, + treeView, ) var help string @@ -423,7 +459,7 @@ func (m Model) View() string { help = m.helpView() } - return lipgloss.JoinVertical(lipgloss.Left, s, t, help) + return lipgloss.JoinVertical(lipgloss.Left, treeView, help) } // SetStyles sets the styles for this component. From 93b09ccfc38e0550484252b1cf0a2342fe978d21 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 19 Oct 2024 20:39:53 +0300 Subject: [PATCH 14/29] chore --- tree/tree.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index 42f640711..c1abebd1a 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -141,7 +141,7 @@ type Node struct { // value is the root value of the node value any - // TODO: expose a getter for this in lipgloss + // TODO: expose a getter for this in lipgloss? rootStyle lipgloss.Style opts *itemOptions @@ -247,7 +247,7 @@ func (t *Node) ItemStyle(s lipgloss.Style) *Node { func (t *Node) ItemStyleFunc(f StyleFunc) *Node { t.tree.ItemStyleFunc(func(children ltree.Children, i int) lipgloss.Style { c := make(Nodes, children.Length()) - // TODO: if we expose Depth and Size, we can avoid this + // TODO: if we expose Depth and Size in lipgloss, we can avoid this for i := 0; i < children.Length(); i++ { c[i] = children.At(i).(*Node) } @@ -346,8 +346,6 @@ func (t *Node) Child(children ...any) *Node { t.tree.Child(child) default: item := new(Node) - // TODO: should I create a tree for leaf nodes? - // makes the code a bit simpler item.tree = ltree.Root(child) item.size = 1 item.open = false From a470d252b854faab427b9e5fad4b6738a34c1dce Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Wed, 23 Oct 2024 11:10:54 +0300 Subject: [PATCH 15/29] feat: viewport with scrolloff --- tree/tree.go | 51 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index c1abebd1a..094c655e9 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/viewport" ) // StyleFunc allows the tree to be styled per item. @@ -97,6 +98,8 @@ var DefaultKeyMap = KeyMap{ // Model is the Bubble Tea model for this tree element. type Model struct { showHelp bool + // ScrollOff is the minimal number of lines to keep visible above and below the selected node. + ScrollOff int // OpenCharacter is the character used to represent an open node. OpenCharacter string // ClosedCharacter is the character used to represent a closed node. @@ -115,9 +118,11 @@ type Model struct { AdditionalShortHelpKeys func() []key.Binding AdditionalFullHelpKeys func() []key.Binding - root *Node - width int - height int + root *Node + + viewport viewport.Model + width int + height int // yOffset is the vertical offset of the selected node. yOffset int } @@ -377,14 +382,17 @@ func New(t *Node, width, height int) Model { OpenCharacter: "▼", ClosedCharacter: "▶", Help: help.New(), + ScrollOff: 5, showHelp: true, root: t, + viewport: viewport.Model{}, } m.SetStyles(DefaultStyles()) m.setSize(width, height) m.setAttributes() m.updateStyles() + m.viewport.SetContent(m.root.String()) return m } @@ -397,8 +405,10 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch { case key.Matches(msg, m.KeyMap.Down): m.yOffset = min(m.root.size-1, m.yOffset+1) + m.updateViewport(1) case key.Matches(msg, m.KeyMap.Up): m.yOffset = max(0, m.yOffset-1) + m.updateViewport(-1) case key.Matches(msg, m.KeyMap.Toggle): node := findNode(m.root, m.yOffset) if node == nil { @@ -411,7 +421,9 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { node.tree.Offset(0, node.tree.Children().Length()) } m.setAttributes() + diff := m.yOffset - node.yOffset m.yOffset = node.yOffset + m.updateViewport(diff) case key.Matches(msg, m.KeyMap.Quit): return m, tea.Quit case key.Matches(msg, m.KeyMap.ShowFullHelp): @@ -422,7 +434,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } // not sure why, but I think m.yOffset is captured in the closure, so we need to update the styles - m.updateStyles() return m, tea.Batch(cmds...) } @@ -443,7 +454,7 @@ func (m Model) View() string { } } - treeView = m.styles.TreeStyle.Render(m.root.String()) + treeView = m.styles.TreeStyle.Render(m.viewport.View()) treeView = lipgloss.JoinHorizontal( lipgloss.Top, @@ -460,6 +471,32 @@ func (m Model) View() string { return lipgloss.JoinVertical(lipgloss.Left, treeView, help) } +func (m *Model) updateViewport(movement int) { + m.updateStyles() + m.viewport.SetContent(m.root.String()) + if movement == 0 { + return + } + + // make sure there are enough lines above and below the selected node + height := m.viewport.VisibleLineCount() + scrolloff := min(m.ScrollOff, height/2) + + minTop := max(m.yOffset-scrolloff, 0) + minBottom := min(m.viewport.TotalLineCount()-1, m.yOffset+scrolloff) + isMinTopVisible := m.viewport.YOffset <= minTop + // 0 + 8 8 + isMinBottomVisible := m.viewport.YOffset+height >= minBottom+1 + + if !isMinTopVisible { + // reveal more lines above + m.viewport.SetYOffset(minTop) + } else if !isMinBottomVisible { + // reveal more lines below + m.viewport.SetYOffset(minBottom - height + 1) + } +} + // SetStyles sets the styles for this component. func (m *Model) SetStyles(styles Styles) { if styles.NodeStyleFunc != nil { @@ -477,7 +514,7 @@ func (m *Model) SetStyles(styles Styles) { } } m.styles = styles - m.updateStyles() + m.updateViewport(0) } // SetShowHelp shows or hides the help view. @@ -503,6 +540,8 @@ func (m *Model) SetHeight(v int) { func (m *Model) setSize(width, height int) { m.width = width m.height = height + m.viewport.Width = width + m.viewport.Height = height - lipgloss.Height(m.helpView()) m.Help.Width = width } From 80b5457d08f7dc7ae0beca11c43249aa85f04983 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Wed, 23 Oct 2024 17:35:43 +0300 Subject: [PATCH 16/29] feat: ability to initialize tree as closed --- tree/tree.go | 99 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 67 insertions(+), 32 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index 094c655e9..3f69cc799 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -33,6 +33,7 @@ type Styles struct { // DefaultStyles returns a set of default style definitions for this tree // component. func DefaultStyles() (s Styles) { + s.TreeStyle = lipgloss.NewStyle() s.NodeStyle = lipgloss.NewStyle() s.SelectedNodeStyle = lipgloss.NewStyle(). Background(lipgloss.Color("62")). @@ -140,8 +141,9 @@ type Node struct { depth int // isRoot is true for every Node which was added with tree.Root - isRoot bool - open bool + isRoot bool + initialClosed bool + open bool // value is the root value of the node value any @@ -176,6 +178,28 @@ func (t *Node) YOffset() int { return t.yOffset } +// Close closes the node. +func (t *Node) Close() *Node { + t.initialClosed = true + t.open = false + // reset the offset to 0,0 first + t.tree.Offset(0, 0) + t.tree.Offset(t.tree.Children().Length(), 0) + return t +} + +// Open opens the node. +func (t *Node) Open() *Node { + t.open = true + t.tree.Offset(0, 0) + return t +} + +// IsOpen returns whether the node is open. +func (t *Node) IsOpen() bool { + return t.open +} + type itemOptions struct { openCharacter string closedCharacter string @@ -346,9 +370,12 @@ func (t *Node) Child(children ...any) *Node { switch child := child.(type) { case *Node: t.size = t.size + child.size - t.open = t.size > 1 - child.open = child.size > 1 t.tree.Child(child) + + // Close the node again as the number of children as changed + if t.initialClosed { + t.Close() + } default: item := new(Node) item.tree = ltree.Root(child) @@ -356,8 +383,12 @@ func (t *Node) Child(children ...any) *Node { item.open = false item.value = child t.size = t.size + item.size - t.open = t.size > 1 t.tree.Child(item) + + // Close the node again as the number of children as changed + if t.initialClosed { + t.Close() + } } } @@ -368,8 +399,8 @@ func (t *Node) Child(children ...any) *Node { func Root(root any) *Node { t := new(Node) t.size = 1 - t.open = true t.value = root + t.open = true t.isRoot = true t.tree = ltree.Root(root) return t @@ -389,7 +420,7 @@ func New(t *Node, width, height int) Model { viewport: viewport.Model{}, } m.SetStyles(DefaultStyles()) - m.setSize(width, height) + m.SetSize(width, height) m.setAttributes() m.updateStyles() m.viewport.SetContent(m.root.String()) @@ -414,11 +445,11 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if node == nil { break } - node.open = !node.open - if node.open { + node.open = !node.IsOpen() + if node.IsOpen() { node.tree.Offset(0, 0) } else { - node.tree.Offset(0, node.tree.Children().Length()) + node.tree.Offset(node.tree.Children().Length(), 0) } m.setAttributes() diff := m.yOffset - node.yOffset @@ -439,10 +470,9 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { // View renders the component. func (m Model) View() string { - var treeView, leftDebugView, cursor string + var leftDebugView, cursor string // TODO: remove if os.Getenv("DEBUG") == "true" { - // topDebugView += fmt.Sprintf("y=%2d\n", m.yOffset) leftDebugView = printDebugInfo(m.root) + " " for i := 0; i < m.root.size; i++ { if i == m.yOffset { @@ -454,13 +484,11 @@ func (m Model) View() string { } } - treeView = m.styles.TreeStyle.Render(m.viewport.View()) - - treeView = lipgloss.JoinHorizontal( + treeView := lipgloss.JoinHorizontal( lipgloss.Top, leftDebugView, cursor, - treeView, + m.viewport.View(), ) var help string @@ -473,7 +501,8 @@ func (m Model) View() string { func (m *Model) updateViewport(movement int) { m.updateStyles() - m.viewport.SetContent(m.root.String()) + m.viewport.Style = m.styles.TreeStyle + m.viewport.SetContent(m.styles.TreeStyle.Render(m.root.String())) if movement == 0 { return } @@ -481,18 +510,12 @@ func (m *Model) updateViewport(movement int) { // make sure there are enough lines above and below the selected node height := m.viewport.VisibleLineCount() scrolloff := min(m.ScrollOff, height/2) - minTop := max(m.yOffset-scrolloff, 0) minBottom := min(m.viewport.TotalLineCount()-1, m.yOffset+scrolloff) - isMinTopVisible := m.viewport.YOffset <= minTop - // 0 + 8 8 - isMinBottomVisible := m.viewport.YOffset+height >= minBottom+1 - if !isMinTopVisible { - // reveal more lines above + if m.viewport.YOffset > minTop { // reveal more lines above m.viewport.SetYOffset(minTop) - } else if !isMinBottomVisible { - // reveal more lines below + } else if m.viewport.YOffset+height < minBottom+1 { // reveal more lines below m.viewport.SetYOffset(minBottom - height + 1) } } @@ -520,28 +543,40 @@ func (m *Model) SetStyles(styles Styles) { // SetShowHelp shows or hides the help view. func (m *Model) SetShowHelp(v bool) { m.showHelp = v + m.SetSize(m.width, m.height) } -// SetSize sets the width and height of this component. -func (m *Model) SetSize(width, height int) { - m.setSize(width, height) +// Width returns the current width setting. +func (m Model) Width() int { + return m.width +} + +// Height returns the current height setting. +func (m Model) Height() int { + return m.height } // SetWidth sets the width of this component. func (m *Model) SetWidth(v int) { - m.setSize(v, m.height) + m.SetSize(v, m.height) } // SetHeight sets the height of this component. func (m *Model) SetHeight(v int) { - m.setSize(m.width, v) + m.SetSize(m.width, v) } -func (m *Model) setSize(width, height int) { +// SetSize sets the width and height of this component. +func (m *Model) SetSize(width, height int) { m.width = width m.height = height + m.viewport.Width = width - m.viewport.Height = height - lipgloss.Height(m.helpView()) + hv := 0 + if m.showHelp { + hv = lipgloss.Height(m.helpView()) + } + m.viewport.Height = height - hv m.Help.Width = width } From 2118087543c3500216d451a319d4326459c4dd35 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Thu, 24 Oct 2024 17:22:37 +0300 Subject: [PATCH 17/29] feat: more viewport navigation - pageup/down/top/bottom etc --- tree/tree.go | 72 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index 3f69cc799..9b2cd72be 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -51,10 +51,19 @@ func DefaultStyles() (s Styles) { return s } +const spacebar = " " + // KeyMap is the key bindings for different actions within the tree. type KeyMap struct { - Down key.Binding - Up key.Binding + Down key.Binding + Up key.Binding + PageDown key.Binding + PageUp key.Binding + HalfPageUp key.Binding + HalfPageDown key.Binding + GoToTop key.Binding + GoToBottom key.Binding + Toggle key.Binding // Help toggle keybindings. @@ -75,6 +84,31 @@ var DefaultKeyMap = KeyMap{ key.WithKeys("up", "k", "ctrl+p"), key.WithHelp("↑/k", "up"), ), + PageDown: key.NewBinding( + key.WithKeys("pgdown", spacebar, "f"), + key.WithHelp("f/pgdn", "page down"), + ), + PageUp: key.NewBinding( + key.WithKeys("pgup", "b"), + key.WithHelp("b/pgup", "page up"), + ), + HalfPageDown: key.NewBinding( + key.WithKeys("d", "ctrl+d"), + key.WithHelp("d", "½ page down"), + ), + HalfPageUp: key.NewBinding( + key.WithKeys("u", "ctrl+u"), + key.WithHelp("u", "½ page up"), + ), + GoToTop: key.NewBinding( + key.WithKeys("g", "home"), + key.WithHelp("g", "top"), + ), + GoToBottom: key.NewBinding( + key.WithKeys("G", "end"), + key.WithHelp("G", "bottom"), + ), + Toggle: key.NewBinding( key.WithKeys("enter"), key.WithHelp("⏎", "toggle"), @@ -435,11 +469,22 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { case tea.KeyMsg: switch { case key.Matches(msg, m.KeyMap.Down): - m.yOffset = min(m.root.size-1, m.yOffset+1) m.updateViewport(1) case key.Matches(msg, m.KeyMap.Up): - m.yOffset = max(0, m.yOffset-1) m.updateViewport(-1) + case key.Matches(msg, m.KeyMap.PageDown): + m.updateViewport(m.viewport.Height) + case key.Matches(msg, m.KeyMap.PageUp): + m.updateViewport(-m.viewport.Height) + case key.Matches(msg, m.KeyMap.HalfPageDown): + m.updateViewport(m.viewport.Height / 2) + case key.Matches(msg, m.KeyMap.HalfPageUp): + m.updateViewport(-m.viewport.Height / 2) + case key.Matches(msg, m.KeyMap.GoToTop): + m.updateViewport(-m.yOffset) + case key.Matches(msg, m.KeyMap.GoToBottom): + m.updateViewport(m.root.size) + case key.Matches(msg, m.KeyMap.Toggle): node := findNode(m.root, m.yOffset) if node == nil { @@ -452,9 +497,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { node.tree.Offset(node.tree.Children().Length(), 0) } m.setAttributes() - diff := m.yOffset - node.yOffset - m.yOffset = node.yOffset - m.updateViewport(diff) + m.updateViewport(m.yOffset - node.yOffset) case key.Matches(msg, m.KeyMap.Quit): return m, tea.Quit case key.Matches(msg, m.KeyMap.ShowFullHelp): @@ -500,6 +543,7 @@ func (m Model) View() string { } func (m *Model) updateViewport(movement int) { + m.yOffset = max(min(m.root.size-1, m.yOffset+movement), 0) m.updateStyles() m.viewport.Style = m.styles.TreeStyle m.viewport.SetContent(m.styles.TreeStyle.Render(m.root.String())) @@ -584,8 +628,8 @@ func (m *Model) SetSize(width, height int) { // of the help.KeyMap interface. func (m Model) ShortHelp() []key.Binding { kb := []key.Binding{ - m.KeyMap.Up, m.KeyMap.Down, + m.KeyMap.Up, m.KeyMap.Toggle, } @@ -603,10 +647,20 @@ func (m Model) ShortHelp() []key.Binding { func (m Model) FullHelp() [][]key.Binding { kb := [][]key.Binding{ { - m.KeyMap.Up, m.KeyMap.Down, + m.KeyMap.Up, m.KeyMap.Toggle, }, + { + m.KeyMap.PageDown, + m.KeyMap.PageUp, + m.KeyMap.HalfPageDown, + m.KeyMap.HalfPageUp, + }, + { + m.KeyMap.GoToTop, + m.KeyMap.GoToBottom, + }, } if m.AdditionalFullHelpKeys != nil { From 5cf4211ea65f0fb1c29fb57453b66b490e6883ab Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Thu, 24 Oct 2024 17:25:17 +0300 Subject: [PATCH 18/29] revert: remove unused selection cursor --- tree/tree.go | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index 9b2cd72be..31740a6a8 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -19,7 +19,6 @@ type StyleFunc func(children Nodes, i int) lipgloss.Style // Styles contains style definitions for this tree component. By default, these // values are generated by DefaultStyles. type Styles struct { - SelectionCursor lipgloss.Style HelpStyle lipgloss.Style TreeStyle lipgloss.Style selectedNodeFunc StyleFunc @@ -45,7 +44,6 @@ func DefaultStyles() (s Styles) { s.selectedNodeFunc = func(_ Nodes, _ int) lipgloss.Style { return s.SelectedNodeStyle } - s.SelectionCursor = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) s.HelpStyle = lipgloss.NewStyle().PaddingTop(1) return s @@ -513,24 +511,15 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { // View renders the component. func (m Model) View() string { - var leftDebugView, cursor string + var leftDebugView string // TODO: remove if os.Getenv("DEBUG") == "true" { leftDebugView = printDebugInfo(m.root) + " " - for i := 0; i < m.root.size; i++ { - if i == m.yOffset { - cursor = cursor + "👉 " - } else { - cursor = cursor + " " - } - cursor = cursor + "\n" - } } treeView := lipgloss.JoinHorizontal( lipgloss.Top, leftDebugView, - cursor, m.viewport.View(), ) From b1140adba4188b2bec633f518f1087708fc84b1d Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Thu, 24 Oct 2024 17:40:05 +0300 Subject: [PATCH 19/29] feat: open/close keybinds --- tree/tree.go | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index 31740a6a8..2c19a744f 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -63,6 +63,8 @@ type KeyMap struct { GoToBottom key.Binding Toggle key.Binding + Open key.Binding + Close key.Binding // Help toggle keybindings. ShowFullHelp key.Binding @@ -111,6 +113,14 @@ var DefaultKeyMap = KeyMap{ key.WithKeys("enter"), key.WithHelp("⏎", "toggle"), ), + Open: key.NewBinding( + key.WithKeys("l", "right"), + key.WithHelp("→/l", "open"), + ), + Close: key.NewBinding( + key.WithKeys("h", "left"), + key.WithHelp("←/h", "close"), + ), // Toggle help. ShowFullHelp: key.NewBinding( @@ -488,14 +498,20 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if node == nil { break } - node.open = !node.IsOpen() - if node.IsOpen() { - node.tree.Offset(0, 0) - } else { - node.tree.Offset(node.tree.Children().Length(), 0) + m.toggleNode(node, !node.IsOpen()) + case key.Matches(msg, m.KeyMap.Open): + node := findNode(m.root, m.yOffset) + if node == nil { + break + } + m.toggleNode(node, true) + case key.Matches(msg, m.KeyMap.Close): + node := findNode(m.root, m.yOffset) + if node == nil { + break } - m.setAttributes() - m.updateViewport(m.yOffset - node.yOffset) + m.toggleNode(node, false) + case key.Matches(msg, m.KeyMap.Quit): return m, tea.Quit case key.Matches(msg, m.KeyMap.ShowFullHelp): @@ -531,6 +547,18 @@ func (m Model) View() string { return lipgloss.JoinVertical(lipgloss.Left, treeView, help) } +func (m *Model) toggleNode(node *Node, open bool) { + node.open = open + + // reset the offset to 0,0 first + node.tree.Offset(0, 0) + if !open { + node.tree.Offset(node.tree.Children().Length(), 0) + } + m.setAttributes() + m.updateViewport(m.yOffset - node.yOffset) +} + func (m *Model) updateViewport(movement int) { m.yOffset = max(min(m.root.size-1, m.yOffset+movement), 0) m.updateStyles() @@ -638,7 +666,11 @@ func (m Model) FullHelp() [][]key.Binding { { m.KeyMap.Down, m.KeyMap.Up, + }, + { m.KeyMap.Toggle, + m.KeyMap.Open, + m.KeyMap.Close, }, { m.KeyMap.PageDown, From 5f5b2baf510ba80371175866ce704cf7ee7c97d6 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Thu, 24 Oct 2024 18:32:28 +0300 Subject: [PATCH 20/29] refactor: move node to node.go --- tree/node.go | 284 +++++++++++++++++++++++++++++++++++++++++++++++++++ tree/tree.go | 279 -------------------------------------------------- 2 files changed, 284 insertions(+), 279 deletions(-) create mode 100644 tree/node.go diff --git a/tree/node.go b/tree/node.go new file mode 100644 index 000000000..5a223a921 --- /dev/null +++ b/tree/node.go @@ -0,0 +1,284 @@ +package tree + +import ( + "github.com/charmbracelet/lipgloss" + ltree "github.com/charmbracelet/lipgloss/tree" +) + +// Node is a a node in the tree +// Node implements lipgloss's tree.Node +type Node struct { + // tree is used as the renderer layer + tree *ltree.Tree + + // yOffset is the vertical offset of the selected node. + yOffset int + + // depth is the depth of the node in the tree + depth int + + // isRoot is true for every Node which was added with tree.Root + isRoot bool + initialClosed bool + open bool + + // value is the root value of the node + value any + + // TODO: expose a getter for this in lipgloss? + rootStyle lipgloss.Style + + opts *itemOptions + + // TODO: move to lipgloss.Tree? + size int +} + +// IsSelected returns whether this item is selected. +func (t *Node) IsSelected() bool { + return t.yOffset == t.opts.treeYOffset +} + +// Depth returns the depth of the node in the tree. +func (t *Node) Depth() int { + return t.depth +} + +// Size returns the number of nodes in the tree. +// Note that if a child isn't open, its size is 1 +func (t *Node) Size() int { + return t.size +} + +// YOffset returns the vertical offset of the Node +func (t *Node) YOffset() int { + return t.yOffset +} + +// Close closes the node. +func (t *Node) Close() *Node { + t.initialClosed = true + t.open = false + // reset the offset to 0,0 first + t.tree.Offset(0, 0) + t.tree.Offset(t.tree.Children().Length(), 0) + return t +} + +// Open opens the node. +func (t *Node) Open() *Node { + t.open = true + t.tree.Offset(0, 0) + return t +} + +// IsOpen returns whether the node is open. +func (t *Node) IsOpen() bool { + return t.open +} + +type itemOptions struct { + openCharacter string + closedCharacter string + treeYOffset int +} + +// Used to print the Node's tree +// TODO: Value is not called on the root node, so we need to repeat the open/closed character +// Should this be fixed in lipgloss? +func (t *Node) String() string { + s := t.rootStyle.UnsetWidth() + if t.open { + return s.Render(t.opts.openCharacter+" ") + t.tree.String() + } + return s.Render(t.opts.closedCharacter+" ") + t.tree.String() +} + +// Value returns the root name of this node. +func (t *Node) Value() string { + s := lipgloss.NewStyle() + if t.isRoot { + if t.open { + return s.Render(t.opts.openCharacter + " " + t.tree.Value()) + } + return s.Render(t.opts.closedCharacter + " " + t.tree.Value()) + } + return s.Render(t.tree.Value()) +} + +// GivenValue returns the value passed to the node. +func (t *Node) GivenValue() any { + return t.value +} + +// Children returns the children of an item. +func (t *Node) Children() ltree.Children { + return t.tree.Children() +} + +// Hidden returns whether this item is hidden. +func (t *Node) Hidden() bool { + return t.tree.Hidden() +} + +// Nodes are a list of tree nodes. +type Nodes []*Node + +// Children returns the children of an item. +func (t Nodes) At(index int) *Node { + return t[index] +} + +// Children returns the children of an item. +func (t Nodes) Length() int { + return len(t) +} + +// ItemStyle sets a static style for all items. +func (t *Node) ItemStyle(s lipgloss.Style) *Node { + t.tree.ItemStyle(s) + return t +} + +// ItemStyleFunc sets the item style function. Use this for conditional styling. +// For example: +// +// t := tree.Root("root"). +// ItemStyleFunc(func(_ tree.Nodes, i int) lipgloss.Style { +// if selected == i { +// return lipgloss.NewStyle().Foreground(hightlightColor) +// } +// return lipgloss.NewStyle().Foreground(dimColor) +// }) +func (t *Node) ItemStyleFunc(f StyleFunc) *Node { + t.tree.ItemStyleFunc(func(children ltree.Children, i int) lipgloss.Style { + c := make(Nodes, children.Length()) + // TODO: if we expose Depth and Size in lipgloss, we can avoid this + for i := 0; i < children.Length(); i++ { + c[i] = children.At(i).(*Node) + } + return f(c, i) + }) + return t +} + +// TODO: support IndentStyleFunc in lipgloss so we can have a full background for the item + +// TODO: should we re-export RoundedEnumerator from lipgloss? +// Enumerator sets the enumerator implementation. This can be used to change the +// way the branches indicators look. Lipgloss includes predefined enumerators +// for a classic or rounded tree. For example, you can have a rounded tree: +// +// tree.New(). +// Enumerator(ltree.RoundedEnumerator) +func (t *Node) Enumerator(enumerator ltree.Enumerator) *Node { + t.tree.Enumerator(enumerator) + return t +} + +// Indenter sets the indenter implementation. This is used to change the way +// the tree is indented. The default indentor places a border connecting sibling +// elements and no border for the last child. +// +// └── Foo +// └── Bar +// └── Baz +// └── Qux +// └── Quux +// +// You can define your own indenter. +// +// func ArrowIndenter(children tree.Children, index int) string { +// return "→ " +// } +// +// → Foo +// → → Bar +// → → → Baz +// → → → → Qux +// → → → → → Quux +func (t *Node) Indenter(indenter ltree.Indenter) *Node { + t.tree.Indenter(indenter) + return t +} + +// EnumeratorStyle sets a static style for all enumerators. +// +// Use EnumeratorStyleFunc to conditionally set styles based on the tree node. +func (t *Node) EnumeratorStyle(style lipgloss.Style) *Node { + t.tree.EnumeratorStyle(style) + return t +} + +// EnumeratorStyleFunc sets the enumeration style function. Use this function +// for conditional styling. +// +// t := tree.Root("root"). +// EnumeratorStyleFunc(func(_ tree.Children, i int) lipgloss.Style { +// if selected == i { +// return lipgloss.NewStyle().Foreground(hightlightColor) +// } +// return lipgloss.NewStyle().Foreground(dimColor) +// }) +func (t *Node) EnumeratorStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Node { + t.tree.EnumeratorStyleFunc(f) + return t +} + +// RootStyle sets a style for the root element. +func (t *Node) RootStyle(style lipgloss.Style) *Node { + t.tree.RootStyle(style) + return t +} + +// Child adds a child to this tree. +// +// If a Child Node is passed without a root, it will be parented to it's sibling +// child (auto-nesting). +// +// tree.Root("Foo").Child(tree.Root("Bar").Child("Baz"), "Qux") +// +// ├── Foo +// ├── Bar +// │ └── Baz +// └── Qux +func (t *Node) Child(children ...any) *Node { + for _, child := range children { + switch child := child.(type) { + case *Node: + t.size = t.size + child.size + t.tree.Child(child) + + // Close the node again as the number of children as changed + if t.initialClosed { + t.Close() + } + default: + item := new(Node) + item.tree = ltree.Root(child) + item.size = 1 + item.open = false + item.value = child + t.size = t.size + item.size + t.tree.Child(item) + + // Close the node again as the number of children as changed + if t.initialClosed { + t.Close() + } + } + } + + return t +} + +// Root returns a new tree with the root set. +func Root(root any) *Node { + t := new(Node) + t.size = 1 + t.value = root + t.open = true + t.isRoot = true + t.tree = ltree.Root(root) + return t +} diff --git a/tree/tree.go b/tree/tree.go index 2c19a744f..ad1bfd134 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -6,7 +6,6 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - ltree "github.com/charmbracelet/lipgloss/tree" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" @@ -170,284 +169,6 @@ type Model struct { yOffset int } -// Node is a a node in the tree -// Node implements lipgloss's tree.Node -type Node struct { - // tree is used as the renderer layer - tree *ltree.Tree - - // yOffset is the vertical offset of the selected node. - yOffset int - - // depth is the depth of the node in the tree - depth int - - // isRoot is true for every Node which was added with tree.Root - isRoot bool - initialClosed bool - open bool - - // value is the root value of the node - value any - - // TODO: expose a getter for this in lipgloss? - rootStyle lipgloss.Style - - opts *itemOptions - - // TODO: move to lipgloss.Tree? - size int -} - -// IsSelected returns whether this item is selected. -func (t *Node) IsSelected() bool { - return t.yOffset == t.opts.treeYOffset -} - -// Depth returns the depth of the node in the tree. -func (t *Node) Depth() int { - return t.depth -} - -// Size returns the number of nodes in the tree. -// Note that if a child isn't open, its size is 1 -func (t *Node) Size() int { - return t.size -} - -// YOffset returns the vertical offset of the Node -func (t *Node) YOffset() int { - return t.yOffset -} - -// Close closes the node. -func (t *Node) Close() *Node { - t.initialClosed = true - t.open = false - // reset the offset to 0,0 first - t.tree.Offset(0, 0) - t.tree.Offset(t.tree.Children().Length(), 0) - return t -} - -// Open opens the node. -func (t *Node) Open() *Node { - t.open = true - t.tree.Offset(0, 0) - return t -} - -// IsOpen returns whether the node is open. -func (t *Node) IsOpen() bool { - return t.open -} - -type itemOptions struct { - openCharacter string - closedCharacter string - treeYOffset int -} - -// Used to print the Node's tree -// TODO: Value is not called on the root node, so we need to repeat the open/closed character -// Should this be fixed in lipgloss? -func (t *Node) String() string { - s := t.rootStyle.UnsetWidth() - if t.open { - return s.Render(t.opts.openCharacter+" ") + t.tree.String() - } - return s.Render(t.opts.closedCharacter+" ") + t.tree.String() -} - -// Value returns the root name of this node. -func (t *Node) Value() string { - s := lipgloss.NewStyle() - if t.isRoot { - if t.open { - return s.Render(t.opts.openCharacter + " " + t.tree.Value()) - } - return s.Render(t.opts.closedCharacter + " " + t.tree.Value()) - } - return s.Render(t.tree.Value()) -} - -// GivenValue returns the value passed to the node. -func (t *Node) GivenValue() any { - return t.value -} - -// Children returns the children of an item. -func (t *Node) Children() ltree.Children { - return t.tree.Children() -} - -// Hidden returns whether this item is hidden. -func (t *Node) Hidden() bool { - return t.tree.Hidden() -} - -// Nodes are a list of tree nodes. -type Nodes []*Node - -// Children returns the children of an item. -func (t Nodes) At(index int) *Node { - return t[index] -} - -// Children returns the children of an item. -func (t Nodes) Length() int { - return len(t) -} - -// ItemStyle sets a static style for all items. -func (t *Node) ItemStyle(s lipgloss.Style) *Node { - t.tree.ItemStyle(s) - return t -} - -// ItemStyleFunc sets the item style function. Use this for conditional styling. -// For example: -// -// t := tree.Root("root"). -// ItemStyleFunc(func(_ tree.Nodes, i int) lipgloss.Style { -// if selected == i { -// return lipgloss.NewStyle().Foreground(hightlightColor) -// } -// return lipgloss.NewStyle().Foreground(dimColor) -// }) -func (t *Node) ItemStyleFunc(f StyleFunc) *Node { - t.tree.ItemStyleFunc(func(children ltree.Children, i int) lipgloss.Style { - c := make(Nodes, children.Length()) - // TODO: if we expose Depth and Size in lipgloss, we can avoid this - for i := 0; i < children.Length(); i++ { - c[i] = children.At(i).(*Node) - } - return f(c, i) - }) - return t -} - -// TODO: support IndentStyleFunc in lipgloss so we can have a full background for the item - -// TODO: should we re-export RoundedEnumerator from lipgloss? -// Enumerator sets the enumerator implementation. This can be used to change the -// way the branches indicators look. Lipgloss includes predefined enumerators -// for a classic or rounded tree. For example, you can have a rounded tree: -// -// tree.New(). -// Enumerator(ltree.RoundedEnumerator) -func (t *Node) Enumerator(enumerator ltree.Enumerator) *Node { - t.tree.Enumerator(enumerator) - return t -} - -// Indenter sets the indenter implementation. This is used to change the way -// the tree is indented. The default indentor places a border connecting sibling -// elements and no border for the last child. -// -// └── Foo -// └── Bar -// └── Baz -// └── Qux -// └── Quux -// -// You can define your own indenter. -// -// func ArrowIndenter(children tree.Children, index int) string { -// return "→ " -// } -// -// → Foo -// → → Bar -// → → → Baz -// → → → → Qux -// → → → → → Quux -func (t *Node) Indenter(indenter ltree.Indenter) *Node { - t.tree.Indenter(indenter) - return t -} - -// EnumeratorStyle sets a static style for all enumerators. -// -// Use EnumeratorStyleFunc to conditionally set styles based on the tree node. -func (t *Node) EnumeratorStyle(style lipgloss.Style) *Node { - t.tree.EnumeratorStyle(style) - return t -} - -// EnumeratorStyleFunc sets the enumeration style function. Use this function -// for conditional styling. -// -// t := tree.Root("root"). -// EnumeratorStyleFunc(func(_ tree.Children, i int) lipgloss.Style { -// if selected == i { -// return lipgloss.NewStyle().Foreground(hightlightColor) -// } -// return lipgloss.NewStyle().Foreground(dimColor) -// }) -func (t *Node) EnumeratorStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Node { - t.tree.EnumeratorStyleFunc(f) - return t -} - -// RootStyle sets a style for the root element. -func (t *Node) RootStyle(style lipgloss.Style) *Node { - t.tree.RootStyle(style) - return t -} - -// Child adds a child to this tree. -// -// If a Child Node is passed without a root, it will be parented to it's sibling -// child (auto-nesting). -// -// tree.Root("Foo").Child(tree.Root("Bar").Child("Baz"), "Qux") -// -// ├── Foo -// ├── Bar -// │ └── Baz -// └── Qux -func (t *Node) Child(children ...any) *Node { - for _, child := range children { - switch child := child.(type) { - case *Node: - t.size = t.size + child.size - t.tree.Child(child) - - // Close the node again as the number of children as changed - if t.initialClosed { - t.Close() - } - default: - item := new(Node) - item.tree = ltree.Root(child) - item.size = 1 - item.open = false - item.value = child - t.size = t.size + item.size - t.tree.Child(item) - - // Close the node again as the number of children as changed - if t.initialClosed { - t.Close() - } - } - } - - return t -} - -// Root returns a new tree with the root set. -func Root(root any) *Node { - t := new(Node) - t.size = 1 - t.value = root - t.open = true - t.isRoot = true - t.tree = ltree.Root(root) - return t -} - // New creates a new model with default settings. func New(t *Node, width, height int) Model { m := Model{ From 3bf5606dc3ded5bc1e2fd3bed97047c789605de3 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Thu, 24 Oct 2024 18:44:41 +0300 Subject: [PATCH 21/29] feat: expose all scrolling methods --- tree/tree.go | 105 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 24 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index ad1bfd134..1c4b18911 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -198,40 +198,28 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { case tea.KeyMsg: switch { case key.Matches(msg, m.KeyMap.Down): - m.updateViewport(1) + m.Down() case key.Matches(msg, m.KeyMap.Up): - m.updateViewport(-1) + m.Up() case key.Matches(msg, m.KeyMap.PageDown): - m.updateViewport(m.viewport.Height) + m.PageDown() case key.Matches(msg, m.KeyMap.PageUp): - m.updateViewport(-m.viewport.Height) + m.PageUp() case key.Matches(msg, m.KeyMap.HalfPageDown): - m.updateViewport(m.viewport.Height / 2) + m.HalfPageDown() case key.Matches(msg, m.KeyMap.HalfPageUp): - m.updateViewport(-m.viewport.Height / 2) + m.HalfPageUp() case key.Matches(msg, m.KeyMap.GoToTop): - m.updateViewport(-m.yOffset) + m.GoToTop() case key.Matches(msg, m.KeyMap.GoToBottom): - m.updateViewport(m.root.size) + m.GoToBottom() case key.Matches(msg, m.KeyMap.Toggle): - node := findNode(m.root, m.yOffset) - if node == nil { - break - } - m.toggleNode(node, !node.IsOpen()) + m.ToggleCurrentNode() case key.Matches(msg, m.KeyMap.Open): - node := findNode(m.root, m.yOffset) - if node == nil { - break - } - m.toggleNode(node, true) + m.OpenCurrentNode() case key.Matches(msg, m.KeyMap.Close): - node := findNode(m.root, m.yOffset) - if node == nil { - break - } - m.toggleNode(node, false) + m.CloseCurrentNode() case key.Matches(msg, m.KeyMap.Quit): return m, tea.Quit @@ -268,6 +256,73 @@ func (m Model) View() string { return lipgloss.JoinVertical(lipgloss.Left, treeView, help) } +// Down moves the selection down by one item. +func (m *Model) Down() { + m.updateViewport(1) +} + +// Up moves the selection up by one item. +func (m *Model) Up() { + m.updateViewport(-1) +} + +// PageDown moves the selection down by one page. +func (m *Model) PageDown() { + m.updateViewport(m.viewport.Height) +} + +// PageUp moves the selection up by one page. +func (m *Model) PageUp() { + m.updateViewport(-m.viewport.Height) +} + +// HalfPageDown moves the selection down by half a page. +func (m *Model) HalfPageDown() { + m.updateViewport(m.viewport.Height / 2) +} + +// HalfPageUp moves the selection up by half a page. +func (m *Model) HalfPageUp() { + m.updateViewport(-m.viewport.Height / 2) +} + +// GoToTop moves the selection to the top of the tree. +func (m *Model) GoToTop() { + m.updateViewport(-m.yOffset) +} + +// GoToBottom moves the selection to the bottom of the tree. +func (m *Model) GoToBottom() { + m.updateViewport(m.root.size) +} + +// ToggleCurrentNode toggles the current node open/close state. +func (m *Model) ToggleCurrentNode() { + node := findNode(m.root, m.yOffset) + if node == nil { + return + } + m.toggleNode(node, !node.IsOpen()) +} + +// OpenCurrentNode opens the currently selected node. +func (m *Model) OpenCurrentNode() { + node := findNode(m.root, m.yOffset) + if node == nil { + return + } + m.toggleNode(node, true) +} + +// CloseCurrentNode closes the currently selected node. +func (m *Model) CloseCurrentNode() { + node := findNode(m.root, m.yOffset) + if node == nil { + return + } + m.toggleNode(node, false) +} + func (m *Model) toggleNode(node *Node, open bool) { node.open = open @@ -285,7 +340,9 @@ func (m *Model) updateViewport(movement int) { m.updateStyles() m.viewport.Style = m.styles.TreeStyle m.viewport.SetContent(m.styles.TreeStyle.Render(m.root.String())) - if movement == 0 { + + // if this is the initial render, make sure we show the root node + if m.yOffset == 0 && movement == 0 { return } From 1ccc644401d36257fd3ccea63c3f738496677113 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Fri, 25 Oct 2024 11:49:16 +0300 Subject: [PATCH 22/29] fix: examples --- tree/node.go | 27 ++++++++++---- tree/tree.go | 103 ++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/tree/node.go b/tree/node.go index 5a223a921..93275920b 100644 --- a/tree/node.go +++ b/tree/node.go @@ -81,29 +81,42 @@ type itemOptions struct { openCharacter string closedCharacter string treeYOffset int + styles *Styles } // Used to print the Node's tree // TODO: Value is not called on the root node, so we need to repeat the open/closed character // Should this be fixed in lipgloss? func (t *Node) String() string { - s := t.rootStyle.UnsetWidth() + s := t.opts.styles.OpenIndicatorStyle if t.open { - return s.Render(t.opts.openCharacter+" ") + t.tree.String() + return s.Render(t.opts.openCharacter) + " " + t.tree.String() } - return s.Render(t.opts.closedCharacter+" ") + t.tree.String() + return s.Render(t.opts.closedCharacter) + " " + t.tree.String() } // Value returns the root name of this node. func (t *Node) Value() string { - s := lipgloss.NewStyle() + s := t.opts.styles + var ns lipgloss.Style + + if t.yOffset == t.opts.treeYOffset { + ns = s.selectedNodeFunc(Nodes{t}, 0) + } else if t.isRoot { + ns = s.parentNodeFunc(Nodes{t}, 0) + } else { + ns = s.nodeFunc(Nodes{t}, 0) + } + + v := ns.Render(t.tree.Value()) + if t.isRoot { if t.open { - return s.Render(t.opts.openCharacter + " " + t.tree.Value()) + return s.OpenIndicatorStyle.Render(t.opts.openCharacter) + " " + v } - return s.Render(t.opts.closedCharacter + " " + t.tree.Value()) + return s.OpenIndicatorStyle.Render(t.opts.closedCharacter) + " " + v } - return s.Render(t.tree.Value()) + return v } // GivenValue returns the value passed to the node. diff --git a/tree/tree.go b/tree/tree.go index 1c4b18911..32b4f137e 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -3,6 +3,7 @@ package tree import ( "fmt" "os" + "strings" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -18,32 +19,65 @@ type StyleFunc func(children Nodes, i int) lipgloss.Style // Styles contains style definitions for this tree component. By default, these // values are generated by DefaultStyles. type Styles struct { - HelpStyle lipgloss.Style - TreeStyle lipgloss.Style + TreeStyle lipgloss.Style + HelpStyle lipgloss.Style + selectedNodeFunc StyleFunc SelectedNodeStyle lipgloss.Style SelectedNodeStyleFunc StyleFunc - nodeFunc StyleFunc - NodeStyle lipgloss.Style - NodeStyleFunc StyleFunc + + nodeFunc StyleFunc + NodeStyle lipgloss.Style + NodeStyleFunc StyleFunc + + rootNodeFunc StyleFunc + RootNodeStyle lipgloss.Style + RootNodeStyleFunc StyleFunc + + parentNodeFunc StyleFunc + ParentNodeStyle lipgloss.Style + ParentNodeStyleFunc StyleFunc + + CursorStyle lipgloss.Style + + EnumeratorStyle lipgloss.Style + OpenIndicatorStyle lipgloss.Style } // DefaultStyles returns a set of default style definitions for this tree // component. func DefaultStyles() (s Styles) { + verySubduedColor := lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"} + subduedColor := lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"} + s.TreeStyle = lipgloss.NewStyle() - s.NodeStyle = lipgloss.NewStyle() - s.SelectedNodeStyle = lipgloss.NewStyle(). - Background(lipgloss.Color("62")). - Foreground(lipgloss.Color("18")). - Bold(true) + s.HelpStyle = lipgloss.NewStyle().PaddingTop(1) + + s.NodeStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#b0b0b0"}) s.nodeFunc = func(_ Nodes, _ int) lipgloss.Style { return s.NodeStyle } + + s.SelectedNodeStyle = s.NodeStyle.Foreground(lipgloss.Color("212")).Bold(true) s.selectedNodeFunc = func(_ Nodes, _ int) lipgloss.Style { return s.SelectedNodeStyle } - s.HelpStyle = lipgloss.NewStyle().PaddingTop(1) + + s.RootNodeStyle = lipgloss.NewStyle(). + Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}) + s.rootNodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return s.RootNodeStyle + } + + s.ParentNodeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("99")) + s.parentNodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return s.ParentNodeStyle + } + + s.CursorStyle = lipgloss.NewStyle().PaddingRight(1).Foreground(lipgloss.Color("212")).Bold(true) + + s.EnumeratorStyle = lipgloss.NewStyle().Foreground(verySubduedColor).PaddingRight(1) + s.OpenIndicatorStyle = lipgloss.NewStyle().Foreground(subduedColor) return s } @@ -146,6 +180,8 @@ type Model struct { OpenCharacter string // ClosedCharacter is the character used to represent a closed node. ClosedCharacter string + // CursorCharacter is the character used to represent the cursor. + CursorCharacter string // KeyMap encodes the keybindings recognized by the widget. KeyMap KeyMap // styles sets the styling for the tree @@ -175,6 +211,7 @@ func New(t *Node, width, height int) Model { KeyMap: DefaultKeyMap, OpenCharacter: "▼", ClosedCharacter: "▶", + CursorCharacter: "→", Help: help.New(), ScrollOff: 5, @@ -186,7 +223,7 @@ func New(t *Node, width, height int) Model { m.SetSize(width, height) m.setAttributes() m.updateStyles() - m.viewport.SetContent(m.root.String()) + m.updateViewport(0) return m } @@ -338,8 +375,16 @@ func (m *Model) toggleNode(node *Node, open bool) { func (m *Model) updateViewport(movement int) { m.yOffset = max(min(m.root.size-1, m.yOffset+movement), 0) m.updateStyles() - m.viewport.Style = m.styles.TreeStyle - m.viewport.SetContent(m.styles.TreeStyle.Render(m.root.String())) + // m.viewport.Style = m.styles.TreeStyle + + cursor := m.cursorView() + m.viewport.SetContent( + lipgloss.JoinHorizontal( + lipgloss.Top, + m.styles.TreeStyle.UnsetPaddingRight().UnsetMarginRight().Render(cursor), + m.styles.TreeStyle.UnsetPaddingLeft().UnsetMarginLeft().Render(m.root.String()), + ), + ) // if this is the initial render, make sure we show the root node if m.yOffset == 0 && movement == 0 { @@ -375,6 +420,23 @@ func (m *Model) SetStyles(styles Styles) { return styles.SelectedNodeStyle } } + if styles.ParentNodeStyleFunc != nil { + styles.parentNodeFunc = styles.ParentNodeStyleFunc + } else { + styles.parentNodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return styles.ParentNodeStyle + } + } + if styles.RootNodeStyleFunc != nil { + styles.rootNodeFunc = styles.RootNodeStyleFunc + } else { + styles.rootNodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return styles.RootNodeStyle + } + } + + m.root.EnumeratorStyle(styles.EnumeratorStyle) + m.styles = styles m.updateViewport(0) } @@ -474,6 +536,12 @@ func (m Model) FullHelp() [][]key.Binding { return kb } +func (m Model) cursorView() string { + cursor := strings.Split(strings.Repeat(" ", m.root.size), "") + cursor[m.yOffset] = m.CursorCharacter + return m.styles.CursorStyle.Render(lipgloss.JoinVertical(lipgloss.Left, cursor...)) +} + func (m Model) helpView() string { return m.styles.HelpStyle.Render(m.Help.View(m)) } @@ -557,16 +625,17 @@ func (m *Model) NodeAtCurrentOffset() *Node { // Since the selected node changes, we need to capture m.yOffset in the // style function's closure again func (m *Model) updateStyles() { - m.root.rootStyle = m.rootStyle() // TODO: add RootStyleFunc to the Node interface? + m.root.rootStyle = m.rootStyle() m.root.RootStyle(m.root.rootStyle) - m.root.ItemStyleFunc(m.selectedNodeStyle()) + // m.root.ItemStyleFunc(m.selectedNodeStyle()) items := m.FlatNodes() opts := &itemOptions{ openCharacter: m.OpenCharacter, closedCharacter: m.ClosedCharacter, treeYOffset: m.yOffset, + styles: &m.styles, } for _, item := range items { item.opts = opts @@ -596,7 +665,7 @@ func (m *Model) rootStyle() lipgloss.Style { return s.Width(s.GetWidth() - lipgloss.Width(m.OpenCharacter) - 1) } - s := m.styles.nodeFunc(Nodes{m.root}, 0) + s := m.styles.rootNodeFunc(Nodes{m.root}, 0) return s.Width(s.GetWidth() - lipgloss.Width(m.OpenCharacter) - 1) } From 8a7ecbd3a8b852acef5e3ec648e987bccf4cfced Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Fri, 25 Oct 2024 12:30:21 +0300 Subject: [PATCH 23/29] WIP --- tree/node.go | 8 ++++---- tree/tree.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tree/node.go b/tree/node.go index 93275920b..0f1b07872 100644 --- a/tree/node.go +++ b/tree/node.go @@ -90,9 +90,9 @@ type itemOptions struct { func (t *Node) String() string { s := t.opts.styles.OpenIndicatorStyle if t.open { - return s.Render(t.opts.openCharacter) + " " + t.tree.String() + return s.Render(t.opts.openCharacter+" ") + t.tree.String() } - return s.Render(t.opts.closedCharacter) + " " + t.tree.String() + return s.Render(t.opts.closedCharacter+" ") + t.tree.String() } // Value returns the root name of this node. @@ -112,9 +112,9 @@ func (t *Node) Value() string { if t.isRoot { if t.open { - return s.OpenIndicatorStyle.Render(t.opts.openCharacter) + " " + v + return s.OpenIndicatorStyle.Render(t.opts.openCharacter+" ") + v } - return s.OpenIndicatorStyle.Render(t.opts.closedCharacter) + " " + v + return s.OpenIndicatorStyle.Render(t.opts.closedCharacter+" ") + v } return v } diff --git a/tree/tree.go b/tree/tree.go index 32b4f137e..f6629444e 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -375,7 +375,7 @@ func (m *Model) toggleNode(node *Node, open bool) { func (m *Model) updateViewport(movement int) { m.yOffset = max(min(m.root.size-1, m.yOffset+movement), 0) m.updateStyles() - // m.viewport.Style = m.styles.TreeStyle + m.viewport.Style = lipgloss.NewStyle().Background(m.styles.TreeStyle.GetBackground()) cursor := m.cursorView() m.viewport.SetContent( From 9599a065339c783d81b8b4ca7b47ad26fc5678fe Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Fri, 25 Oct 2024 15:11:15 +0300 Subject: [PATCH 24/29] chore: make DefaultKeyMap a func --- tree/node.go | 13 +++--- tree/tree.go | 124 +++++++++++++++++++++++++-------------------------- 2 files changed, 68 insertions(+), 69 deletions(-) diff --git a/tree/node.go b/tree/node.go index 0f1b07872..31ffd1f60 100644 --- a/tree/node.go +++ b/tree/node.go @@ -25,9 +25,6 @@ type Node struct { // value is the root value of the node value any - // TODO: expose a getter for this in lipgloss? - rootStyle lipgloss.Style - opts *itemOptions // TODO: move to lipgloss.Tree? @@ -163,6 +160,8 @@ func (t *Node) ItemStyle(s lipgloss.Style) *Node { // } // return lipgloss.NewStyle().Foreground(dimColor) // }) +// +// TODO: currently unused as this is set in the Styles struct. func (t *Node) ItemStyleFunc(f StyleFunc) *Node { t.tree.ItemStyleFunc(func(children ltree.Children, i int) lipgloss.Style { c := make(Nodes, children.Length()) @@ -177,7 +176,6 @@ func (t *Node) ItemStyleFunc(f StyleFunc) *Node { // TODO: support IndentStyleFunc in lipgloss so we can have a full background for the item -// TODO: should we re-export RoundedEnumerator from lipgloss? // Enumerator sets the enumerator implementation. This can be used to change the // way the branches indicators look. Lipgloss includes predefined enumerators // for a classic or rounded tree. For example, you can have a rounded tree: @@ -218,6 +216,7 @@ func (t *Node) Indenter(indenter ltree.Indenter) *Node { // EnumeratorStyle sets a static style for all enumerators. // // Use EnumeratorStyleFunc to conditionally set styles based on the tree node. +// TODO: currently unused as this is set in the Styles struct. func (t *Node) EnumeratorStyle(style lipgloss.Style) *Node { t.tree.EnumeratorStyle(style) return t @@ -233,6 +232,8 @@ func (t *Node) EnumeratorStyle(style lipgloss.Style) *Node { // } // return lipgloss.NewStyle().Foreground(dimColor) // }) +// +// TODO: currently unused as this is set in the Styles struct. func (t *Node) EnumeratorStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Node { t.tree.EnumeratorStyleFunc(f) return t @@ -262,7 +263,7 @@ func (t *Node) Child(children ...any) *Node { t.size = t.size + child.size t.tree.Child(child) - // Close the node again as the number of children as changed + // Close the node again as the number of children has changed if t.initialClosed { t.Close() } @@ -275,7 +276,7 @@ func (t *Node) Child(children ...any) *Node { t.size = t.size + item.size t.tree.Child(item) - // Close the node again as the number of children as changed + // Close the node again as the number of children has changed if t.initialClosed { t.Close() } diff --git a/tree/tree.go b/tree/tree.go index f6629444e..532d496e4 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -106,69 +106,70 @@ type KeyMap struct { Quit key.Binding } -// DefaultKeyMap is the default set of key bindings for navigating and acting +// DefaultKeyMap returns the default set of key bindings for navigating and acting // upon the tree. -var DefaultKeyMap = KeyMap{ - Down: key.NewBinding( +func DefaultKeyMap() KeyMap { + return KeyMap{Down: key.NewBinding( key.WithKeys("down", "j", "ctrl+n"), key.WithHelp("↓/j", "down"), ), - Up: key.NewBinding( - key.WithKeys("up", "k", "ctrl+p"), - key.WithHelp("↑/k", "up"), - ), - PageDown: key.NewBinding( - key.WithKeys("pgdown", spacebar, "f"), - key.WithHelp("f/pgdn", "page down"), - ), - PageUp: key.NewBinding( - key.WithKeys("pgup", "b"), - key.WithHelp("b/pgup", "page up"), - ), - HalfPageDown: key.NewBinding( - key.WithKeys("d", "ctrl+d"), - key.WithHelp("d", "½ page down"), - ), - HalfPageUp: key.NewBinding( - key.WithKeys("u", "ctrl+u"), - key.WithHelp("u", "½ page up"), - ), - GoToTop: key.NewBinding( - key.WithKeys("g", "home"), - key.WithHelp("g", "top"), - ), - GoToBottom: key.NewBinding( - key.WithKeys("G", "end"), - key.WithHelp("G", "bottom"), - ), + Up: key.NewBinding( + key.WithKeys("up", "k", "ctrl+p"), + key.WithHelp("↑/k", "up"), + ), + PageDown: key.NewBinding( + key.WithKeys("pgdown", spacebar, "f"), + key.WithHelp("f/pgdn", "page down"), + ), + PageUp: key.NewBinding( + key.WithKeys("pgup", "b"), + key.WithHelp("b/pgup", "page up"), + ), + HalfPageDown: key.NewBinding( + key.WithKeys("d", "ctrl+d"), + key.WithHelp("d", "½ page down"), + ), + HalfPageUp: key.NewBinding( + key.WithKeys("u", "ctrl+u"), + key.WithHelp("u", "½ page up"), + ), + GoToTop: key.NewBinding( + key.WithKeys("g", "home"), + key.WithHelp("g", "top"), + ), + GoToBottom: key.NewBinding( + key.WithKeys("G", "end"), + key.WithHelp("G", "bottom"), + ), - Toggle: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("⏎", "toggle"), - ), - Open: key.NewBinding( - key.WithKeys("l", "right"), - key.WithHelp("→/l", "open"), - ), - Close: key.NewBinding( - key.WithKeys("h", "left"), - key.WithHelp("←/h", "close"), - ), + Toggle: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("⏎", "toggle"), + ), + Open: key.NewBinding( + key.WithKeys("l", "right"), + key.WithHelp("→/l", "open"), + ), + Close: key.NewBinding( + key.WithKeys("h", "left"), + key.WithHelp("←/h", "close"), + ), - // Toggle help. - ShowFullHelp: key.NewBinding( - key.WithKeys("?"), - key.WithHelp("?", "more"), - ), - CloseFullHelp: key.NewBinding( - key.WithKeys("?"), - key.WithHelp("?", "close help"), - ), + // Toggle help. + ShowFullHelp: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "more"), + ), + CloseFullHelp: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "close help"), + ), - Quit: key.NewBinding( - key.WithKeys("q", "ctrl+c"), - key.WithHelp("q", "quit"), - ), + Quit: key.NewBinding( + key.WithKeys("q", "ctrl+c"), + key.WithHelp("q", "quit"), + ), + } } // Model is the Bubble Tea model for this tree element. @@ -208,7 +209,7 @@ type Model struct { // New creates a new model with default settings. func New(t *Node, width, height int) Model { m := Model{ - KeyMap: DefaultKeyMap, + KeyMap: DefaultKeyMap(), OpenCharacter: "▼", ClosedCharacter: "▶", CursorCharacter: "→", @@ -375,14 +376,13 @@ func (m *Model) toggleNode(node *Node, open bool) { func (m *Model) updateViewport(movement int) { m.yOffset = max(min(m.root.size-1, m.yOffset+movement), 0) m.updateStyles() - m.viewport.Style = lipgloss.NewStyle().Background(m.styles.TreeStyle.GetBackground()) cursor := m.cursorView() m.viewport.SetContent( lipgloss.JoinHorizontal( lipgloss.Top, - m.styles.TreeStyle.UnsetPaddingRight().UnsetMarginRight().Render(cursor), - m.styles.TreeStyle.UnsetPaddingLeft().UnsetMarginLeft().Render(m.root.String()), + cursor, + m.styles.TreeStyle.Render(m.root.String()), ), ) @@ -626,9 +626,7 @@ func (m *Model) NodeAtCurrentOffset() *Node { // style function's closure again func (m *Model) updateStyles() { // TODO: add RootStyleFunc to the Node interface? - m.root.rootStyle = m.rootStyle() - m.root.RootStyle(m.root.rootStyle) - // m.root.ItemStyleFunc(m.selectedNodeStyle()) + m.root.RootStyle(m.rootStyle()) items := m.FlatNodes() opts := &itemOptions{ From fdf80a6bfa2ea03200f5af243df590aa88693d46 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 26 Oct 2024 18:34:24 +0300 Subject: [PATCH 25/29] feat: allow to pass nil tree and set its nodes later --- tree/tree.go | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tree/tree.go b/tree/tree.go index 532d496e4..ee9c3e307 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -222,9 +222,11 @@ func New(t *Node, width, height int) Model { } m.SetStyles(DefaultStyles()) m.SetSize(width, height) - m.setAttributes() - m.updateStyles() - m.updateViewport(0) + if m.root != nil { + m.setAttributes() + m.updateStyles() + m.updateViewport(0) + } return m } @@ -294,6 +296,14 @@ func (m Model) View() string { return lipgloss.JoinVertical(lipgloss.Left, treeView, help) } +// SetNodes sets the tree to the given root node. +func (m *Model) SetNodes(t *Node) { + m.root = t + m.setAttributes() + m.updateStyles() + m.updateViewport(0) +} + // Down moves the selection down by one item. func (m *Model) Down() { m.updateViewport(1) @@ -374,6 +384,10 @@ func (m *Model) toggleNode(node *Node, open bool) { } func (m *Model) updateViewport(movement int) { + if m.root == nil { + return + } + m.yOffset = max(min(m.root.size-1, m.yOffset+movement), 0) m.updateStyles() @@ -435,7 +449,9 @@ func (m *Model) SetStyles(styles Styles) { } } - m.root.EnumeratorStyle(styles.EnumeratorStyle) + if m.root != nil { + m.root.EnumeratorStyle(styles.EnumeratorStyle) + } m.styles = styles m.updateViewport(0) @@ -626,7 +642,9 @@ func (m *Model) NodeAtCurrentOffset() *Node { // style function's closure again func (m *Model) updateStyles() { // TODO: add RootStyleFunc to the Node interface? - m.root.RootStyle(m.rootStyle()) + if m.root != nil { + m.root.RootStyle(m.rootStyle()) + } items := m.FlatNodes() opts := &itemOptions{ From 3b2d45a96ba48aa3939e6be095559ba1e2240225 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 26 Oct 2024 20:39:15 +0300 Subject: [PATCH 26/29] refactor: move styles to styles.go --- tree/node.go | 28 ++++++++++++- tree/styles.go | 72 +++++++++++++++++++++++++++++++++ tree/tree.go | 106 +++++++++---------------------------------------- 3 files changed, 116 insertions(+), 90 deletions(-) create mode 100644 tree/styles.go diff --git a/tree/node.go b/tree/node.go index 31ffd1f60..69cd265f8 100644 --- a/tree/node.go +++ b/tree/node.go @@ -25,7 +25,7 @@ type Node struct { // value is the root value of the node value any - opts *itemOptions + opts itemOptions // TODO: move to lipgloss.Tree? size int @@ -78,7 +78,7 @@ type itemOptions struct { openCharacter string closedCharacter string treeYOffset int - styles *Styles + styles Styles } // Used to print the Node's tree @@ -126,6 +126,28 @@ func (t *Node) Children() ltree.Children { return t.tree.Children() } +// ChildNodes returns the children of an item. +func (t *Node) ChildNodes() []*Node { + res := []*Node{} + children := t.tree.Children() + for i := 0; i < children.Length(); i++ { + child := children.At(i) + res = append(res, child.(*Node)) + } + return res +} + +// FlatNodes returns all descendant items in as a flat list. +func (t *Node) AllNodes() []*Node { + res := []*Node{t} + children := t.tree.Children() + for i := 0; i < children.Length(); i++ { + child := children.At(i) + res = append(res, child.(*Node).AllNodes()...) + } + return res +} + // Hidden returns whether this item is hidden. func (t *Node) Hidden() bool { return t.tree.Hidden() @@ -269,6 +291,7 @@ func (t *Node) Child(children ...any) *Node { } default: item := new(Node) + item.opts.styles = DefaultStyles() item.tree = ltree.Root(child) item.size = 1 item.open = false @@ -289,6 +312,7 @@ func (t *Node) Child(children ...any) *Node { // Root returns a new tree with the root set. func Root(root any) *Node { t := new(Node) + t.opts.styles = DefaultStyles() t.size = 1 t.value = root t.open = true diff --git a/tree/styles.go b/tree/styles.go new file mode 100644 index 000000000..3ff165931 --- /dev/null +++ b/tree/styles.go @@ -0,0 +1,72 @@ +package tree + +import "github.com/charmbracelet/lipgloss" + +// StyleFunc allows the tree to be styled per item. +type StyleFunc func(children Nodes, i int) lipgloss.Style + +// Styles contains style definitions for this tree component. By default, these +// values are generated by DefaultStyles. +type Styles struct { + TreeStyle lipgloss.Style + HelpStyle lipgloss.Style + + selectedNodeFunc StyleFunc + SelectedNodeStyle lipgloss.Style + SelectedNodeStyleFunc StyleFunc + + nodeFunc StyleFunc + NodeStyle lipgloss.Style + NodeStyleFunc StyleFunc + + rootNodeFunc StyleFunc + RootNodeStyle lipgloss.Style + RootNodeStyleFunc StyleFunc + + parentNodeFunc StyleFunc + ParentNodeStyle lipgloss.Style + ParentNodeStyleFunc StyleFunc + + CursorStyle lipgloss.Style + + EnumeratorStyle lipgloss.Style + OpenIndicatorStyle lipgloss.Style +} + +// DefaultStyles returns a set of default style definitions for this tree +// component. +func DefaultStyles() (s Styles) { + verySubduedColor := lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"} + subduedColor := lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"} + + s.TreeStyle = lipgloss.NewStyle() + s.HelpStyle = lipgloss.NewStyle().PaddingTop(1) + + s.NodeStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#b0b0b0"}) + s.nodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return s.NodeStyle + } + + s.SelectedNodeStyle = s.NodeStyle.Foreground(lipgloss.Color("212")).Bold(true) + s.selectedNodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return s.SelectedNodeStyle + } + + s.RootNodeStyle = lipgloss.NewStyle(). + Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}) + s.rootNodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return s.RootNodeStyle + } + + s.ParentNodeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("99")) + s.parentNodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return s.ParentNodeStyle + } + + s.CursorStyle = lipgloss.NewStyle().PaddingRight(1).Foreground(lipgloss.Color("212")).Bold(true) + + s.EnumeratorStyle = lipgloss.NewStyle().Foreground(verySubduedColor).PaddingRight(1) + s.OpenIndicatorStyle = lipgloss.NewStyle().Foreground(subduedColor) + + return s +} diff --git a/tree/tree.go b/tree/tree.go index ee9c3e307..6e2cc670d 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -13,75 +13,6 @@ import ( "github.com/charmbracelet/bubbles/viewport" ) -// StyleFunc allows the tree to be styled per item. -type StyleFunc func(children Nodes, i int) lipgloss.Style - -// Styles contains style definitions for this tree component. By default, these -// values are generated by DefaultStyles. -type Styles struct { - TreeStyle lipgloss.Style - HelpStyle lipgloss.Style - - selectedNodeFunc StyleFunc - SelectedNodeStyle lipgloss.Style - SelectedNodeStyleFunc StyleFunc - - nodeFunc StyleFunc - NodeStyle lipgloss.Style - NodeStyleFunc StyleFunc - - rootNodeFunc StyleFunc - RootNodeStyle lipgloss.Style - RootNodeStyleFunc StyleFunc - - parentNodeFunc StyleFunc - ParentNodeStyle lipgloss.Style - ParentNodeStyleFunc StyleFunc - - CursorStyle lipgloss.Style - - EnumeratorStyle lipgloss.Style - OpenIndicatorStyle lipgloss.Style -} - -// DefaultStyles returns a set of default style definitions for this tree -// component. -func DefaultStyles() (s Styles) { - verySubduedColor := lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"} - subduedColor := lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"} - - s.TreeStyle = lipgloss.NewStyle() - s.HelpStyle = lipgloss.NewStyle().PaddingTop(1) - - s.NodeStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#b0b0b0"}) - s.nodeFunc = func(_ Nodes, _ int) lipgloss.Style { - return s.NodeStyle - } - - s.SelectedNodeStyle = s.NodeStyle.Foreground(lipgloss.Color("212")).Bold(true) - s.selectedNodeFunc = func(_ Nodes, _ int) lipgloss.Style { - return s.SelectedNodeStyle - } - - s.RootNodeStyle = lipgloss.NewStyle(). - Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}) - s.rootNodeFunc = func(_ Nodes, _ int) lipgloss.Style { - return s.RootNodeStyle - } - - s.ParentNodeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("99")) - s.parentNodeFunc = func(_ Nodes, _ int) lipgloss.Style { - return s.ParentNodeStyle - } - - s.CursorStyle = lipgloss.NewStyle().PaddingRight(1).Foreground(lipgloss.Color("212")).Bold(true) - - s.EnumeratorStyle = lipgloss.NewStyle().Foreground(verySubduedColor).PaddingRight(1) - s.OpenIndicatorStyle = lipgloss.NewStyle().Foreground(subduedColor) - - return s -} - const spacebar = " " // KeyMap is the key bindings for different actions within the tree. @@ -562,9 +493,15 @@ func (m Model) helpView() string { return m.styles.HelpStyle.Render(m.Help.View(m)) } +// Root returns the root node of the tree. +// Equivalent to calling `Model.Node(0)` +func (m *Model) Root() *Node { + return m.root +} + // FlatNodes returns all items in the tree as a flat list. -func (m *Model) FlatNodes() []*Node { - return m.root.FlatNodes() +func (m *Model) AllNodes() []*Node { + return m.root.AllNodes() } func (m *Model) setAttributes() { @@ -573,17 +510,6 @@ func (m *Model) setAttributes() { setYOffsets(m.root) } -// FlatNodes returns all descendant items in as a flat list. -func (t *Node) FlatNodes() []*Node { - res := []*Node{t} - children := t.tree.Children() - for i := 0; i < children.Length(); i++ { - child := children.At(i) - res = append(res, child.(*Node).FlatNodes()...) - } - return res -} - // setSizes updates each Node's size // Note that if a child isn't open, its size is 1 func setDepths(t *Node, depth int) { @@ -646,15 +572,19 @@ func (m *Model) updateStyles() { m.root.RootStyle(m.rootStyle()) } - items := m.FlatNodes() - opts := &itemOptions{ + items := m.AllNodes() + opts := m.getItemOpts() + for _, item := range items { + item.opts = *opts + } +} + +func (m *Model) getItemOpts() *itemOptions { + return &itemOptions{ openCharacter: m.OpenCharacter, closedCharacter: m.ClosedCharacter, treeYOffset: m.yOffset, - styles: &m.styles, - } - for _, item := range items { - item.opts = opts + styles: m.styles, } } From eaf5b4709c7129ad642b9ece59a1f18b73b22365 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 16 Nov 2024 20:59:14 +0200 Subject: [PATCH 27/29] feat: add Indenter and Enumerator at the model level --- tree/node.go | 11 +++++++++++ tree/tree.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/tree/node.go b/tree/node.go index 69cd265f8..2d792325c 100644 --- a/tree/node.go +++ b/tree/node.go @@ -309,6 +309,17 @@ func (t *Node) Child(children ...any) *Node { return t } +// NewNode returns a new node. +func NewNode() *Node { + t := new(Node) + t.opts.styles = DefaultStyles() + t.size = 1 + t.open = true + t.isRoot = true + t.tree = ltree.New() + return t +} + // Root returns a new tree with the root set. func Root(root any) *Node { t := new(Node) diff --git a/tree/tree.go b/tree/tree.go index 6e2cc670d..618c01f3b 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -7,6 +7,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + ltree "github.com/charmbracelet/lipgloss/tree" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" @@ -130,6 +131,9 @@ type Model struct { root *Node + enumerator *ltree.Enumerator + indenter *ltree.Indenter + viewport viewport.Model width int height int @@ -151,6 +155,11 @@ func New(t *Node, width, height int) Model { root: t, viewport: viewport.Model{}, } + + if m.root == nil { + m.root = NewNode() + } + m.SetStyles(DefaultStyles()) m.SetSize(width, height) if m.root != nil { @@ -230,6 +239,12 @@ func (m Model) View() string { // SetNodes sets the tree to the given root node. func (m *Model) SetNodes(t *Node) { m.root = t + if m.enumerator != nil { + m.root.Enumerator(*m.enumerator) + } + if m.indenter != nil { + m.root.Indenter(*m.indenter) + } m.setAttributes() m.updateStyles() m.updateViewport(0) @@ -564,6 +579,20 @@ func (m *Model) NodeAtCurrentOffset() *Node { return findNode(m.root, m.yOffset) } +// Enumerator sets the enumerator for the tree +func (m *Model) Enumerator(enumerator ltree.Enumerator) *Model { + m.enumerator = &enumerator + m.root.Enumerator(enumerator) + return m +} + +// Indenter sets the indenter for the tree +func (m *Model) Indenter(indenter ltree.Indenter) *Model { + m.indenter = &indenter + m.root.Indenter(indenter) + return m +} + // Since the selected node changes, we need to capture m.yOffset in the // style function's closure again func (m *Model) updateStyles() { From 3e06d6e420e8690bc2bbe4a88a16fa78bae487e2 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sun, 1 Dec 2024 14:00:58 +0200 Subject: [PATCH 28/29] WIP --- tree/node.go | 55 ++++++++++++++++++++++++++++++++++++++++++++------ tree/styles.go | 2 ++ tree/tree.go | 49 ++++++++++++++++++++++---------------------- 3 files changed, 75 insertions(+), 31 deletions(-) diff --git a/tree/node.go b/tree/node.go index 2d792325c..88a86eeee 100644 --- a/tree/node.go +++ b/tree/node.go @@ -85,11 +85,12 @@ type itemOptions struct { // TODO: Value is not called on the root node, so we need to repeat the open/closed character // Should this be fixed in lipgloss? func (t *Node) String() string { - s := t.opts.styles.OpenIndicatorStyle - if t.open { - return s.Render(t.opts.openCharacter+" ") + t.tree.String() - } - return s.Render(t.opts.closedCharacter+" ") + t.tree.String() + // s := t.opts.styles.OpenIndicatorStyle + // if t.open { + // return s.Render(t.opts.openCharacter+" ") + t.tree.String() + // } + // return s.Render(t.opts.closedCharacter+" ") + t.tree.String() + return t.tree.String() } // Value returns the root name of this node. @@ -99,6 +100,8 @@ func (t *Node) Value() string { if t.yOffset == t.opts.treeYOffset { ns = s.selectedNodeFunc(Nodes{t}, 0) + } else if t.yOffset == 0 { + ns = s.rootNodeFunc(Nodes{t}, 0) } else if t.isRoot { ns = s.parentNodeFunc(Nodes{t}, 0) } else { @@ -113,6 +116,8 @@ func (t *Node) Value() string { } return s.OpenIndicatorStyle.Render(t.opts.closedCharacter+" ") + v } + + // leaf return v } @@ -261,6 +266,32 @@ func (t *Node) EnumeratorStyleFunc(f func(children ltree.Children, i int) lipglo return t } +// IndenterStyle sets a static style for all indenters. +// +// Use IndenterStyleFunc to conditionally set styles based on the tree node. +// TODO: currently unused as this is set in the Styles struct. +func (t *Node) IndenterStyle(style lipgloss.Style) *Node { + t.tree.IndenterStyle(style) + return t +} + +// IndenterStyleFunc sets the indenter style function. Use this function +// for conditional styling. +// +// t := tree.Root("root"). +// IndenterStyleFunc(func(_ tree.Children, i int) lipgloss.Style { +// if selected == i { +// return lipgloss.NewStyle().Foreground(hightlightColor) +// } +// return lipgloss.NewStyle().Foreground(dimColor) +// }) +// +// TODO: currently unused as this is set in the Styles struct. +func (t *Node) IndenterStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Node { + t.tree.IndenterStyleFunc(f) + return t +} + // RootStyle sets a style for the root element. func (t *Node) RootStyle(style lipgloss.Style) *Node { t.tree.RootStyle(style) @@ -328,6 +359,18 @@ func Root(root any) *Node { t.value = root t.open = true t.isRoot = true - t.tree = ltree.Root(root) + switch root := root.(type) { + case *Node: + t.tree = ltree.Root(root.Value()) + default: + item := new(Node) + item.value = root + item.opts.styles = DefaultStyles() + item.size = 1 + item.open = true + item.isRoot = true + t.tree = ltree.Root(item) + } + return t } diff --git a/tree/styles.go b/tree/styles.go index 3ff165931..d4beb8a97 100644 --- a/tree/styles.go +++ b/tree/styles.go @@ -30,6 +30,7 @@ type Styles struct { CursorStyle lipgloss.Style EnumeratorStyle lipgloss.Style + IndenterStyle lipgloss.Style OpenIndicatorStyle lipgloss.Style } @@ -66,6 +67,7 @@ func DefaultStyles() (s Styles) { s.CursorStyle = lipgloss.NewStyle().PaddingRight(1).Foreground(lipgloss.Color("212")).Bold(true) s.EnumeratorStyle = lipgloss.NewStyle().Foreground(verySubduedColor).PaddingRight(1) + s.IndenterStyle = lipgloss.NewStyle().Foreground(verySubduedColor) s.OpenIndicatorStyle = lipgloss.NewStyle().Foreground(subduedColor) return s diff --git a/tree/tree.go b/tree/tree.go index 618c01f3b..0cc4be7c5 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -2,7 +2,6 @@ package tree import ( "fmt" - "os" "strings" tea "github.com/charmbracelet/bubbletea" @@ -216,17 +215,18 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { // View renders the component. func (m Model) View() string { - var leftDebugView string + // var leftDebugView string // TODO: remove - if os.Getenv("DEBUG") == "true" { - leftDebugView = printDebugInfo(m.root) + " " - } + // if os.Getenv("DEBUG") == "true" { + // leftDebugView = printDebugInfo(m.root) + " " + // } - treeView := lipgloss.JoinHorizontal( - lipgloss.Top, - leftDebugView, - m.viewport.View(), - ) + // treeView := lipgloss.JoinHorizontal( + // lipgloss.Top, + // leftDebugView, + // m.viewport.View(), + // ) + treeView := m.viewport.View() var help string if m.showHelp { @@ -397,9 +397,12 @@ func (m *Model) SetStyles(styles Styles) { if m.root != nil { m.root.EnumeratorStyle(styles.EnumeratorStyle) + m.root.IndenterStyle(styles.IndenterStyle) } m.styles = styles + // call SetSize as it takes into account width/height of the styles frame sizes + m.SetSize(m.width, m.height) m.updateViewport(0) } @@ -420,19 +423,20 @@ func (m Model) Height() int { } // SetWidth sets the width of this component. -func (m *Model) SetWidth(v int) { - m.SetSize(v, m.height) +func (m *Model) SetWidth(width int) { + m.SetSize(width, m.height) } // SetHeight sets the height of this component. -func (m *Model) SetHeight(v int) { - m.SetSize(m.width, v) +func (m *Model) SetHeight(height int) { + m.SetSize(m.width, height) } // SetSize sets the width and height of this component. func (m *Model) SetSize(width, height int) { m.width = width m.height = height + m.root.tree.Width(width - lipgloss.Width(m.cursorView()) - m.styles.TreeStyle.GetHorizontalFrameSize()) m.viewport.Width = width hv := 0 @@ -468,11 +472,9 @@ func (m Model) FullHelp() [][]key.Binding { { m.KeyMap.Down, m.KeyMap.Up, - }, - { - m.KeyMap.Toggle, m.KeyMap.Open, m.KeyMap.Close, + m.KeyMap.Toggle, }, { m.KeyMap.PageDown, @@ -499,6 +501,9 @@ func (m Model) FullHelp() [][]key.Binding { } func (m Model) cursorView() string { + if m.CursorCharacter == "" { + return "" + } cursor := strings.Split(strings.Repeat(" ", m.root.size), "") cursor[m.yOffset] = m.CursorCharacter return m.styles.CursorStyle.Render(lipgloss.JoinVertical(lipgloss.Left, cursor...)) @@ -631,17 +636,11 @@ func (m *Model) selectedNodeStyle() StyleFunc { } func (m *Model) rootStyle() lipgloss.Style { - if m.styles.nodeFunc == nil || m.styles.selectedNodeFunc == nil { - return lipgloss.NewStyle() - } if m.root.yOffset == m.yOffset { - s := m.styles.selectedNodeFunc(Nodes{m.root}, 0) - // TODO: if we call Value on the root node in lipgloss, we wouldn't need this - return s.Width(s.GetWidth() - lipgloss.Width(m.OpenCharacter) - 1) + return m.styles.selectedNodeFunc(Nodes{m.root}, 0) } - s := m.styles.rootNodeFunc(Nodes{m.root}, 0) - return s.Width(s.GetWidth() - lipgloss.Width(m.OpenCharacter) - 1) + return m.styles.rootNodeFunc(Nodes{m.root}, 0) } // TODO: remove From dbf022302c7fc1ebace2f7088528c95c687f65fa Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Thu, 5 Dec 2024 23:29:52 +0200 Subject: [PATCH 29/29] fix: comments + cleanup --- tree/node.go | 71 ++++++++++++---------------------------------- tree/styles.go | 2 +- tree/tree.go | 77 +++++++++++++------------------------------------- 3 files changed, 39 insertions(+), 111 deletions(-) diff --git a/tree/node.go b/tree/node.go index 88a86eeee..14d2d62b0 100644 --- a/tree/node.go +++ b/tree/node.go @@ -26,9 +26,6 @@ type Node struct { value any opts itemOptions - - // TODO: move to lipgloss.Tree? - size int } // IsSelected returns whether this item is selected. @@ -44,7 +41,7 @@ func (t *Node) Depth() int { // Size returns the number of nodes in the tree. // Note that if a child isn't open, its size is 1 func (t *Node) Size() int { - return t.size + return len(t.AllNodes()) } // YOffset returns the vertical offset of the Node @@ -82,32 +79,31 @@ type itemOptions struct { } // Used to print the Node's tree -// TODO: Value is not called on the root node, so we need to repeat the open/closed character -// Should this be fixed in lipgloss? func (t *Node) String() string { - // s := t.opts.styles.OpenIndicatorStyle - // if t.open { - // return s.Render(t.opts.openCharacter+" ") + t.tree.String() - // } - // return s.Render(t.opts.closedCharacter+" ") + t.tree.String() - return t.tree.String() + s := t.opts.styles.OpenIndicatorStyle + if t.open { + return s.Render(t.opts.openCharacter+" ") + t.tree.String() + } + return s.Render(t.opts.closedCharacter+" ") + t.tree.String() } -// Value returns the root name of this node. -func (t *Node) Value() string { +func (t *Node) getStyle() lipgloss.Style { s := t.opts.styles - var ns lipgloss.Style - if t.yOffset == t.opts.treeYOffset { - ns = s.selectedNodeFunc(Nodes{t}, 0) + return s.selectedNodeFunc(Nodes{t}, 0) } else if t.yOffset == 0 { - ns = s.rootNodeFunc(Nodes{t}, 0) + return s.rootNodeFunc(Nodes{t}, 0) } else if t.isRoot { - ns = s.parentNodeFunc(Nodes{t}, 0) + return s.parentNodeFunc(Nodes{t}, 0) } else { - ns = s.nodeFunc(Nodes{t}, 0) + return s.nodeFunc(Nodes{t}, 0) } +} +// Value returns the root name of this node. +func (t *Node) Value() string { + s := t.opts.styles + ns := t.getStyle() v := ns.Render(t.tree.Value()) if t.isRoot { @@ -187,12 +183,9 @@ func (t *Node) ItemStyle(s lipgloss.Style) *Node { // } // return lipgloss.NewStyle().Foreground(dimColor) // }) -// -// TODO: currently unused as this is set in the Styles struct. func (t *Node) ItemStyleFunc(f StyleFunc) *Node { t.tree.ItemStyleFunc(func(children ltree.Children, i int) lipgloss.Style { c := make(Nodes, children.Length()) - // TODO: if we expose Depth and Size in lipgloss, we can avoid this for i := 0; i < children.Length(); i++ { c[i] = children.At(i).(*Node) } @@ -201,8 +194,6 @@ func (t *Node) ItemStyleFunc(f StyleFunc) *Node { return t } -// TODO: support IndentStyleFunc in lipgloss so we can have a full background for the item - // Enumerator sets the enumerator implementation. This can be used to change the // way the branches indicators look. Lipgloss includes predefined enumerators // for a classic or rounded tree. For example, you can have a rounded tree: @@ -243,7 +234,6 @@ func (t *Node) Indenter(indenter ltree.Indenter) *Node { // EnumeratorStyle sets a static style for all enumerators. // // Use EnumeratorStyleFunc to conditionally set styles based on the tree node. -// TODO: currently unused as this is set in the Styles struct. func (t *Node) EnumeratorStyle(style lipgloss.Style) *Node { t.tree.EnumeratorStyle(style) return t @@ -259,8 +249,6 @@ func (t *Node) EnumeratorStyle(style lipgloss.Style) *Node { // } // return lipgloss.NewStyle().Foreground(dimColor) // }) -// -// TODO: currently unused as this is set in the Styles struct. func (t *Node) EnumeratorStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Node { t.tree.EnumeratorStyleFunc(f) return t @@ -269,7 +257,6 @@ func (t *Node) EnumeratorStyleFunc(f func(children ltree.Children, i int) lipglo // IndenterStyle sets a static style for all indenters. // // Use IndenterStyleFunc to conditionally set styles based on the tree node. -// TODO: currently unused as this is set in the Styles struct. func (t *Node) IndenterStyle(style lipgloss.Style) *Node { t.tree.IndenterStyle(style) return t @@ -285,8 +272,6 @@ func (t *Node) IndenterStyle(style lipgloss.Style) *Node { // } // return lipgloss.NewStyle().Foreground(dimColor) // }) -// -// TODO: currently unused as this is set in the Styles struct. func (t *Node) IndenterStyleFunc(f func(children ltree.Children, i int) lipgloss.Style) *Node { t.tree.IndenterStyleFunc(f) return t @@ -313,7 +298,6 @@ func (t *Node) Child(children ...any) *Node { for _, child := range children { switch child := child.(type) { case *Node: - t.size = t.size + child.size t.tree.Child(child) // Close the node again as the number of children has changed @@ -324,10 +308,8 @@ func (t *Node) Child(children ...any) *Node { item := new(Node) item.opts.styles = DefaultStyles() item.tree = ltree.Root(child) - item.size = 1 item.open = false item.value = child - t.size = t.size + item.size t.tree.Child(item) // Close the node again as the number of children has changed @@ -344,7 +326,6 @@ func (t *Node) Child(children ...any) *Node { func NewNode() *Node { t := new(Node) t.opts.styles = DefaultStyles() - t.size = 1 t.open = true t.isRoot = true t.tree = ltree.New() @@ -353,24 +334,8 @@ func NewNode() *Node { // Root returns a new tree with the root set. func Root(root any) *Node { - t := new(Node) - t.opts.styles = DefaultStyles() - t.size = 1 + t := NewNode() t.value = root - t.open = true - t.isRoot = true - switch root := root.(type) { - case *Node: - t.tree = ltree.Root(root.Value()) - default: - item := new(Node) - item.value = root - item.opts.styles = DefaultStyles() - item.size = 1 - item.open = true - item.isRoot = true - t.tree = ltree.Root(item) - } - + t.tree = ltree.Root(root) return t } diff --git a/tree/styles.go b/tree/styles.go index d4beb8a97..c4c0320ea 100644 --- a/tree/styles.go +++ b/tree/styles.go @@ -66,7 +66,7 @@ func DefaultStyles() (s Styles) { s.CursorStyle = lipgloss.NewStyle().PaddingRight(1).Foreground(lipgloss.Color("212")).Bold(true) - s.EnumeratorStyle = lipgloss.NewStyle().Foreground(verySubduedColor).PaddingRight(1) + s.EnumeratorStyle = lipgloss.NewStyle().Foreground(verySubduedColor) s.IndenterStyle = lipgloss.NewStyle().Foreground(verySubduedColor) s.OpenIndicatorStyle = lipgloss.NewStyle().Foreground(subduedColor) diff --git a/tree/tree.go b/tree/tree.go index 0cc4be7c5..66c61f301 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -1,7 +1,6 @@ package tree import ( - "fmt" "strings" tea "github.com/charmbracelet/bubbletea" @@ -215,17 +214,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { // View renders the component. func (m Model) View() string { - // var leftDebugView string - // TODO: remove - // if os.Getenv("DEBUG") == "true" { - // leftDebugView = printDebugInfo(m.root) + " " - // } - - // treeView := lipgloss.JoinHorizontal( - // lipgloss.Top, - // leftDebugView, - // m.viewport.View(), - // ) treeView := m.viewport.View() var help string @@ -287,7 +275,7 @@ func (m *Model) GoToTop() { // GoToBottom moves the selection to the bottom of the tree. func (m *Model) GoToBottom() { - m.updateViewport(m.root.size) + m.updateViewport(m.root.Size()) } // ToggleCurrentNode toggles the current node open/close state. @@ -334,7 +322,7 @@ func (m *Model) updateViewport(movement int) { return } - m.yOffset = max(min(m.root.size-1, m.yOffset+movement), 0) + m.yOffset = max(min(m.root.Size()-1, m.yOffset+movement), 0) m.updateStyles() cursor := m.cursorView() @@ -398,6 +386,10 @@ func (m *Model) SetStyles(styles Styles) { if m.root != nil { m.root.EnumeratorStyle(styles.EnumeratorStyle) m.root.IndenterStyle(styles.IndenterStyle) + m.root.ItemStyleFunc(func(children Nodes, i int) lipgloss.Style { + child := children.At(i) + return child.getStyle() + }) } m.styles = styles @@ -504,7 +496,7 @@ func (m Model) cursorView() string { if m.CursorCharacter == "" { return "" } - cursor := strings.Split(strings.Repeat(" ", m.root.size), "") + cursor := strings.Split(strings.Repeat(" ", m.root.Size()), "") cursor[m.yOffset] = m.CursorCharacter return m.styles.CursorStyle.Render(lipgloss.JoinVertical(lipgloss.Left, cursor...)) } @@ -526,7 +518,6 @@ func (m *Model) AllNodes() []*Node { func (m *Model) setAttributes() { setDepths(m.root, 0) - setSizes(m.root) setYOffsets(m.root) } @@ -541,19 +532,6 @@ func setDepths(t *Node, depth int) { } } -// setSizes updates each Node's size -// Note that if a child isn't open, its size is 1 -func setSizes(t *Node) int { - children := t.tree.Children() - size := 1 + children.Length() - for i := 0; i < children.Length(); i++ { - child := children.At(i) - size = size + setSizes(child.(*Node)) - 1 - } - t.size = size - return size -} - // setYOffsets updates each Node's yOffset based on how many items are "above" it func setYOffsets(t *Node) { children := t.tree.Children() @@ -563,7 +541,7 @@ func setYOffsets(t *Node) { if child, ok := child.(*Node); ok { child.yOffset = t.yOffset + above + i + 1 setYOffsets(child) - above += child.size - 1 + above += child.Size() - 1 } } } @@ -601,7 +579,6 @@ func (m *Model) Indenter(indenter ltree.Indenter) *Model { // Since the selected node changes, we need to capture m.yOffset in the // style function's closure again func (m *Model) updateStyles() { - // TODO: add RootStyleFunc to the Node interface? if m.root != nil { m.root.RootStyle(m.rootStyle()) } @@ -622,18 +599,18 @@ func (m *Model) getItemOpts() *itemOptions { } } -// selectedNodeStyle sets the node style -// and takes into account whether it's selected or not -func (m *Model) selectedNodeStyle() StyleFunc { - return func(children Nodes, i int) lipgloss.Style { - child := children.At(i) - if child.yOffset == m.yOffset { - return m.styles.selectedNodeFunc(children, i) - } - - return m.styles.nodeFunc(children, i) - } -} +// // selectedNodeStyle sets the node style +// // and takes into account whether it's selected or not +// func (m *Model) selectedNodeStyle() StyleFunc { +// return func(children Nodes, i int) lipgloss.Style { +// child := children.At(i) +// if child.yOffset == m.yOffset { +// return m.styles.selectedNodeFunc(children, i) +// } +// +// return m.styles.nodeFunc(children, i) +// } +// } func (m *Model) rootStyle() lipgloss.Style { if m.root.yOffset == m.yOffset { @@ -643,20 +620,6 @@ func (m *Model) rootStyle() lipgloss.Style { return m.styles.rootNodeFunc(Nodes{m.root}, 0) } -// TODO: remove -func printDebugInfo(t *Node) string { - debug := fmt.Sprintf("size=%2d y=%2d depth=%2d", t.size, t.yOffset, t.depth) - children := t.Children() - for i := 0; i < children.Length(); i++ { - child := children.At(i) - if child, ok := child.(*Node); ok { - debug = debug + "\n" + printDebugInfo(child) - } - } - - return debug -} - // findNode starts a DFS search for the node with the given yOffset // starting from the given item func findNode(t *Node, yOffset int) *Node {