From 7b1890477535b405ebeaf8a17a958af4f19b1296 Mon Sep 17 00:00:00 2001 From: ruinivist Date: Thu, 1 Jan 2026 01:03:32 +0000 Subject: [PATCH] add keyboard based movement on the file tree --- fstree/fs.go | 9 +++++ fstree/fstree.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++-- main.go | 12 ++++++ styles/styles.go | 1 + 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/fstree/fs.go b/fstree/fs.go index 3248966..ddf3ef9 100644 --- a/fstree/fs.go +++ b/fstree/fs.go @@ -12,6 +12,7 @@ import ( "errors" "os" "path/filepath" + "sort" ) func walkFileSystemAndBuildTree(rootPath string, node *FsNode) error { @@ -60,6 +61,14 @@ func walkFileSystemAndBuildTree(rootPath string, node *FsNode) error { } } + // 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() + } + return node.children[i].nodeType == FileNode + }) + return nil } diff --git a/fstree/fstree.go b/fstree/fstree.go index a986da8..8eff0fe 100644 --- a/fstree/fstree.go +++ b/fstree/fstree.go @@ -34,7 +34,7 @@ type FsNode struct { children []*FsNode parent *FsNode // for fast traversal up the tree // for view layer - expanded bool + expanded bool // makes sense only for folder nodes } func (n *FsNode) FileName() string { @@ -43,7 +43,8 @@ func (n *FsNode) FileName() string { // ==================== FsNode definition ==================== type FsTree struct { - root *FsNode + root *FsNode + selected *FsNode // currently selected node } func NewFsTree(rootPath string) *FsTree { @@ -55,6 +56,12 @@ func NewFsTree(rootPath string) *FsTree { } walkFileSystemAndBuildTree(rootPath, root) + if len(root.children) > 0 { + return &FsTree{ + root: root, + selected: root.children[0], + } + } return &FsTree{ root: root, } @@ -108,6 +115,86 @@ 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 { + 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 + } + return nil +} + +func (t *FsTree) MoveUp() error { return t.move(-1) } +func (t *FsTree) MoveDown() error { return t.move(1) } + +func (t *FsTree) ToggleSelectedExpand() error { + if t.selected == nil { + return errors.New("no node is currently selected") + } + if t.selected.nodeType != FolderNode { + return errors.New("only folder nodes can be expanded or collapsed") + } + + t.ToggleExpand(t.selected) + return nil +} + func (t *FsTree) Render() string { builder := &strings.Builder{} t.renderNode(t.root, 0, builder) @@ -135,7 +222,13 @@ func (t *FsTree) renderNode(node *FsNode, depth int, builder *strings.Builder) { icon = lipgloss.NewStyle().Faint(true).Render(prevIndent + icon + indent) } - line := icon + " " + node.FileName() + "\n" + // Apply highlight if selected + fileName := node.FileName() + if node == t.selected { + fileName = lipgloss.NewStyle().Foreground(styles.Highlight).Bold(true).Render(fileName) + } + + line := icon + " " + fileName + "\n" builder.WriteString(line) if node.expanded { diff --git a/main.go b/main.go index 6102573..a494c0b 100644 --- a/main.go +++ b/main.go @@ -95,6 +95,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.String() { case "q", "ctrl+c": return m, tea.Quit + case "up", "k": + if err := m.tree.MoveUp(); err != nil { + fmt.Println("Error moving up:", err) + } + case "down", "j": + if err := m.tree.MoveDown(); err != nil { + fmt.Println("Error moving down:", err) + } + case "enter", " ": + if err := m.tree.ToggleSelectedExpand(); err != nil { + fmt.Println("Error toggling expand:", err) + } } } return m, nil diff --git a/styles/styles.go b/styles/styles.go index 69e3e5f..def01ea 100644 --- a/styles/styles.go +++ b/styles/styles.go @@ -14,6 +14,7 @@ var ( Primary = lipgloss.Color("#7D56F4") Secondary = lipgloss.Color("#FF5C8F") Background = lipgloss.Color("#1E1E2E") + Highlight = lipgloss.Color("#FFA500") // Orange highlight for selected node // TODO: to extend as needed, right now this is just a // placeholder )