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
87 changes: 56 additions & 31 deletions pass/cmd/tui/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,12 @@ type Model struct {
gitStatusErr error

// State
loading bool
error error
quitting bool
selected string
width int
height int
error error
quitting bool
selected string
width int
height int
gitStatusReady bool // Indicates if git status has been loaded
}

// getTitle returns the title for the given mode
Expand Down Expand Up @@ -262,7 +262,11 @@ func getGitStatusLine(status git.GitStatus) string {

// Init initializes the model
func (m *Model) Init() tea.Cmd {
return textinput.Blink
// Start async git status check
return tea.Batch(
textinput.Blink,
loadGitStatusCmd(getPasswordStoreDir()),
)
}

// Update handles messages and updates the model
Expand Down Expand Up @@ -303,20 +307,29 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.error = fmt.Errorf("git push failed: %v", msg.err)
} else {
// Refresh git status and password list after successful push
m.gitStatus = git.GetGitStatus(getPasswordStoreDir())
m.gitStatusReady = false
cmds = append(cmds, loadGitStatusCmd(getPasswordStoreDir()))
m.refreshPasswordList()
}
return m, nil
return m, tea.Batch(cmds...)

case gitUpdateResult:
// Handle git update result
if msg.err != nil {
m.error = fmt.Errorf("git update failed: %v", msg.err)
} else {
// Refresh git status and password list after successful update
m.gitStatus = git.GetGitStatus(getPasswordStoreDir())
m.gitStatusReady = false
cmds = append(cmds, loadGitStatusCmd(getPasswordStoreDir()))
m.refreshPasswordList()
}
return m, tea.Batch(cmds...)

case gitStatusResult:
// Handle async git status result
m.gitStatus = msg.status
m.gitStatusErr = msg.err
m.gitStatusReady = true
return m, nil
}

Expand All @@ -334,10 +347,6 @@ func (m *Model) View() string {
return ""
}

if m.loading {
return "Loading passwords...\n"
}

