Private
Public Access
1
0

Add pkg/util/dirtree

Update dir_tree tool to use it
Update filepicker bubble to use it
This commit is contained in:
2025-01-01 05:06:37 +00:00
parent 3cac7e8e11
commit 8e2991da1a
5 changed files with 340 additions and 346 deletions

View File

@@ -0,0 +1,113 @@
package filepicker
import (
"git.mlow.ca/mlow/lmcli/pkg/util/dirtree"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type Model struct {
tree *dirtree.Tree
cursor int
displayNodes []*dirtree.Node // Flattened view of visible nodes
}
func New(path string) Model {
m := Model{
tree: dirtree.NewTree(path),
}
m.tree.Root.LoadChildren(false, 0, nil)
m.updateRenderTree()
return m
}
func (m *Model) updateRenderTree() {
m.displayNodes = dirtree.FlattenTree(m.tree.Root, false)
}
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "j":
if m.cursor < len(m.displayNodes)-1 {
m.cursor++
}
case "k":
if m.cursor > 0 {
m.cursor--
}
case "l", "right":
current := m.displayNodes[m.cursor]
if current.IsDir() {
current.Expanded = true
current.LoadChildren(false, 0, nil)
m.updateRenderTree()
}
case "h", "left":
current := m.displayNodes[m.cursor]
if current.IsDir() && current.Expanded {
current.Expanded = false
m.updateRenderTree()
} else {
// Find and collapse parent
if !current.IsRoot() {
current.Parent().Expanded = false
m.updateRenderTree()
// Move cursor to parent
for i, node := range m.displayNodes {
if node == current.Parent() {
m.cursor = i
break
}
}
}
}
case " ":
current := m.displayNodes[m.cursor]
if current.IsSelected() {
current.Deselect(true)
} else {
current.Select(true, 0)
}
m.updateRenderTree()
case "H":
m.tree.ShowHidden = !m.tree.ShowHidden
m.tree.Root.LoadChildren(false, 0, nil)
}
}
return m, nil
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) View() string {
// Define styles
dirStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")) // Blue for directories
fileStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("7")) // White for files
selectedStyle := lipgloss.NewStyle().Bold(true)
renderNode := func(node *dirtree.Node) string {
var nodeName string
if node.IsDir() {
caret := "▶ "
if node.Expanded {
caret = "▼ "
}
nodeName = caret + node.Name + "/"
nodeName = dirStyle.Render(nodeName)
} else {
nodeName = fileStyle.Render(node.Name)
}
if node.IsSelected() {
nodeName = selectedStyle.Render(nodeName)
}
return nodeName
}
return dirtree.RenderNodes(m.displayNodes, dirtree.Compact, m.cursor, renderNode)
}

View File

