Private
Public Access
1
0
Files
lmcli/pkg/tui/bubbles/filepicker/model.go
2024-12-27 06:14:40 +00:00

275 lines
7.0 KiB
Go

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
}
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,
}
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
}
node.children = make([]*Node, 0)
for _, entry := range entries {
if !m.showHidden && strings.HasPrefix(entry.Name(), ".") {
continue
}
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
}
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.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
}
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() {
m.root.children = nil
m.loadDirectory(m.root)
m.updateDisplayLines()
}
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
}