Private
Public Access
1
0

filepicker: Track expanded paths

This commit is contained in:
2024-12-27 06:18:49 +00:00
parent 6b629ed1d6
commit 3cac7e8e11

View File

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