From 113627c80c85e26a0ef4893824fcd34a5411a16b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85smund=20=C3=98deg=C3=A5rd?= Date: Sun, 28 Jun 2026 00:06:10 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix=20tui=20tree=20view=20direct?= =?UTF-8?q?ory=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When selecting a directory in the tree view, automatically select the first password file within that directory instead of failing with 'No such file or directory'. This allows users to see the full hierarchy while ensuring only actual password files are actionable. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- pass/cmd/tui/models.go | 37 +++++++++++++++++++++++++++++---- pass/cmd/tui/tree_view_test.go | 2 +- specs/006-tui-tree-view/spec.md | 19 ++++++++++------- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/pass/cmd/tui/models.go b/pass/cmd/tui/models.go index f3e8fc8..25ea287 100644 --- a/pass/cmd/tui/models.go +++ b/pass/cmd/tui/models.go @@ -20,6 +20,8 @@ type item struct { path string matchScore int matchIndices []int + isDir bool + isPassword bool } // Title returns the title of the item (the password path) @@ -72,6 +74,18 @@ func CreateTreeFormattedItems(passwords []string) []list.Item { return items } +// findFirstPasswordUnderPath finds the first password in the list that is under the given directory path +func findFirstPasswordUnderPath(items []list.Item, dirPath string) string { + for _, listItem := range items { + if treeItem, ok := listItem.(treeFormattedItem); ok { + if treeItem.isPassword && strings.HasPrefix(treeItem.path, dirPath+"/") { + return treeItem.path + } + } + } + return "" +} + // flattenTreeToFormattedItems recursively flattens the tree to formatted items func flattenTreeToFormattedItems(node *tree.TreeNode, prefix string, isLast bool, parentPath string, items *[]list.Item) { // Build the full path for this node @@ -99,7 +113,9 @@ func flattenTreeToFormattedItems(node *tree.TreeNode, prefix string, isLast bool // Note: A node can be both a password (IsPassword=true) and a directory (IsDir=true) formattedItem := treeFormattedItem{ item: item{ - path: nodePath, + path: nodePath, + isDir: node.IsDir, + isPassword: node.IsPassword, }, displayName: displayName, } @@ -499,8 +515,21 @@ func (m *Model) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) { // Enter - select case msg.String() == "enter": if m.list.SelectedItem() != nil { - selectedItem := m.list.SelectedItem().(item) - m.selected = selectedItem.path + selectedItem := m.list.SelectedItem().(treeFormattedItem) + selectedPath := selectedItem.item.path + + // If selected item is a directory, find the first password under it + if selectedItem.item.isDir && !selectedItem.item.isPassword { + firstPassword := findFirstPasswordUnderPath(m.list.Items(), selectedPath) + if firstPassword != "" { + selectedPath = firstPassword + } else { + // No password under this directory, do nothing + return m, nil + } + } + + m.selected = selectedPath m.quitting = true return m, tea.Quit } @@ -691,4 +720,4 @@ func (m *Model) refreshPasswordList() { m.allPasswords = passwords // Re-filter with current query m.filterList() -} \ No newline at end of file +} diff --git a/pass/cmd/tui/tree_view_test.go b/pass/cmd/tui/tree_view_test.go index 0b66f32..398be9c 100644 --- a/pass/cmd/tui/tree_view_test.go +++ b/pass/cmd/tui/tree_view_test.go @@ -123,7 +123,7 @@ func TestTreeViewFiltering(t *testing.T) { model.input.SetValue("gmail") model.filterList() - // Should show gmail.com and its parent email/ + // Should show email/ and gmail.com (2 items) filtered := model.list.Items() if len(filtered) != 2 { t.Errorf("Expected 2 filtered items for 'gmail' (email/ and gmail.com), got %d", len(filtered)) diff --git a/specs/006-tui-tree-view/spec.md b/specs/006-tui-tree-view/spec.md index c4767d2..7695a75 100644 --- a/specs/006-tui-tree-view/spec.md +++ b/specs/006-tui-tree-view/spec.md @@ -53,13 +53,14 @@ The `pass find ` command should continue to use flat view as currently i So that I can better understand the directory hierarchy of my password store. **Acceptance Criteria**: -- [ ] TUI fuzzy finder displays passwords in tree structure -- [ ] Tree structure accurately reflects the directory hierarchy -- [ ] Tree uses box-drawing characters (├──, └──, │) -- [ ] Directories are shown with trailing `/` -- [ ] Files are shown without `.gpg` extension -- [ ] Results are sorted alphabetically at each level -- [ ] Works in all TUI modes (show, clip, rm, edit) +- [x] TUI fuzzy finder displays passwords in tree structure +- [x] Tree structure accurately reflects the directory hierarchy +- [x] Tree uses box-drawing characters (├──, └──, │) +- [x] Directories are shown with trailing `/` +- [x] Files are shown without `.gpg` extension +- [x] Results are sorted alphabetically at each level +- [x] Works in all TUI modes (show, clip, rm, edit) +- [x] Selecting a directory with Enter automatically selects the first password within it ### As a pass CLI user, I want `pass find` to maintain flat output So that my existing scripts and workflows continue to work. @@ -361,7 +362,7 @@ var TreeViewEnabled = true // Default to tree view **Status**: OPEN - Propose: Let list component handle truncation as it does now ### OQ-003: Should directories be selectable in tree view? -**Status**: OPEN - Propose: No, only leaf nodes (password files) should be selectable +**Status**: **RESOLVED** - Directories are displayed in the tree structure but when selected (Enter key), the TUI automatically selects the first password file within that directory. This allows users to see the full hierarchy while ensuring only actual password files are actionable. ## Success Criteria @@ -437,6 +438,8 @@ The tree view feature has been successfully implemented for the pass TUI fuzzy f 2. **CLI Flat View**: Modified `pass find` command to use flat view by default 3. **Tree Formatting**: Reused existing `cmd/tree` package for tree rendering 4. **Backward Compatibility**: All existing functionality preserved +5. **Directory Selection**: When Enter is pressed on a directory, automatically selects the first password file within that directory +6. **Item Metadata**: Added `isDir` and `isPassword` fields to distinguish between directory and password nodes ### Files Modified