From 30b801a38567b9314f3f17641c0c5b5a873cea15 Mon Sep 17 00:00:00 2001 From: ruinivist Date: Thu, 1 Jan 2026 02:25:35 +0000 Subject: [PATCH] simplify moving by flattening tree + add click to load file --- .gitignore | 3 +- fstree/fs.go | 59 +++++++++++++-------------- fstree/fs_test.go | 17 +++++--- fstree/fstree.go | 102 ++++++++++++++++------------------------------ main.go | 13 ++++++ 5 files changed, 91 insertions(+), 103 deletions(-) diff --git a/.gitignore b/.gitignore index 1793ea9..d7c9812 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .gemini -prompts \ No newline at end of file +prompts +mend \ No newline at end of file diff --git a/fstree/fs.go b/fstree/fs.go index ddf3ef9..226d102 100644 --- a/fstree/fs.go +++ b/fstree/fs.go @@ -12,10 +12,9 @@ import ( "errors" "os" "path/filepath" - "sort" ) -func walkFileSystemAndBuildTree(rootPath string, node *FsNode) error { +func walkFileSystemAndBuildTree(rootPath string, node *FsNode, flatTree *[]*FsNode) error { if node == nil { return errors.New("node cannot be nil") } @@ -28,46 +27,46 @@ func walkFileSystemAndBuildTree(rootPath string, node *FsNode) error { return err } + files := make([]os.DirEntry, 0) + folders := make([]os.DirEntry, 0) + for _, entry := range entries { // dot folders and files skipped if len(entry.Name()) > 0 && entry.Name()[0] == '.' { continue } - childPath := filepath.Join(rootPath, entry.Name()) - - var nodeType FsNodeType if entry.IsDir() { - nodeType = FolderNode + folders = append(folders, entry) } else { - nodeType = FileNode - } - - childNode := &FsNode{ - nodeType: nodeType, - path: childPath, - children: make([]*FsNode, 0), - parent: node, - expanded: true, - } - - node.children = append(node.children, childNode) - - // Recursively walk subdirectories - if entry.IsDir() { - if err := walkFileSystemAndBuildTree(childPath, childNode); err != nil { - return err - } + files = append(files, entry) } } - // Sort children: files first, then folders - sort.Slice(node.children, func(i, j int) bool { - if node.children[i].nodeType == node.children[j].nodeType { - return node.children[i].FileName() < node.children[j].FileName() + *flatTree = append(*flatTree, node) // add self + for _, file := range files { + newNode := &FsNode{ + nodeType: FileNode, + path: filepath.Join(rootPath, file.Name()), + children: make([]*FsNode, 0), + parent: node, + expanded: false, } - return node.children[i].nodeType == FileNode - }) + node.children = append(node.children, newNode) + *flatTree = append(*flatTree, newNode) + } + + for _, folder := range folders { + newNode := &FsNode{ + nodeType: FolderNode, + path: filepath.Join(rootPath, folder.Name()), + children: make([]*FsNode, 0), + parent: node, + expanded: true, // all expanded by default + } + node.children = append(node.children, newNode) + walkFileSystemAndBuildTree(newNode.path, newNode, flatTree) + } return nil } diff --git a/fstree/fs_test.go b/fstree/fs_test.go index a9ea59a..acd61a3 100644 --- a/fstree/fs_test.go +++ b/fstree/fs_test.go @@ -37,7 +37,8 @@ func TestWalkFileSystemAndBuildTree(t *testing.T) { expanded: true, } - err := walkFileSystemAndBuildTree(tmpDir, root) + flatTree := make([]*FsNode, 0) + err := walkFileSystemAndBuildTree(tmpDir, root, &flatTree) if err != nil { t.Fatalf("walkFileSystemAndBuildTree() error = %v", err) } @@ -73,7 +74,7 @@ func TestWalkFileSystemAndBuildTree(t *testing.T) { } if folderCount != 2 { t.Errorf("folder count = %d, want 2", folderCount) - } + } // Update viewport with selected file content // Verify nested structure if folder1 == nil { @@ -92,7 +93,8 @@ func TestWalkFileSystemAndBuildTree(t *testing.T) { t.Run("error when node is nil", func(t *testing.T) { tmpDir := t.TempDir() - err := walkFileSystemAndBuildTree(tmpDir, nil) + flatTree := make([]*FsNode, 0) + err := walkFileSystemAndBuildTree(tmpDir, nil, &flatTree) if err == nil { t.Fatal("walkFileSystemAndBuildTree() with nil node should return error") } @@ -110,7 +112,8 @@ func TestWalkFileSystemAndBuildTree(t *testing.T) { expanded: true, } - err := walkFileSystemAndBuildTree(tmpDir, root) + flatTree := make([]*FsNode, 0) + err := walkFileSystemAndBuildTree(tmpDir, root, &flatTree) if err == nil { t.Fatal("walkFileSystemAndBuildTree() with existing children should return error") } @@ -127,7 +130,8 @@ func TestWalkFileSystemAndBuildTree(t *testing.T) { expanded: true, } - err := walkFileSystemAndBuildTree("/nonexistent/path", root) + flatTree := make([]*FsNode, 0) + err := walkFileSystemAndBuildTree("/nonexistent/path", root, &flatTree) if err == nil { t.Fatal("walkFileSystemAndBuildTree() with nonexistent path should return error") } @@ -142,7 +146,8 @@ func TestWalkFileSystemAndBuildTree(t *testing.T) { expanded: true, } - err := walkFileSystemAndBuildTree(tmpDir, root) + flatTree := make([]*FsNode, 0) + err := walkFileSystemAndBuildTree(tmpDir, root, &flatTree) if err != nil { t.Fatalf("walkFileSystemAndBuildTree() error = %v", err) } diff --git a/fstree/fstree.go b/fstree/fstree.go index a14bf75..481d6ad 100644 --- a/fstree/fstree.go +++ b/fstree/fstree.go @@ -45,7 +45,15 @@ func (n *FsNode) FileName() string { // ==================== FsNode definition ==================== type FsTree struct { root *FsNode - selected *FsNode // currently selected node + selected int + lines []*FsNode // flattened view of nodes +} + +func (t *FsTree) selectedNode() *FsNode { + if t.selected <= 0 || t.selected >= len(t.lines) { + return nil + } + return t.lines[t.selected] } func NewFsTree(rootPath string) *FsTree { @@ -55,16 +63,19 @@ func NewFsTree(rootPath string) *FsTree { children: make([]*FsNode, 0), expanded: true, } - walkFileSystemAndBuildTree(rootPath, root) + flatTree := make([]*FsNode, 0) + walkFileSystemAndBuildTree(rootPath, root, &flatTree) if len(root.children) > 0 { return &FsTree{ root: root, - selected: root.children[0], + selected: 1, // first child + lines: flatTree, } } return &FsTree{ - root: root, + root: root, + lines: flatTree, } } @@ -116,96 +127,55 @@ func (t *FsTree) ToggleExpand(node *FsNode) error { return nil } -// At this point I think there HAS to be a simpler way to do this -// but since I'm going to refactor it some day let's leave this -// as it is func (t *FsTree) move(delta int) error { - if t.selected == nil { + if t.selectedNode() == nil { return errors.New("no node is currently selected") } if delta != -1 && delta != 1 { return errors.New("delta must be either -1 (up) or 1 (down)") } - // base case: if it's a folder and expanded and going down - if delta == 1 && t.selected.nodeType == FolderNode && t.selected.expanded && len(t.selected.children) > 0 { - t.selected = t.selected.children[0] - return nil - } - // base case: if it's the first element of a folder and going up - if delta == -1 && t.selected.parent != t.root { - parent := t.selected.parent - if len(parent.children) > 0 && parent.children[0] == t.selected { - t.selected = parent - return nil - } - } - - // same level sibling move - // the idea is really simple for now: - // - go up the parent and find the sibling in the direction - // - if no sibling go up again - // - if no parent stop - siblingFor := t.selected - for siblingFor.parent != nil { - parent := siblingFor.parent - // O(n) for now, though easy to obtimize with index tracking - nextIdx := -1 - for idx, child := range parent.children { - if child == siblingFor { - nextIdx = idx + delta - break - } - } - - if nextIdx >= 0 && nextIdx < len(parent.children) { - // found case - t.selected = parent.children[nextIdx] - break - } - // not found case, go up again - siblingFor = parent - } - - // case: folder and I'm going up, I want to go as deep and last - if delta == -1 && t.selected.nodeType == FolderNode { - current := t.selected - for { - if len(current.children) == 0 || !current.expanded { - break - } - current = current.children[len(current.children)-1] - } - t.selected = current + newIndex := t.selected + delta + if newIndex < 1 || newIndex >= len(t.lines) { + return nil // noop } + t.selected = newIndex return nil } func (t *FsTree) MoveUp() error { return t.move(-1) } func (t *FsTree) MoveDown() error { return t.move(1) } +func (t *FsTree) SelectNodeAtLine(line int) error { + if line < 0 || line >= len(t.lines) { + return errors.New("line number out of bounds") + } + t.selected = line + return nil +} + func (t *FsTree) ToggleSelectedExpand() error { - if t.selected == nil { + if t.selectedNode() == nil { return errors.New("no node is currently selected") } - if t.selected.nodeType != FolderNode { + if t.selectedNode().nodeType != FolderNode { return errors.New("only folder nodes can be expanded or collapsed") } - t.ToggleExpand(t.selected) + t.ToggleExpand(t.selectedNode()) return nil } func (t *FsTree) GetSelectedContent() (string, error) { - if t.selected == nil { + if t.selectedNode() == nil { return "", errors.New("no node is currently selected") } - if t.selected.nodeType != FileNode { + if t.selectedNode().nodeType != FileNode { return "", nil } - contentBytes, err := os.ReadFile(t.selected.path) + contentBytes, err := os.ReadFile(t.selectedNode().path) if err != nil { return "", err } @@ -239,9 +209,9 @@ func (t *FsTree) renderNode(node *FsNode, depth int, builder *strings.Builder) { icon = lipgloss.NewStyle().Faint(true).Render(prevIndent + icon + indent) } - // Apply highlight if selected + // highlight if selected fileName := node.FileName() - if node == t.selected { + if node == t.selectedNode() { fileName = lipgloss.NewStyle().Foreground(styles.Highlight).Bold(true).Render(fileName) } diff --git a/main.go b/main.go index 0a500a3..c008867 100644 --- a/main.go +++ b/main.go @@ -104,6 +104,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd + case tea.MouseMsg: + if msg.Type == tea.MouseLeft { + // within file tree + if msg.X < m.width { + if err := m.tree.SelectNodeAtLine(msg.Y); err == nil { + content, err := m.tree.GetSelectedContent() + if err == nil { + m.viewport.SetContent(content) + } + } + } + } case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c": @@ -161,6 +173,7 @@ func main() { p := tea.NewProgram( createModel(), tea.WithAltScreen(), // full screen tui + tea.WithMouseCellMotion(), ) if _, err := p.Run(); err != nil { fmt.Printf("Error: %v", err)