package filetree import ( "os" "path/filepath" "sort" "strings" ) // Node represents a file or directory in the tree type Node struct { Name string Path string IsDir bool IsOpen bool Level int Parent *Node Children []*Node } // Model represents the file tree state type Model struct { Root *Node Nodes []*Node // Flattened visible nodes for navigation Cursor int RootPath string } // New creates a new file tree model func New(rootPath string) Model { absPath, err := filepath.Abs(rootPath) if err != nil { absPath = rootPath } root := &Node{ Name: filepath.Base(absPath), Path: absPath, IsDir: true, IsOpen: true, Level: 0, } m := Model{ Root: root, RootPath: absPath, Cursor: 0, } m.loadChildren(root) m.updateVisibleNodes() return m } // loadChildren loads the children of a directory node func (m *Model) loadChildren(node *Node) { if !node.IsDir { return } entries, err := os.ReadDir(node.Path) if err != nil { return } // Sort: directories first, then alphabetically sort.Slice(entries, func(i, j int) bool { iDir := entries[i].IsDir() jDir := entries[j].IsDir() if iDir != jDir { return iDir } return strings.ToLower(entries[i].Name()) < strings.ToLower(entries[j].Name()) }) node.Children = make([]*Node, 0, len(entries)) for _, entry := range entries { // Skip hidden files if strings.HasPrefix(entry.Name(), ".") { continue } child := &Node{ Name: entry.Name(), Path: filepath.Join(node.Path, entry.Name()), IsDir: entry.IsDir(), Level: node.Level + 1, Parent: node, } node.Children = append(node.Children, child) } } // updateVisibleNodes updates the flattened list of visible nodes func (m *Model) updateVisibleNodes() { m.Nodes = make([]*Node, 0) m.flattenNode(m.Root) } func (m *Model) flattenNode(node *Node) { m.Nodes = append(m.Nodes, node) if node.IsDir && node.IsOpen { for _, child := range node.Children { m.flattenNode(child) } } } // SelectedNode returns the currently selected node func (m *Model) SelectedNode() *Node { if m.Cursor < 0 || m.Cursor >= len(m.Nodes) { return nil } return m.Nodes[m.Cursor] } // Toggle expands or collapses the selected directory func (m *Model) Toggle() { node := m.SelectedNode() if node == nil || !node.IsDir { return } node.IsOpen = !node.IsOpen if node.IsOpen && len(node.Children) == 0 { m.loadChildren(node) } m.updateVisibleNodes() } // MoveUp moves the cursor up func (m *Model) MoveUp() { if m.Cursor > 0 { m.Cursor-- } } // MoveDown moves the cursor down func (m *Model) MoveDown() { if m.Cursor < len(m.Nodes)-1 { m.Cursor++ } } // Collapse collapses the current directory or moves to parent func (m *Model) Collapse() { node := m.SelectedNode() if node == nil { return } if node.IsDir && node.IsOpen { node.IsOpen = false m.updateVisibleNodes() } else if node.Parent != nil { // Find parent in visible nodes for i, n := range m.Nodes { if n == node.Parent { m.Cursor = i break } } } } // Expand expands the current directory func (m *Model) Expand() { node := m.SelectedNode() if node == nil || !node.IsDir { return } if !node.IsOpen { node.IsOpen = true if len(node.Children) == 0 { m.loadChildren(node) } m.updateVisibleNodes() } } // Ext returns the file extension of a node func (n *Node) Ext() string { if n.IsDir { return "" } return strings.ToLower(filepath.Ext(n.Name)) }