@@ -1,299 +0,0 @@
package filepicker
import (
"fmt"
"os"
"path/filepath"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type Node struct {
path string
name string
isDir bool
expanded bool
children []*Node
}
type Model struct {
root *Node
cursor int
showHidden bool
displayLines []*Node // Flattened view of visible nodes
rootPath string
SelectedPaths []string
expandedPaths map[string]bool // Track expanded paths
}
func New(path string) Model {
root := &Node{
path: path,
name: filepath.Base(path),
isDir: true,
expanded: true,
}
m := Model{
root: root,
rootPath: path,
SelectedPaths: make([]string, 0),
showHidden: false,
expandedPaths: make(map[string]bool),
}
m.loadDirectory(root)
m.updateDisplayLines()
return m
}
func (m *Model) loadDirectory(node *Node) error {
// Save current expanded state of children
for _, child := range node.children {
if child.expanded {
m.expandedPaths[child.path] = true
}
}
entries, err := os.ReadDir(node.path)
if err != nil {
return err
}
node.children = make([]*Node, 0)
for _, entry := range entries {
if !m.showHidden && strings.HasPrefix(entry.Name(), ".") {
continue
}
childPath := filepath.Join(node.path, entry.Name())
child := &Node{
path: childPath,
name: entry.Name(),
isDir: entry.IsDir(),
expanded: m.expandedPaths[childPath], // Restore expanded state
}
node.children = append(node.children, child)
}
return nil
}
func (m *Model) updateDisplayLines() {
m.displayLines = make([]*Node, 0)
m.flattenTree(m.root, 0)
}
func (m *Model) flattenTree(node *Node, depth int) {
m.displayLines = append(m.displayLines, node)
if node.isDir && node.expanded {
for _, child := range node.children {
m.flattenTree(child, depth+1)
}
}
}
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "j":
if m.cursor < len(m.displayLines)-1 {
m.cursor++
}
case "k":
if m.cursor > 0 {
m.cursor--
}
case "l":
current := m.displayLines[m.cursor]
if current.isDir {
current.expanded = true
m.expandedPaths[current.path] = true
m.loadDirectory(current)
m.updateDisplayLines()
}
case "h":
current := m.displayLines[m.cursor]
if current.isDir && current.expanded {
current.expanded = false
delete(m.expandedPaths, current.path)
m.updateDisplayLines()
} else {
// Find and collapse parent
parent := m.findParent(current)
if parent != nil && parent.path != m.rootPath {
parent.expanded = false
delete(m.expandedPaths, parent.path)
m.updateDisplayLines()
// Move cursor to parent
for i, node := range m.displayLines {
if node == parent {
m.cursor = i
break
}
}
}
}
case " ":
current := m.displayLines[m.cursor]
if m.isSelected(current.path) {
m.SelectedPaths = removeString(m.SelectedPaths, current.path)
} else {
m.SelectedPaths = append(m.SelectedPaths, current.path)
}
case "H":
m.showHidden = !m.showHidden
m.reloadTree()
}
}
return m, nil
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) View() string {
var sb strings.Builder
// Define styles
dirStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")) // Blue for directories
fileStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("7")) // White for files
cursorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("yellow"))
selectedStyle := lipgloss.NewStyle().Bold(true)
// Track the parent-child relationships for proper indentation
levelIsLast := make(map[int]bool)
for i, node := range m.displayLines {
// Calculate depth and prefix
depth := 0
if node != m.root {
depth = strings.Count(node.path[len(m.rootPath):], string(os.PathSeparator))
}
// Build the prefix (tree structure)
var prefix strings.Builder
if node != m.root {
// For each level up to current depth, add the appropriate prefix
for d := 0; d < depth; d++ {
if levelIsLast[d] {
prefix.WriteString(" ")
} else {
prefix.WriteString("│ ")
}
}
// Find if this node is the last among its siblings
parent := m.findParent(node)
if parent != nil {
isLast := parent.children[len(parent.children)-1] == node
levelIsLast[depth] = isLast
if isLast {
prefix.WriteString("└── ")
} else {
prefix.WriteString("├── ")
}
}
}
// Cursor indicator
cursor := " "
if i == m.cursor {
cursor = cursorStyle.Render(">")
}
// Node name with optional caret for directories
var nodeName string
if node.isDir {
caret := "▶ "
if node.expanded {
caret = "▼ "
}
nodeName = caret + node.name
if node != m.root {
nodeName += "/"
}
nodeName = dirStyle.Render(nodeName)
} else {
nodeName = fileStyle.Render(node.name)
}
// Apply bold style if selected
if m.isSelected(node.path) {
nodeName = selectedStyle.Render(nodeName)
}
// Build the line
line := fmt.Sprintf("%s %s%s\n",
cursor,
prefix.String(),
nodeName)
sb.WriteString(line)
}
return sb.String()
}
func (m *Model) reloadTree() {
// Save expanded state before reloading
m.saveExpandedState(m.root)
m.root.children = nil
m.loadDirectory(m.root)
m.updateDisplayLines()
}
func (m *Model) saveExpandedState(node *Node) {
if node.isDir && node.expanded {
m.expandedPaths[node.path] = true
for _, child := range node.children {
m.saveExpandedState(child)
}
}
}
func (m *Model) findParent(node *Node) *Node {
var findParentRec func(*Node) *Node
findParentRec = func(current *Node) *Node {
if current == nil {
return nil
}
for _, child := range current.children {
if child == node {
return current
}
if result := findParentRec(child); result != nil {
return result
}
}
return nil
}
return findParentRec(m.root)
}
func (m Model) isSelected(path string) bool {
for _, selected := range m.SelectedPaths {
if selected == path {
return true
}
}
return false
}
func removeString(slice []string, s string) []string {
for i, v := range slice {
if v == s {
return append(slice[:i], slice[i+1:]...)
}
}
return slice
}