diff --git a/tree/node.go b/tree/node.go new file mode 100644 index 00000000..14d2d62b --- /dev/null +++ b/tree/node.go @@ -0,0 +1,341 @@ +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 + + opts itemOptions +} + +// 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 len(t.AllNodes()) +} + +// 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 + styles Styles +} + +// Used to print the Node's tree +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() +} + +func (t *Node) getStyle() lipgloss.Style { + s := t.opts.styles + if t.yOffset == t.opts.treeYOffset { + return s.selectedNodeFunc(Nodes{t}, 0) + } else if t.yOffset == 0 { + return s.rootNodeFunc(Nodes{t}, 0) + } else if t.isRoot { + return s.parentNodeFunc(Nodes{t}, 0) + } else { + 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 { + if t.open { + return s.OpenIndicatorStyle.Render(t.opts.openCharacter+" ") + v + } + return s.OpenIndicatorStyle.Render(t.opts.closedCharacter+" ") + v + } + + // leaf + return v +} + +// 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() +} + +// 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() +} + +// 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()) + for i := 0; i < children.Length(); i++ { + c[i] = children.At(i).(*Node) + } + return f(c, i) + }) + return t +} + +// 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 +} + +// IndenterStyle sets a static style for all indenters. +// +// Use IndenterStyleFunc to conditionally set styles based on the tree node. +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) +// }) +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) + 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.tree.Child(child) + + // Close the node again as the number of children has changed + if t.initialClosed { + t.Close() + } + default: + item := new(Node) + item.opts.styles = DefaultStyles() + item.tree = ltree.Root(child) + item.open = false + item.value = child + t.tree.Child(item) + + // Close the node again as the number of children has changed + if t.initialClosed { + t.Close() + } + } + } + + return t +} + +// NewNode returns a new node. +func NewNode() *Node { + t := new(Node) + t.opts.styles = DefaultStyles() + 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 := NewNode() + t.value = root + t.tree = ltree.Root(root) + return t +} diff --git a/tree/styles.go b/tree/styles.go new file mode 100644 index 00000000..c4c0320e --- /dev/null +++ b/tree/styles.go @@ -0,0 +1,74 @@ +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 + IndenterStyle 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) + s.IndenterStyle = lipgloss.NewStyle().Foreground(verySubduedColor) + s.OpenIndicatorStyle = lipgloss.NewStyle().Foreground(subduedColor) + + return s +} diff --git a/tree/tree.go b/tree/tree.go new file mode 100644 index 00000000..66c61f30 --- /dev/null +++ b/tree/tree.go @@ -0,0 +1,656 @@ +package tree + +import ( + "strings" + + 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" + "github.com/charmbracelet/bubbles/viewport" +) + +const spacebar = " " + +// KeyMap is the key bindings for different actions within the tree. +type KeyMap struct { + 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 + Open key.Binding + Close key.Binding + + // Help toggle keybindings. + ShowFullHelp key.Binding + CloseFullHelp key.Binding + + Quit key.Binding +} + +// DefaultKeyMap returns the default set of key bindings for navigating and acting +// upon the tree. +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"), + ), + + 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"), + ), + + 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 { + 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. + 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 + styles Styles + Help help.Model + + // 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 + + root *Node + + enumerator *ltree.Enumerator + indenter *ltree.Indenter + + viewport viewport.Model + width int + height int + // yOffset is the vertical offset of the selected node. + yOffset int +} + +// New creates a new model with default settings. +func New(t *Node, width, height int) Model { + m := Model{ + KeyMap: DefaultKeyMap(), + OpenCharacter: "▼", + ClosedCharacter: "▶", + CursorCharacter: "→", + Help: help.New(), + ScrollOff: 5, + + showHelp: true, + root: t, + viewport: viewport.Model{}, + } + + if m.root == nil { + m.root = NewNode() + } + + m.SetStyles(DefaultStyles()) + m.SetSize(width, height) + if m.root != nil { + m.setAttributes() + m.updateStyles() + m.updateViewport(0) + } + return m +} + +// Update is the Bubble Tea update loop. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.KeyMap.Down): + m.Down() + case key.Matches(msg, m.KeyMap.Up): + m.Up() + case key.Matches(msg, m.KeyMap.PageDown): + m.PageDown() + case key.Matches(msg, m.KeyMap.PageUp): + m.PageUp() + case key.Matches(msg, m.KeyMap.HalfPageDown): + m.HalfPageDown() + case key.Matches(msg, m.KeyMap.HalfPageUp): + m.HalfPageUp() + case key.Matches(msg, m.KeyMap.GoToTop): + m.GoToTop() + case key.Matches(msg, m.KeyMap.GoToBottom): + m.GoToBottom() + + case key.Matches(msg, m.KeyMap.Toggle): + m.ToggleCurrentNode() + case key.Matches(msg, m.KeyMap.Open): + m.OpenCurrentNode() + case key.Matches(msg, m.KeyMap.Close): + m.CloseCurrentNode() + + 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 + } + } + + // not sure why, but I think m.yOffset is captured in the closure, so we need to update the styles + return m, tea.Batch(cmds...) +} + +// View renders the component. +func (m Model) View() string { + treeView := m.viewport.View() + + var help string + if m.showHelp { + help = m.helpView() + } + + 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 + 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) +} + +// 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 + + // 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) { + if m.root == nil { + return + } + + m.yOffset = max(min(m.root.Size()-1, m.yOffset+movement), 0) + m.updateStyles() + + cursor := m.cursorView() + m.viewport.SetContent( + lipgloss.JoinHorizontal( + lipgloss.Top, + cursor, + m.styles.TreeStyle.Render(m.root.String()), + ), + ) + + // if this is the initial render, make sure we show the root node + if m.yOffset == 0 && 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) + + if m.viewport.YOffset > minTop { // reveal more lines above + m.viewport.SetYOffset(minTop) + } else if m.viewport.YOffset+height < minBottom+1 { // 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 { + styles.nodeFunc = styles.NodeStyleFunc + } else { + styles.nodeFunc = func(_ Nodes, _ int) lipgloss.Style { + return styles.NodeStyle + } + } + if styles.SelectedNodeStyleFunc != nil { + styles.selectedNodeFunc = styles.SelectedNodeStyleFunc + } else { + styles.selectedNodeFunc = func(_ Nodes, _ int) lipgloss.Style { + 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 + } + } + + 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 + // call SetSize as it takes into account width/height of the styles frame sizes + m.SetSize(m.width, m.height) + m.updateViewport(0) +} + +// SetShowHelp shows or hides the help view. +func (m *Model) SetShowHelp(v bool) { + m.showHelp = v + m.SetSize(m.width, m.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(width int) { + m.SetSize(width, m.height) +} + +// SetHeight sets the height of this component. +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 + if m.showHelp { + hv = lipgloss.Height(m.helpView()) + } + m.viewport.Height = height - hv + 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 { + kb := []key.Binding{ + m.KeyMap.Down, + m.KeyMap.Up, + m.KeyMap.Toggle, + } + + if m.AdditionalShortHelpKeys != nil { + kb = append(kb, m.AdditionalShortHelpKeys()...) + } + + kb = append(kb, m.KeyMap.Quit, m.KeyMap.ShowFullHelp) + + 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.Down, + m.KeyMap.Up, + m.KeyMap.Open, + m.KeyMap.Close, + 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 { + kb = append(kb, m.AdditionalFullHelpKeys()) + } + + kb = append(kb, []key.Binding{ + m.KeyMap.Quit, + m.KeyMap.CloseFullHelp, + }) + + return kb +} + +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...)) +} + +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) AllNodes() []*Node { + return m.root.AllNodes() +} + +func (m *Model) setAttributes() { + setDepths(m.root, 0) + setYOffsets(m.root) +} + +// setSizes updates each Node's size +// Note that if a child isn't open, its size is 1 +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.(*Node), depth+1) + } +} + +// 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.(*Node); 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 +} + +// Node returns the item at the given yoffset +func (m *Model) Node(yoffset int) *Node { + return findNode(m.root, yoffset) +} + +// NodeAtCurrentOffset returns the item at the current yoffset +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() { + if m.root != nil { + m.root.RootStyle(m.rootStyle()) + } + + 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, + } +} + +// // 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 { + return m.styles.selectedNodeFunc(Nodes{m.root}, 0) + } + + return m.styles.rootNodeFunc(Nodes{m.root}, 0) +} + +// findNode starts a DFS search for the node with the given yOffset +// starting from the given item +func findNode(t *Node, yOffset int) *Node { + 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.(*Node); ok { + found := findNode(child, yOffset) + if found != nil { + return found + } + } + } + + 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 +}