Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions pass/cmd/tui/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -691,4 +720,4 @@ func (m *Model) refreshPasswordList() {
m.allPasswords = passwords
// Re-filter with current query
m.filterList()
}
}
2 changes: 1 addition & 1 deletion pass/cmd/tui/tree_view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
19 changes: 11 additions & 8 deletions specs/006-tui-tree-view/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ The `pass find <string>` 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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
Loading