From 0c038ffae1294222c2e19d027e71fdbff63f6a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85smund=20=C3=98deg=C3=A5rd?= Date: Mon, 22 Jun 2026 20:02:03 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20perf:=20async=20git=20status=20load?= =?UTF-8?q?ing=20in=20TUI=20for=20improved=20startup=20performance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pass/cmd/tui/models.go | 87 +++++++++++++++++--------- pass/cmd/tui/tui_test.go | 4 +- pass/demo_async_git_status.sh | 76 ++++++++++++++++++++++ specs/005-pass-git-integration-impl.md | 34 ++++++++++ specs/005-pass-git-integration-spec.md | 9 ++- 5 files changed, 176 insertions(+), 34 deletions(-) create mode 100644 pass/demo_async_git_status.sh diff --git a/pass/cmd/tui/models.go b/pass/cmd/tui/models.go index 1db994f..e7cfe49 100644 --- a/pass/cmd/tui/models.go +++ b/pass/cmd/tui/models.go @@ -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 @@ -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 @@ -303,10 +307,11 @@ 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 @@ -314,9 +319,17 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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 } @@ -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" } @@ -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) @@ -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": @@ -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() @@ -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 diff --git a/pass/cmd/tui/tui_test.go b/pass/cmd/tui/tui_test.go index 753b8b6..6b58426 100644 --- a/pass/cmd/tui/tui_test.go +++ b/pass/cmd/tui/tui_test.go @@ -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 { diff --git a/pass/demo_async_git_status.sh b/pass/demo_async_git_status.sh new file mode 100644 index 0000000..5795dbf --- /dev/null +++ b/pass/demo_async_git_status.sh @@ -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 ===" \ No newline at end of file diff --git a/specs/005-pass-git-integration-impl.md b/specs/005-pass-git-integration-impl.md index 252fe04..be6a25e 100644 --- a/specs/005-pass-git-integration-impl.md +++ b/specs/005-pass-git-integration-impl.md @@ -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: @@ -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. diff --git a/specs/005-pass-git-integration-spec.md b/specs/005-pass-git-integration-spec.md index 5177e5f..101d5d0 100644 --- a/specs/005-pass-git-integration-spec.md +++ b/specs/005-pass-git-integration-spec.md @@ -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: @@ -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