filepicker: Track expanded paths
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user