diff --git a/pkg/tui/bubbles/filepicker/model.go b/pkg/tui/bubbles/filepicker/model.go index 324ae8e..d0835f3 100644 --- a/pkg/tui/bubbles/filepicker/model.go +++ b/pkg/tui/bubbles/filepicker/model.go @@ -11,264 +11,289 @@ import ( ) type Node struct { - path string - name string - isDir bool - expanded bool - children []*Node + 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 + root *Node + cursor int + showHidden bool + displayLines []*Node // Flattened view of visible nodes + rootPath string - SelectedPaths []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, - } + root := &Node{ + path: path, + name: filepath.Base(path), + isDir: true, + expanded: true, + } - m := Model{ - root: root, - rootPath: path, - SelectedPaths: make([]string, 0), - showHidden: false, - } + m := Model{ + root: root, + rootPath: path, + SelectedPaths: make([]string, 0), + showHidden: false, + expandedPaths: make(map[string]bool), + } - m.loadDirectory(root) - m.updateDisplayLines() - return m + m.loadDirectory(root) + m.updateDisplayLines() + return m } func (m *Model) loadDirectory(node *Node) error { - entries, err := os.ReadDir(node.path) - if err != nil { - return err - } + // Save current expanded state of children + for _, child := range node.children { + if child.expanded { + m.expandedPaths[child.path] = true + } + } - node.children = make([]*Node, 0) - for _, entry := range entries { - if !m.showHidden && strings.HasPrefix(entry.Name(), ".") { - continue - } + entries, err := os.ReadDir(node.path) + if err != nil { + return err + } - child := &Node{ - path: filepath.Join(node.path, entry.Name()), - name: entry.Name(), - isDir: entry.IsDir(), - expanded: false, - } - node.children = append(node.children, child) - } - return nil + 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) + 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) - } - } + 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.loadDirectory(current) - m.updateDisplayLines() - } - case "h": - current := m.displayLines[m.cursor] - if current.isDir && current.expanded { - current.expanded = false - m.updateDisplayLines() - } else { - // Find and collapse parent - parent := m.findParent(current) - if parent != nil && parent.path != m.rootPath { - parent.expanded = false - 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 + 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 + return nil } func (m Model) View() string { - var sb strings.Builder + 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) + // 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) + // 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)) - } + 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 + // 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("│ ") - } - } + 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 + // 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("├── ") - } - } - } + if isLast { + prefix.WriteString("└── ") + } else { + prefix.WriteString("├── ") + } + } + } - // Cursor indicator - cursor := " " - if i == m.cursor { - cursor = cursorStyle.Render(">") - } + // 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) - } + // 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) - } + // 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) + // Build the line + line := fmt.Sprintf("%s %s%s\n", + cursor, + prefix.String(), + nodeName) - sb.WriteString(line) - } + sb.WriteString(line) + } - return sb.String() + return sb.String() } func (m *Model) reloadTree() { - m.root.children = nil - m.loadDirectory(m.root) - m.updateDisplayLines() + // 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 + 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 - } + 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) + return findParentRec(m.root) } func (m Model) isSelected(path string) bool { - for _, selected := range m.SelectedPaths { - if selected == path { - return true - } - } - return false + 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 + for i, v := range slice { + if v == s { + return append(slice[:i], slice[i+1:]...) + } + } + return slice }