if m.error != nil {
return "Error: " + m.error.Error() + "\n"
}
Expand All @@ -349,17 +358,18 @@ func (m *Model) View() string {
view += getTitle(m.mode) + "\n"

// Git status line (if available)
if m.gitStatus.IsGitRepo {
if !m.gitStatusReady {
view += "Git: loading...\n"
} else if m.gitStatusErr != nil {
view += fmt.Sprintf("Git: %v\n", m.gitStatusErr)
} else if m.gitStatus.IsGitRepo {
gitStatusLine := getGitStatusLine(m.gitStatus)
if gitStatusLine != "" {
view += gitStatusLine + "\n"
}
} else if m.gitStatusErr != nil {
view += fmt.Sprintf("Git: %v\n", m.gitStatusErr)
} else {
view += "Git: not a repository\n"
}
view += ""

// Input prompt and value
prompt := getPrompt(m.mode)
Expand Down Expand Up @@ -411,9 +421,9 @@ func (m *Model) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, handleGitUpdate

case msg.String() == "ctrl+r":
// Refresh git status
m.gitStatus = git.GetGitStatus(getPasswordStoreDir())
return m, nil
// Refresh git status asynchronously
m.gitStatusReady = false
return m, loadGitStatusCmd(getPasswordStoreDir())

// List navigation keys - pass to list
case msg.String() == "up" || msg.String() == "down" || msg.String() == "pageup" || msg.String() == "pagedown":
Expand Down Expand Up @@ -500,6 +510,20 @@ type gitUpdateResult struct {
err error
}

// gitStatusResult represents the result of async git status loading
type gitStatusResult struct {
status git.GitStatus
err error
}

// loadGitStatusCmd creates a command to load git status asynchronously
func loadGitStatusCmd(dir string) tea.Cmd {
return func() tea.Msg {
status := git.GetGitStatus(dir)
return gitStatusResult{status: status, err: nil}
}
}

// handleGitPush attempts to push changes to the remote
func handleGitPush() tea.Msg {
storeDir := getPasswordStoreDir()
Expand Down Expand Up @@ -541,17 +565,18 @@ func NewModel(passwords []string, mode FuzzySearchMode) *Model {
listModel.SetWidth(76)
listModel.SetHeight(15)

// Create model
// Create model with empty git status (will be loaded async)
model := &Model{
list: listModel,
input: input,
mode: mode,
allPasswords: passwords,
gitStatus: git.GetGitStatus(getPasswordStoreDir()),
loading: false,
quitting: false,
width: 80,
height: 24,
list: listModel,
input: input,
mode: mode,
allPasswords: passwords,
gitStatus: git.GitStatus{IsGitRepo: false},
gitStatusErr: nil,
quitting: false,
gitStatusReady: false,
width: 80,
height: 24,
}

return model
Expand Down
4 changes: 2 additions & 2 deletions pass/cmd/tui/tui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ func TestNewModel(t *testing.T) {
t.Errorf("Expected %d passwords, got %d", len(passwords), len(model.allPasswords))
}

if model.loading {
t.Error("Model should not be loading initially")
if model.gitStatusReady {
t.Error("Model gitStatusReady should be false initially")
}

if model.quitting {
Expand Down
76 changes: 76 additions & 0 deletions pass/demo_async_git_status.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/bin/bash

# Demonstration script for async git status loading in pass TUI
# This script shows the performance improvement when loading git status asynchronously

echo "=== Pass TUI Async Git Status Loading Demo ==="
echo ""

echo "This demo shows that the TUI now loads immediately and displays 'Git: loading...'"
echo "while the git status is being fetched asynchronously in the background."
echo ""

echo "Before the fix:"
echo " - TUI would hang for several seconds during 'git fetch'"
echo " - User couldn't interact with the password list"
echo " - Poor user experience, especially with slow networks"
echo ""

echo "After the fix:"
echo " - TUI appears instantly"
echo " - User can navigate and select passwords immediately"
echo " - Git status appears automatically when ready"
echo " - All git operations (push, update, refresh) are async"
echo ""

echo "=== Technical Implementation ==="
echo ""
echo "1. Modified Model struct:"
echo " - Added 'gitStatusReady' field to track loading state"
echo " - Removed 'loading' field (replaced by gitStatusReady)"
echo ""
echo "2. Modified NewModel():"
echo " - Initialize with empty git status (IsGitRepo: false)"
echo " - Set gitStatusReady to false initially"
echo ""
echo "3. Modified Init():"
echo " - Start async git status load using loadGitStatusCmd()"
echo " - Returns tea.Batch with textinput.Blink and async command"
echo ""
echo "4. Added loadGitStatusCmd():"
echo " - Returns tea.Cmd that fetches git status in background"
echo " - Returns gitStatusResult message when complete"
echo ""
echo "5. Modified Update():"
echo " - Handle gitStatusResult message"
echo " - Update gitStatus and gitStatusErr fields"
echo " - Set gitStatusReady to true"
echo " - All git operations now trigger async refresh"
echo ""
echo "6. Modified View():"
echo " - Show 'Git: loading...' while gitStatusReady is false"
echo " - Show actual git status when ready"
echo " - Show errors if they occurred"
echo ""

echo "=== Files Changed ==="
echo ""
echo "1. pass/cmd/tui/models.go - Main implementation"
echo "2. pass/cmd/tui/tui_test.go - Updated test for new field"
echo "3. specs/005-pass-git-integration-spec.md - Updated spec"
echo "4. specs/005-pass-git-integration-impl.md - Added implementation details"
echo ""

echo "=== Testing ==="
echo ""
echo "Run tests with:"
echo " cd pass && go test ./..."
echo ""
echo "Build with:"
echo " cd pass && go build -o pass"
echo ""
echo "Test the TUI with a git repo that has a slow remote:"
echo " PASSWORD_STORE_DIR=/path/to/your/password-store ./pass"
echo ""

echo "=== Demo Complete ==="
34 changes: 34 additions & 0 deletions specs/005-pass-git-integration-impl.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,37 @@ PASSWORD_STORE_DIR=/tmp/test_pass_git ./pass git
# Uncommitted changes present
```

## Performance Improvements

### Async Git Status Loading in TUI

**Problem**: After adding git integration, the TUI startup time was significantly slower because `GetGitStatus()` performs a `git fetch` operation synchronously, which is a network operation that can take several seconds.

**Solution**: Implemented asynchronous git status loading in the TUI:

1. **Initial Load**: The TUI now loads immediately with a "Git: loading..." indicator
2. **Background Loading**: Git status is fetched asynchronously in a separate goroutine
3. **Non-Blocking UI**: Users can navigate and select passwords while git status is loading
4. **Automatic Updates**: Git status is automatically displayed when the async operation completes
5. **Async Refresh**: All git operations (push, update, refresh) now trigger async status reloads

**Technical Implementation**:
- Added `gitStatusReady` field to `Model` struct to track loading state
- Modified `NewModel()` to initialize with empty git status
- Added `loadGitStatusCmd()` function that returns a `tea.Cmd` for async execution
- Modified `Init()` to start the async git status load
- Added `gitStatusResult` message type to handle async results
- Updated `Update()` to handle the async result message
- Updated `View()` to show "loading..." while status is being fetched
- Modified all git operation handlers to use async refresh

**Benefits**:
- ✅ TUI appears instantly, even with slow network or large git repos
- ✅ Users can interact with the password list immediately
- ✅ Git status appears automatically when ready
- ✅ No blocking operations in the UI thread
- ✅ All git operations maintain async behavior

## Known Limitations

1. **Ahead/Behind Detection**: The `getSyncStatus` function uses `git rev-list --left-right` which requires proper upstream tracking. This may not work correctly if:
Expand All @@ -181,6 +212,9 @@ PASSWORD_STORE_DIR=/tmp/test_pass_git ./pass git
## Future Enhancements

1. **Improve Ahead/Behind Detection**: Use `git fetch` first to ensure remote tracking is up to date.
2. **Add Timeout to Git Fetch**: Consider adding a timeout to the git fetch operation in `getSyncStatus` so that if it takes too long, the TUI can fall back to using stale local refs.
3. **Progress Indicator**: Add a spinner or progress indicator for better visual feedback during git status loading.
4. **Status Caching**: Cache git status results to avoid repeated fetches within a short time period.
2. **Add Confirmation Dialogs**: For push operations in TUI, add confirmation before pushing.
3. **Better Error Messages**: Provide more actionable error messages for git operations.
4. **Git Config Integration**: Respect git configuration for default remote/branch.
Expand Down
9 changes: 8 additions & 1 deletion specs/005-pass-git-integration-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ Search:
4. **Merge Conflicts**: For update, warn about conflicts, provide instructions
5. **Authentication Errors**: For push, prompt for credentials if needed

### Performance Considerations

1. **Async Loading**: Git status checking (especially `git fetch`) should not block the TUI. The status should load asynchronously while allowing user interaction.
2. **Non-Blocking Operations**: All git operations in the TUI (push, update, refresh) should be non-blocking and allow continued interaction.
3. **Visual Feedback**: Provide clear visual feedback (e.g., "loading..." indicator) when git operations are in progress.

### Dependencies

The implementation will use:
Expand Down Expand Up @@ -345,7 +351,8 @@ func setupTestGitRepo(t *testing.T) (string, func()) {
- [x] All tests pass
- [x] No personal data in tests or code
- [x] Documentation updated (README, specs)
- [ ] Password list refreshes after git operations
- [x] Password list refreshes after git operations
- [x] Git status loads asynchronously in TUI (non-blocking)

## Appendix

Expand Down
Loading