diff --git a/pkg/app/app.go b/pkg/app/app.go index f8e267ee99b..a16fbcc1fe7 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -23,6 +23,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/env" "github.com/jesseduffield/lazygit/pkg/gui" "github.com/jesseduffield/lazygit/pkg/i18n" + integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" "github.com/jesseduffield/lazygit/pkg/logs" "github.com/jesseduffield/lazygit/pkg/updates" ) @@ -42,7 +43,7 @@ func Run( common *common.Common, startArgs appTypes.StartArgs, ) { - app, err := NewApp(config, common) + app, err := NewApp(config, startArgs.IntegrationTest, common) if err == nil { err = app.Run(startArgs) @@ -94,7 +95,7 @@ func newLogger(cfg config.AppConfigurer) *logrus.Entry { } // NewApp bootstrap a new application -func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) { +func NewApp(config config.AppConfigurer, test integrationTypes.IntegrationTest, common *common.Common) (*App, error) { app := &App{ closers: []io.Closer{}, Config: config, @@ -128,7 +129,7 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) { showRecentRepos = true } - app.Gui, err = gui.NewGui(common, config, gitVersion, updater, showRecentRepos, dirName) + app.Gui, err = gui.NewGui(common, config, gitVersion, updater, showRecentRepos, dirName, test) if err != nil { return app, err } diff --git a/pkg/cheatsheet/generate.go b/pkg/cheatsheet/generate.go index 392e9d64d4b..205abf7cb37 100644 --- a/pkg/cheatsheet/generate.go +++ b/pkg/cheatsheet/generate.go @@ -61,7 +61,7 @@ func generateAtDir(cheatsheetDir string) { if err != nil { log.Fatal(err) } - mApp, _ := app.NewApp(mConfig, common) + mApp, _ := app.NewApp(mConfig, nil, common) path := cheatsheetDir + "/Keybindings_" + lang + ".md" file, err := os.Create(path) if err != nil { diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index f972f2fbb4e..b5c1a3c20bf 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -90,7 +90,7 @@ func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string { return lo.Map(menuItems, func(item *types.MenuItem, _ int) []string { displayStrings := item.LabelColumns - if item.DisabledReason != "" { + if item.DisabledReason != nil { displayStrings[0] = style.FgDefault.SetStrikethrough().Sprint(displayStrings[0]) } @@ -172,8 +172,13 @@ func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Bin } func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error { - if selectedItem != nil && selectedItem.DisabledReason != "" { - return self.c.ErrorMsg(selectedItem.DisabledReason) + if selectedItem != nil && selectedItem.DisabledReason != nil { + if selectedItem.DisabledReason.ShowErrorInPanel { + return self.c.ErrorMsg(selectedItem.DisabledReason.Text) + } + + self.c.ErrorToast(self.c.Tr.DisabledMenuItemPrefix + selectedItem.DisabledReason.Text) + return nil } if err := self.c.PopContext(); err != nil { diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index 22390fc8e8b..e0a38d463ca 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -264,13 +264,13 @@ func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branc } if !selectedBranch.IsTrackingRemote() { - unsetUpstreamItem.DisabledReason = self.c.Tr.UpstreamNotSetError + unsetUpstreamItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError} } if !selectedBranch.RemoteBranchStoredLocally() { - viewDivergenceItem.DisabledReason = self.c.Tr.UpstreamNotSetError - upstreamResetItem.DisabledReason = self.c.Tr.UpstreamNotSetError - upstreamRebaseItem.DisabledReason = self.c.Tr.UpstreamNotSetError + viewDivergenceItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError} + upstreamResetItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError} + upstreamRebaseItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError} } options := []*types.MenuItem{ @@ -309,16 +309,16 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error { return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{}) } -func (self *BranchesController) getDisabledReasonForPress() string { +func (self *BranchesController) getDisabledReasonForPress() *types.DisabledReason { currentBranch := self.c.Helpers().Refs.GetCheckedOutRef() if currentBranch != nil { op := self.c.State().GetItemOperation(currentBranch) if op == types.ItemOperationFastForwarding || op == types.ItemOperationPulling { - return self.c.Tr.CantCheckoutBranchWhilePulling + return &types.DisabledReason{Text: self.c.Tr.CantCheckoutBranchWhilePulling} } } - return "" + return nil } func (self *BranchesController) worktreeForBranch(branch *models.Branch) (*models.Worktree, bool) { @@ -525,7 +525,7 @@ func (self *BranchesController) delete(branch *models.Branch) error { }, } if checkedOutBranch.Name == branch.Name { - localDeleteItem.DisabledReason = self.c.Tr.CantDeleteCheckOutBranch + localDeleteItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CantDeleteCheckOutBranch} } remoteDeleteItem := &types.MenuItem{ @@ -536,7 +536,7 @@ func (self *BranchesController) delete(branch *models.Branch) error { }, } if !branch.IsTrackingRemote() || branch.UpstreamGone { - remoteDeleteItem.DisabledReason = self.c.Tr.UpstreamNotSetError + remoteDeleteItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError} } menuTitle := utils.ResolvePlaceholderString( @@ -562,14 +562,14 @@ func (self *BranchesController) rebase() error { return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName) } -func (self *BranchesController) getDisabledReasonForRebase() string { +func (self *BranchesController) getDisabledReasonForRebase() *types.DisabledReason { selectedBranchName := self.context().GetSelected().Name checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name if selectedBranchName == checkedOutBranch { - return self.c.Tr.CantRebaseOntoSelf + return &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf} } - return "" + return nil } func (self *BranchesController) fastForward(branch *models.Branch) error { diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index d60773ec14d..7776da5b33c 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -848,15 +848,15 @@ func (self *FilesController) openCopyMenu() error { } if node == nil { - copyNameItem.DisabledReason = self.c.Tr.NoContentToCopyError - copyPathItem.DisabledReason = self.c.Tr.NoContentToCopyError - copyFileDiffItem.DisabledReason = self.c.Tr.NoContentToCopyError + copyNameItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError} + copyPathItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError} + copyFileDiffItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError} } if node != nil && !node.GetHasStagedOrTrackedChanges() { - copyFileDiffItem.DisabledReason = self.c.Tr.NoContentToCopyError + copyFileDiffItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError} } if !self.anyStagedOrTrackedFile() { - copyAllDiff.DisabledReason = self.c.Tr.NoContentToCopyError + copyAllDiff.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError} } return self.c.Menu(types.CreateMenuOptions{ diff --git a/pkg/gui/controllers/helpers/app_status_helper.go b/pkg/gui/controllers/helpers/app_status_helper.go index 8e4741bd53b..fd6bc247fa3 100644 --- a/pkg/gui/controllers/helpers/app_status_helper.go +++ b/pkg/gui/controllers/helpers/app_status_helper.go @@ -5,6 +5,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/status" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -23,7 +24,7 @@ func NewAppStatusHelper(c *HelperCommon, statusMgr func() *status.StatusManager, } } -func (self *AppStatusHelper) Toast(message string) { +func (self *AppStatusHelper) Toast(message string, kind types.ToastKind) { if self.c.RunningIntegrationTest() { // Don't bother showing toasts in integration tests. You can't check for // them anyway, and they would only slow down the test unnecessarily by @@ -31,7 +32,7 @@ func (self *AppStatusHelper) Toast(message string) { return } - self.statusMgr().AddToastStatus(message) + self.statusMgr().AddToastStatus(message, kind) self.renderAppStatus() } @@ -87,7 +88,8 @@ func (self *AppStatusHelper) HasStatus() bool { } func (self *AppStatusHelper) GetStatusString() string { - return self.statusMgr().GetStatusString() + appStatus, _ := self.statusMgr().GetStatusString() + return appStatus } func (self *AppStatusHelper) renderAppStatus() { @@ -95,7 +97,8 @@ func (self *AppStatusHelper) renderAppStatus() { ticker := time.NewTicker(time.Millisecond * utils.LoaderAnimationInterval) defer ticker.Stop() for range ticker.C { - appStatus := self.statusMgr().GetStatusString() + appStatus, color := self.statusMgr().GetStatusString() + self.c.Views().AppStatus.FgColor = color self.c.OnUIThread(func() error { self.c.SetViewContent(self.c.Views().AppStatus, appStatus) return nil @@ -127,7 +130,8 @@ func (self *AppStatusHelper) renderAppStatusSync(stop chan struct{}) { for { select { case <-ticker.C: - appStatus := self.statusMgr().GetStatusString() + appStatus, color := self.statusMgr().GetStatusString() + self.c.Views().AppStatus.FgColor = color self.c.SetViewContent(self.c.Views().AppStatus, appStatus) // Redraw all views of the bottom line: bottomLineViews := []*gocui.View{ diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index 62beaacda24..8a61a86e14e 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -378,11 +378,11 @@ func (self *ConfirmationHelper) IsPopupPanelFocused() bool { func (self *ConfirmationHelper) TooltipForMenuItem(menuItem *types.MenuItem) string { tooltip := menuItem.Tooltip - if menuItem.DisabledReason != "" { + if menuItem.DisabledReason != nil { if tooltip != "" { tooltip += "\n\n" } - tooltip += style.FgRed.Sprintf(self.c.Tr.DisabledMenuItemPrefix) + menuItem.DisabledReason + tooltip += style.FgRed.Sprintf(self.c.Tr.DisabledMenuItemPrefix) + menuItem.DisabledReason.Text } return tooltip } diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index d3b92bc935b..97151f4feb6 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -261,9 +261,9 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error { }) } -func (self *LocalCommitsController) getDisabledReasonForSquashDown(commit *models.Commit) string { +func (self *LocalCommitsController) getDisabledReasonForSquashDown(commit *models.Commit) *types.DisabledReason { if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 { - return self.c.Tr.CannotSquashOrFixupFirstCommit + return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit} } return self.rebaseCommandEnabled(todo.Squash, commit) @@ -290,9 +290,9 @@ func (self *LocalCommitsController) fixup(commit *models.Commit) error { }) } -func (self *LocalCommitsController) getDisabledReasonForFixup(commit *models.Commit) string { +func (self *LocalCommitsController) getDisabledReasonForFixup(commit *models.Commit) *types.DisabledReason { if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 { - return self.c.Tr.CannotSquashOrFixupFirstCommit + return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit} } return self.rebaseCommandEnabled(todo.Squash, commit) @@ -528,9 +528,9 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoComma }) } -func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand, commit *models.Commit) string { +func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand, commit *models.Commit) *types.DisabledReason { if commit.Action == models.ActionConflict { - return self.c.Tr.ChangingThisActionIsNotAllowed + return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed} } if !commit.IsTODO() { @@ -538,11 +538,11 @@ func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand // If we are in a rebase, the only action that is allowed for // non-todo commits is rewording the current head commit if !(action == todo.Reword && self.isHeadCommit()) { - return self.c.Tr.AlreadyRebasing + return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} } } - return "" + return nil } // for now we do not support setting 'reword' because it requires an editor @@ -550,14 +550,14 @@ func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand // our input or we set a lazygit client as the EDITOR env variable and have it // request us to edit the commit message when prompted. if action == todo.Reword { - return self.c.Tr.RewordNotSupported + return &types.DisabledReason{Text: self.c.Tr.RewordNotSupported} } if allowed := isChangeOfRebaseTodoAllowed(action); !allowed { - return self.c.Tr.ChangingThisActionIsNotAllowed + return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed} } - return "" + return nil } func (self *LocalCommitsController) moveDown(commit *models.Commit) error { @@ -687,12 +687,12 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error { }) } -func (self *LocalCommitsController) getDisabledReasonForAmendTo(commit *models.Commit) string { +func (self *LocalCommitsController) getDisabledReasonForAmendTo(commit *models.Commit) *types.DisabledReason { if !self.isHeadCommit() && self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { - return self.c.Tr.AlreadyRebasing + return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} } - return "" + return nil } func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error { @@ -870,30 +870,30 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co }) } -func (self *LocalCommitsController) getDisabledReasonForSquashAllAboveFixupCommits(commit *models.Commit) string { +func (self *LocalCommitsController) getDisabledReasonForSquashAllAboveFixupCommits(commit *models.Commit) *types.DisabledReason { if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { - return self.c.Tr.AlreadyRebasing + return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} } - return "" + return nil } // For getting disabled reason -func (self *LocalCommitsController) notMidRebase() string { +func (self *LocalCommitsController) notMidRebase() *types.DisabledReason { if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { - return self.c.Tr.AlreadyRebasing + return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} } - return "" + return nil } // For getting disabled reason -func (self *LocalCommitsController) canFindCommitForQuickStart() string { +func (self *LocalCommitsController) canFindCommitForQuickStart() *types.DisabledReason { if _, err := self.findCommitForQuickStartInteractiveRebase(); err != nil { - return err.Error() + return &types.DisabledReason{Text: err.Error(), ShowErrorInPanel: true} } - return "" + return nil } func (self *LocalCommitsController) createTag(commit *models.Commit) error { @@ -1028,23 +1028,23 @@ func (self *LocalCommitsController) checkSelected(callback func(*models.Commit) } } -func (self *LocalCommitsController) callGetDisabledReasonFuncWithSelectedCommit(callback func(*models.Commit) string) func() string { - return func() string { +func (self *LocalCommitsController) callGetDisabledReasonFuncWithSelectedCommit(callback func(*models.Commit) *types.DisabledReason) func() *types.DisabledReason { + return func() *types.DisabledReason { commit := self.context().GetSelected() if commit == nil { - return self.c.Tr.NoCommitSelected + return &types.DisabledReason{Text: self.c.Tr.NoCommitSelected} } return callback(commit) } } -func (self *LocalCommitsController) disabledIfNoSelectedCommit() func() string { - return self.callGetDisabledReasonFuncWithSelectedCommit(func(*models.Commit) string { return "" }) +func (self *LocalCommitsController) disabledIfNoSelectedCommit() func() *types.DisabledReason { + return self.callGetDisabledReasonFuncWithSelectedCommit(func(*models.Commit) *types.DisabledReason { return nil }) } -func (self *LocalCommitsController) getDisabledReasonForRebaseCommandWithSelectedCommit(action todo.TodoCommand) func() string { - return self.callGetDisabledReasonFuncWithSelectedCommit(func(commit *models.Commit) string { +func (self *LocalCommitsController) getDisabledReasonForRebaseCommandWithSelectedCommit(action todo.TodoCommand) func() *types.DisabledReason { + return self.callGetDisabledReasonFuncWithSelectedCommit(func(commit *models.Commit) *types.DisabledReason { return self.rebaseCommandEnabled(action, commit) }) } @@ -1077,12 +1077,12 @@ func (self *LocalCommitsController) paste() error { return self.c.Helpers().CherryPick.Paste() } -func (self *LocalCommitsController) getDisabledReasonForPaste() string { +func (self *LocalCommitsController) getDisabledReasonForPaste() *types.DisabledReason { if !self.c.Helpers().CherryPick.CanPaste() { - return self.c.Tr.NoCopiedCommits + return &types.DisabledReason{Text: self.c.Tr.NoCopiedCommits} } - return "" + return nil } func (self *LocalCommitsController) markAsBaseCommit(commit *models.Commit) error { @@ -1100,15 +1100,15 @@ func (self *LocalCommitsController) isHeadCommit() bool { } // Convenience function for composing multiple disabled reason functions -func (self *LocalCommitsController) require(callbacks ...func() string) func() string { - return func() string { +func (self *LocalCommitsController) require(callbacks ...func() *types.DisabledReason) func() *types.DisabledReason { + return func() *types.DisabledReason { for _, callback := range callbacks { - if disabledReason := callback(); disabledReason != "" { + if disabledReason := callback(); disabledReason != nil { return disabledReason } } - return "" + return nil } } diff --git a/pkg/gui/controllers/options_menu_action.go b/pkg/gui/controllers/options_menu_action.go index 27a2915b8c1..1100cf87658 100644 --- a/pkg/gui/controllers/options_menu_action.go +++ b/pkg/gui/controllers/options_menu_action.go @@ -25,7 +25,7 @@ func (self *OptionsMenuAction) Call() error { appendBindings := func(bindings []*types.Binding, section *types.MenuSection) { menuItems = append(menuItems, lo.Map(bindings, func(binding *types.Binding, _ int) *types.MenuItem { - disabledReason := "" + var disabledReason *types.DisabledReason if binding.GetDisabledReason != nil { disabledReason = binding.GetDisabledReason() } diff --git a/pkg/gui/controllers/sync_controller.go b/pkg/gui/controllers/sync_controller.go index 4e3369e0a2e..ada4997d224 100644 --- a/pkg/gui/controllers/sync_controller.go +++ b/pkg/gui/controllers/sync_controller.go @@ -59,16 +59,16 @@ func (self *SyncController) HandlePull() error { return self.branchCheckedOut(self.pull)() } -func (self *SyncController) getDisabledReasonForPushOrPull() string { +func (self *SyncController) getDisabledReasonForPushOrPull() *types.DisabledReason { currentBranch := self.c.Helpers().Refs.GetCheckedOutRef() if currentBranch != nil { op := self.c.State().GetItemOperation(currentBranch) if op != types.ItemOperationNone { - return self.c.Tr.CantPullOrPushSameBranchTwice + return &types.DisabledReason{Text: self.c.Tr.CantPullOrPushSameBranchTwice} } } - return "" + return nil } func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func() error { diff --git a/pkg/gui/dummies.go b/pkg/gui/dummies.go index 90bd094d862..144df1019d4 100644 --- a/pkg/gui/dummies.go +++ b/pkg/gui/dummies.go @@ -17,6 +17,6 @@ func NewDummyUpdater() *updates.Updater { func NewDummyGui() *Gui { newAppConfig := config.NewDummyAppConfig() - dummyGui, _ := NewGui(utils.NewDummyCommon(), newAppConfig, &git_commands.GitVersion{}, NewDummyUpdater(), false, "") + dummyGui, _ := NewGui(utils.NewDummyCommon(), newAppConfig, &git_commands.GitVersion{}, NewDummyUpdater(), false, "", nil) return dummyGui } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index dd685dc7260..6acdc804ca4 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -467,6 +467,7 @@ func NewGui( updater *updates.Updater, showRecentRepos bool, initialDir string, + test integrationTypes.IntegrationTest, ) (*Gui, error) { gui := &Gui{ Common: cmn, @@ -516,7 +517,7 @@ func NewGui( func(message string, f func() error) { gui.helpers.AppStatus.WithWaitingStatusSync(message, f) }, - func(message string) { gui.helpers.AppStatus.Toast(message) }, + func(message string, kind types.ToastKind) { gui.helpers.AppStatus.Toast(message, kind) }, func() string { return gui.Views.Confirmation.TextArea.GetContent() }, func() bool { return gui.c.InDemo() }, ) diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index c36e68e9396..e7fa097d361 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -20,11 +20,14 @@ import ( type GuiDriver struct { gui *Gui isIdleChan chan struct{} + toastChan chan string } var _ integrationTypes.GuiDriver = &GuiDriver{} func (self *GuiDriver) PressKey(keyStr string) { + self.CheckAllToastsAcknowledged() + key := keybindings.GetKey(keyStr) var r rune @@ -46,6 +49,8 @@ func (self *GuiDriver) PressKey(keyStr string) { } func (self *GuiDriver) Click(x, y int) { + self.CheckAllToastsAcknowledged() + self.gui.g.ReplayedEvents.MouseEvents <- gocui.NewTcellMouseEventWrapper( tcell.NewEventMouse(x, y, tcell.ButtonPrimary, 0), 0, @@ -58,6 +63,12 @@ func (self *GuiDriver) waitTillIdle() { <-self.isIdleChan } +func (self *GuiDriver) CheckAllToastsAcknowledged() { + if t := self.NextToast(); t != nil { + self.Fail("Toast not acknowledged: " + *t) + } +} + func (self *GuiDriver) Keys() config.KeybindingConfig { return self.gui.Config.GetUserConfig().Keybinding } @@ -133,3 +144,12 @@ func (self *GuiDriver) SetCaptionPrefix(prefix string) { self.gui.setCaptionPrefix(prefix) self.waitTillIdle() } + +func (self *GuiDriver) NextToast() *string { + select { + case t := <-self.toastChan: + return &t + default: + return nil + } +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index afb12ce8570..26ce8ec91f9 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -411,12 +411,17 @@ func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error { } func (gui *Gui) callKeybindingHandler(binding *types.Binding) error { - disabledReason := "" + var disabledReason *types.DisabledReason if binding.GetDisabledReason != nil { disabledReason = binding.GetDisabledReason() } - if disabledReason != "" { - return gui.c.ErrorMsg(disabledReason) + if disabledReason != nil { + if disabledReason.ShowErrorInPanel { + return gui.c.ErrorMsg(disabledReason.Text) + } + + gui.c.ErrorToast(gui.Tr.DisabledMenuItemPrefix + disabledReason.Text) + return nil } return binding.Handler() } diff --git a/pkg/gui/popup/popup_handler.go b/pkg/gui/popup/popup_handler.go index 33c01e0cca3..1eb81e80066 100644 --- a/pkg/gui/popup/popup_handler.go +++ b/pkg/gui/popup/popup_handler.go @@ -22,7 +22,7 @@ type PopupHandler struct { createMenuFn func(types.CreateMenuOptions) error withWaitingStatusFn func(message string, f func(gocui.Task) error) withWaitingStatusSyncFn func(message string, f func() error) - toastFn func(message string) + toastFn func(message string, kind types.ToastKind) getPromptInputFn func() string inDemo func() bool } @@ -38,7 +38,7 @@ func NewPopupHandler( createMenuFn func(types.CreateMenuOptions) error, withWaitingStatusFn func(message string, f func(gocui.Task) error), withWaitingStatusSyncFn func(message string, f func() error), - toastFn func(message string), + toastFn func(message string, kind types.ToastKind), getPromptInputFn func() string, inDemo func() bool, ) *PopupHandler { @@ -63,7 +63,15 @@ func (self *PopupHandler) Menu(opts types.CreateMenuOptions) error { } func (self *PopupHandler) Toast(message string) { - self.toastFn(message) + self.toastFn(message, types.ToastKindStatus) +} + +func (self *PopupHandler) ErrorToast(message string) { + self.toastFn(message, types.ToastKindError) +} + +func (self *PopupHandler) SetToastFunc(f func(string, types.ToastKind)) { + self.toastFn = f } func (self *PopupHandler) WithWaitingStatus(message string, f func(gocui.Task) error) error { diff --git a/pkg/gui/status/status_manager.go b/pkg/gui/status/status_manager.go index da4f5f14cba..eb5b01d4a39 100644 --- a/pkg/gui/status/status_manager.go +++ b/pkg/gui/status/status_manager.go @@ -3,6 +3,8 @@ package status import ( "time" + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" "github.com/sasha-s/go-deadlock" @@ -26,7 +28,7 @@ type WaitingStatusHandle struct { } func (self *WaitingStatusHandle) Show() { - self.id = self.statusManager.addStatus(self.message, "waiting") + self.id = self.statusManager.addStatus(self.message, "waiting", types.ToastKindStatus) self.renderFunc() } @@ -37,6 +39,7 @@ func (self *WaitingStatusHandle) Hide() { type appStatus struct { message string statusType string + color gocui.Attribute id int } @@ -53,11 +56,12 @@ func (self *StatusManager) WithWaitingStatus(message string, renderFunc func(), handle.Hide() } -func (self *StatusManager) AddToastStatus(message string) int { - id := self.addStatus(message, "toast") +func (self *StatusManager) AddToastStatus(message string, kind types.ToastKind) int { + id := self.addStatus(message, "toast", kind) go func() { - time.Sleep(time.Second * 2) + delay := lo.Ternary(kind == types.ToastKindError, time.Second*4, time.Second*2) + time.Sleep(delay) self.removeStatus(id) }() @@ -65,31 +69,37 @@ func (self *StatusManager) AddToastStatus(message string) int { return id } -func (self *StatusManager) GetStatusString() string { +func (self *StatusManager) GetStatusString() (string, gocui.Attribute) { if len(self.statuses) == 0 { - return "" + return "", gocui.ColorDefault } topStatus := self.statuses[0] if topStatus.statusType == "waiting" { - return topStatus.message + " " + utils.Loader(time.Now()) + return topStatus.message + " " + utils.Loader(time.Now()), topStatus.color } - return topStatus.message + return topStatus.message, topStatus.color } func (self *StatusManager) HasStatus() bool { return len(self.statuses) > 0 } -func (self *StatusManager) addStatus(message string, statusType string) int { +func (self *StatusManager) addStatus(message string, statusType string, kind types.ToastKind) int { self.mutex.Lock() defer self.mutex.Unlock() self.nextId++ id := self.nextId + color := gocui.ColorCyan + if kind == types.ToastKindError { + color = gocui.ColorRed + } + newStatus := appStatus{ message: message, statusType: statusType, + color: color, id: id, } self.statuses = append([]appStatus{newStatus}, self.statuses...) diff --git a/pkg/gui/test_mode.go b/pkg/gui/test_mode.go index 151cb72465a..65009fb78b7 100644 --- a/pkg/gui/test_mode.go +++ b/pkg/gui/test_mode.go @@ -6,6 +6,8 @@ import ( "time" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -32,7 +34,11 @@ func (gui *Gui) handleTestMode() { go func() { waitUntilIdle() - test.Run(&GuiDriver{gui: gui, isIdleChan: isIdleChan}) + toastChan := make(chan string, 100) + gui.PopupHandler.(*popup.PopupHandler).SetToastFunc( + func(message string, kind types.ToastKind) { toastChan <- message }) + + test.Run(&GuiDriver{gui: gui, isIdleChan: isIdleChan, toastChan: toastChan}) gui.g.Update(func(*gocui.Gui) error { return gocui.ErrQuit diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 366b0fcf0b1..9053e43f9b1 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -144,9 +144,18 @@ type IPopupHandler interface { WithWaitingStatusSync(message string, f func() error) error Menu(opts CreateMenuOptions) error Toast(message string) + ErrorToast(message string) + SetToastFunc(func(string, ToastKind)) GetPromptInput() string } +type ToastKind int + +const ( + ToastKindStatus ToastKind = iota + ToastKindError +) + type CreateMenuOptions struct { Title string Items []*MenuItem @@ -192,6 +201,16 @@ type MenuSection struct { Column int // The column that this section title should be aligned with } +type DisabledReason struct { + Text string + + // When trying to invoke a disabled key binding or menu item, we normally + // show the disabled reason as a toast; setting this to true shows it as an + // error panel instead. This is useful if the text is very long, or if it is + // important enough to show it more prominently, or both. + ShowErrorInPanel bool +} + type MenuItem struct { Label string @@ -210,9 +229,9 @@ type MenuItem struct { // The tooltip will be displayed upon highlighting the menu item Tooltip string - // If non-empty, show this in a tooltip, style the menu item as disabled, + // If non-nil, show this in a tooltip, style the menu item as disabled, // and refuse to invoke the command - DisabledReason string + DisabledReason *DisabledReason // Can be used to group menu items into sections with headers. MenuItems // with the same Section should be contiguous, and will automatically get a diff --git a/pkg/gui/types/keybindings.go b/pkg/gui/types/keybindings.go index 4c3d02c6fdc..bb125d4e59b 100644 --- a/pkg/gui/types/keybindings.go +++ b/pkg/gui/types/keybindings.go @@ -31,7 +31,7 @@ type Binding struct { // disabled and we show the given text in an error message when trying to // invoke it. When left nil, the command is always enabled. Note that this // function must not do expensive calls. - GetDisabledReason func() string + GetDisabledReason func() *DisabledReason } // A guard is a decorator which checks something before executing a handler diff --git a/pkg/integration/components/menu_driver.go b/pkg/integration/components/menu_driver.go index 7ab42a3e1c8..2f93df82d49 100644 --- a/pkg/integration/components/menu_driver.go +++ b/pkg/integration/components/menu_driver.go @@ -18,10 +18,12 @@ func (self *MenuDriver) Title(expected *TextMatcher) *MenuDriver { return self } -func (self *MenuDriver) Confirm() { +func (self *MenuDriver) Confirm() *MenuDriver { self.checkNecessaryChecksCompleted() self.getViewDriver().PressEnter() + + return self } func (self *MenuDriver) Cancel() { @@ -72,6 +74,11 @@ func (self *MenuDriver) Tooltip(option *TextMatcher) *MenuDriver { return self } +func (self *MenuDriver) Tap(f func()) *MenuDriver { + self.getViewDriver().Tap(f) + return self +} + func (self *MenuDriver) checkNecessaryChecksCompleted() { if !self.hasCheckedTitle { self.t.Fail("You must check the title of a menu popup by calling Title() before calling Confirm()/Cancel().") diff --git a/pkg/integration/components/test.go b/pkg/integration/components/test.go index c837d8be800..203436ae755 100644 --- a/pkg/integration/components/test.go +++ b/pkg/integration/components/test.go @@ -194,6 +194,8 @@ func (self *IntegrationTest) Run(gui integrationTypes.GuiDriver) { self.run(testDriver, keys) + gui.CheckAllToastsAcknowledged() + if InputDelay() > 0 { // Clear whatever caption there was so it doesn't linger testDriver.SetCaption("") diff --git a/pkg/integration/components/test_driver.go b/pkg/integration/components/test_driver.go index a862dce06df..d266dfb7351 100644 --- a/pkg/integration/components/test_driver.go +++ b/pkg/integration/components/test_driver.go @@ -102,8 +102,19 @@ func (self *TestDriver) ExpectPopup() *Popup { return &Popup{t: self} } -func (self *TestDriver) ExpectToast(matcher *TextMatcher) { - self.Views().AppStatus().Content(matcher) +func (self *TestDriver) ExpectToast(matcher *TextMatcher) *TestDriver { + t := self.gui.NextToast() + if t == nil { + self.gui.Fail("Expected toast, but didn't get one") + } else { + self.matchString(matcher, "Unexpected toast message", + func() string { + return *t + }, + ) + } + + return self } func (self *TestDriver) ExpectClipboard(matcher *TextMatcher) { diff --git a/pkg/integration/components/test_test.go b/pkg/integration/components/test_test.go index 99e52f5e987..047ee507c68 100644 --- a/pkg/integration/components/test_test.go +++ b/pkg/integration/components/test_test.go @@ -78,6 +78,12 @@ func (self *fakeGuiDriver) SetCaption(string) { func (self *fakeGuiDriver) SetCaptionPrefix(string) { } +func (self *fakeGuiDriver) NextToast() *string { + return nil +} + +func (self *fakeGuiDriver) CheckAllToastsAcknowledged() {} + func TestManualFailure(t *testing.T) { test := NewIntegrationTest(NewIntegrationTestArgs{ Description: unitTestDescription, diff --git a/pkg/integration/tests/branch/delete.go b/pkg/integration/tests/branch/delete.go index cdda78ec6a1..0b6adfac4d0 100644 --- a/pkg/integration/tests/branch/delete.go +++ b/pkg/integration/tests/branch/delete.go @@ -37,12 +37,11 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{ Tooltip(Contains("You cannot delete the checked out branch!")). Title(Equals("Delete branch 'branch-three'?")). Select(Contains("Delete local branch")). - Confirm() - t.ExpectPopup(). - Alert(). - Title(Equals("Error")). - Content(Contains("You cannot delete the checked out branch!")). - Confirm() + Confirm(). + Tap(func() { + t.ExpectToast(Contains("You cannot delete the checked out branch!")) + }). + Cancel() }). SelectNextItem(). Press(keys.Universal.Remove). diff --git a/pkg/integration/tests/branch/rebase_to_upstream.go b/pkg/integration/tests/branch/rebase_to_upstream.go index 1fc51350d0d..5c9e0f388cf 100644 --- a/pkg/integration/tests/branch/rebase_to_upstream.go +++ b/pkg/integration/tests/branch/rebase_to_upstream.go @@ -48,11 +48,11 @@ var RebaseToUpstream = NewIntegrationTest(NewIntegrationTestArgs{ Title(Equals("Upstream options")). Select(Contains("Rebase checked-out branch onto upstream of selected branch")). Tooltip(Contains("Disabled: The selected branch has no upstream (or the upstream is not stored locally)")). - Confirm() - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Equals("The selected branch has no upstream (or the upstream is not stored locally)")). - Confirm() + Confirm(). + Tap(func() { + t.ExpectToast(Equals("Disabled: The selected branch has no upstream (or the upstream is not stored locally)")) + }). + Cancel() }). SelectNextItem(). Lines( diff --git a/pkg/integration/tests/branch/reset_to_upstream.go b/pkg/integration/tests/branch/reset_to_upstream.go index 1eecd689b0c..c933787e492 100644 --- a/pkg/integration/tests/branch/reset_to_upstream.go +++ b/pkg/integration/tests/branch/reset_to_upstream.go @@ -42,11 +42,11 @@ var ResetToUpstream = NewIntegrationTest(NewIntegrationTestArgs{ Title(Equals("Upstream options")). Select(Contains("Reset checked-out branch onto upstream of selected branch")). Tooltip(Contains("Disabled: The selected branch has no upstream (or the upstream is not stored locally)")). - Confirm() - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Equals("The selected branch has no upstream (or the upstream is not stored locally)")). - Confirm() + Confirm(). + Tap(func() { + t.ExpectToast(Equals("Disabled: The selected branch has no upstream (or the upstream is not stored locally)")) + }). + Cancel() }). SelectNextItem(). Lines( diff --git a/pkg/integration/tests/file/copy_menu.go b/pkg/integration/tests/file/copy_menu.go index f00425c967f..a1af13d7b40 100644 --- a/pkg/integration/tests/file/copy_menu.go +++ b/pkg/integration/tests/file/copy_menu.go @@ -30,12 +30,11 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{ Title(Equals("Copy to clipboard")). Select(Contains("File name")). Tooltip(Equals("Disabled: Nothing to copy")). - Confirm() - - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Equals("Nothing to copy")). - Confirm() + Confirm(). + Tap(func() { + t.ExpectToast(Equals("Disabled: Nothing to copy")) + }). + Cancel() }) t.Shell(). @@ -56,12 +55,11 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{ Title(Equals("Copy to clipboard")). Select(Contains("Diff of selected file")). Tooltip(Contains("Disabled: Nothing to copy")). - Confirm() - - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Equals("Nothing to copy")). - Confirm() + Confirm(). + Tap(func() { + t.ExpectToast(Equals("Disabled: Nothing to copy")) + }). + Cancel() }). Press(keys.Files.CopyFileInfoToClipboard). Tap(func() { @@ -69,12 +67,11 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{ Title(Equals("Copy to clipboard")). Select(Contains("Diff of all files")). Tooltip(Contains("Disabled: Nothing to copy")). - Confirm() - - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Equals("Nothing to copy")). - Confirm() + Confirm(). + Tap(func() { + t.ExpectToast(Equals("Disabled: Nothing to copy")) + }). + Cancel() }) t.Shell(). @@ -101,6 +98,8 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{ Select(Contains("File name")). Confirm() + t.ExpectToast(Equals("File name copied to clipboard")) + expectClipboard(t, Contains("unstaged_file")) }) @@ -113,6 +112,8 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{ Select(Contains("Path")). Confirm() + t.ExpectToast(Equals("File path copied to clipboard")) + expectClipboard(t, Contains("dir/1-unstaged_file")) }) @@ -126,6 +127,8 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{ Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")). Confirm() + t.ExpectToast(Equals("File diff copied to clipboard")) + expectClipboard(t, Contains("+unstaged content (new)")) }) @@ -145,6 +148,8 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{ Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")). Confirm() + t.ExpectToast(Equals("File diff copied to clipboard")) + expectClipboard(t, Contains("+staged content (new)")) }) @@ -158,6 +163,8 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{ Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")). Confirm() + t.ExpectToast(Equals("All files diff copied to clipboard")) + expectClipboard(t, Contains("+staged content (new)")) }) @@ -179,6 +186,8 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{ Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")). Confirm() + t.ExpectToast(Equals("All files diff copied to clipboard")) + expectClipboard(t, Contains("+staged content (new)").Contains("+unstaged content (new)")) }) }, diff --git a/pkg/integration/tests/interactive_rebase/amend_non_head_commit_during_rebase.go b/pkg/integration/tests/interactive_rebase/amend_non_head_commit_during_rebase.go index e9c421297c6..a0d0f066e50 100644 --- a/pkg/integration/tests/interactive_rebase/amend_non_head_commit_during_rebase.go +++ b/pkg/integration/tests/interactive_rebase/amend_non_head_commit_during_rebase.go @@ -34,10 +34,7 @@ var AmendNonHeadCommitDuringRebase = NewIntegrationTest(NewIntegrationTestArgs{ NavigateToLine(Contains(commit)). Press(keys.Commits.AmendToCommit) - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Contains("Can't perform this action during a rebase")). - Confirm() + t.ExpectToast(Contains("Can't perform this action during a rebase")) } }, }) diff --git a/pkg/integration/tests/interactive_rebase/edit_non_todo_commit_during_rebase.go b/pkg/integration/tests/interactive_rebase/edit_non_todo_commit_during_rebase.go index 57f2192275f..78cb875acb9 100644 --- a/pkg/integration/tests/interactive_rebase/edit_non_todo_commit_during_rebase.go +++ b/pkg/integration/tests/interactive_rebase/edit_non_todo_commit_during_rebase.go @@ -29,9 +29,6 @@ var EditNonTodoCommitDuringRebase = NewIntegrationTest(NewIntegrationTestArgs{ NavigateToLine(Contains("commit 01")). Press(keys.Universal.Edit) - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Contains("Can't perform this action during a rebase")). - Confirm() + t.ExpectToast(Contains("Can't perform this action during a rebase")) }, }) diff --git a/pkg/integration/tests/interactive_rebase/edit_the_confl_commit.go b/pkg/integration/tests/interactive_rebase/edit_the_confl_commit.go index 96ec81c740e..85a3df27c4a 100644 --- a/pkg/integration/tests/interactive_rebase/edit_the_confl_commit.go +++ b/pkg/integration/tests/interactive_rebase/edit_the_confl_commit.go @@ -39,9 +39,6 @@ var EditTheConflCommit = NewIntegrationTest(NewIntegrationTestArgs{ NavigateToLine(Contains("<-- YOU ARE HERE --- commit three")). Press(keys.Commits.RenameCommit) - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Contains("Changing this kind of rebase todo entry is not allowed")). - Confirm() + t.ExpectToast(Contains("Changing this kind of rebase todo entry is not allowed")) }, }) diff --git a/pkg/integration/tests/interactive_rebase/fixup_first_commit.go b/pkg/integration/tests/interactive_rebase/fixup_first_commit.go index a45c2f050a9..ff099d76099 100644 --- a/pkg/integration/tests/interactive_rebase/fixup_first_commit.go +++ b/pkg/integration/tests/interactive_rebase/fixup_first_commit.go @@ -24,10 +24,7 @@ var FixupFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{ NavigateToLine(Contains("commit 01")). Press(keys.Commits.MarkCommitAsFixup). Tap(func() { - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Equals("There's no commit below to squash into")). - Confirm() + t.ExpectToast(Equals("Disabled: There's no commit below to squash into")) }). Lines( Contains("commit 02"), diff --git a/pkg/integration/tests/interactive_rebase/quick_start.go b/pkg/integration/tests/interactive_rebase/quick_start.go index 713e00cdb00..d0454d6cf74 100644 --- a/pkg/integration/tests/interactive_rebase/quick_start.go +++ b/pkg/integration/tests/interactive_rebase/quick_start.go @@ -50,13 +50,12 @@ var QuickStart = NewIntegrationTest(NewIntegrationTestArgs{ Contains("initial commit"), ). // Verify we can't quick start from main - Press(keys.Commits.StartInteractiveRebase). - Tap(func() { - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Contains("Cannot start interactive rebase: the HEAD commit is a merge commit or is present on the main branch, so there is no appropriate base commit to start the rebase from. You can start an interactive rebase from a specific commit by selecting the commit and pressing `e`.")). - Confirm() - }) + Press(keys.Commits.StartInteractiveRebase) + + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Equals("Cannot start interactive rebase: the HEAD commit is a merge commit or is present on the main branch, so there is no appropriate base commit to start the rebase from. You can start an interactive rebase from a specific commit by selecting the commit and pressing `e`.")). + Confirm() t.Views().Branches(). Focus(). @@ -80,15 +79,10 @@ var QuickStart = NewIntegrationTest(NewIntegrationTestArgs{ Contains("initial commit"), ). // Try again, verify we fail because we're already rebasing - Press(keys.Commits.StartInteractiveRebase). - Tap(func() { - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Contains("Can't perform this action during a rebase")). - Confirm() - - t.Common().AbortRebase() - }) + Press(keys.Commits.StartInteractiveRebase) + + t.ExpectToast(Equals("Disabled: Can't perform this action during a rebase")) + t.Common().AbortRebase() // Verify if a merge commit is present on the branch we start from there t.Views().Branches(). diff --git a/pkg/integration/tests/interactive_rebase/squash_down_first_commit.go b/pkg/integration/tests/interactive_rebase/squash_down_first_commit.go index 0f58419f992..65d6bfaa7c9 100644 --- a/pkg/integration/tests/interactive_rebase/squash_down_first_commit.go +++ b/pkg/integration/tests/interactive_rebase/squash_down_first_commit.go @@ -24,10 +24,7 @@ var SquashDownFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{ NavigateToLine(Contains("commit 01")). Press(keys.Commits.SquashDown). Tap(func() { - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Equals("There's no commit below to squash into")). - Confirm() + t.ExpectToast(Equals("Disabled: There's no commit below to squash into")) }). Lines( Contains("commit 02"), diff --git a/pkg/integration/tests/misc/copy_to_clipboard.go b/pkg/integration/tests/misc/copy_to_clipboard.go index 48fae813664..6b1d5d1f6d4 100644 --- a/pkg/integration/tests/misc/copy_to_clipboard.go +++ b/pkg/integration/tests/misc/copy_to_clipboard.go @@ -27,6 +27,8 @@ var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{ ). Press(keys.Universal.CopyToClipboard) + t.ExpectToast(Equals("'branch-a' Copied to clipboard")) + t.Views().Files(). Focus() diff --git a/pkg/integration/tests/staging/diff_context_change.go b/pkg/integration/tests/staging/diff_context_change.go index ca0aa0780a2..141f8bcec50 100644 --- a/pkg/integration/tests/staging/diff_context_change.go +++ b/pkg/integration/tests/staging/diff_context_change.go @@ -62,6 +62,9 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{ Contains(` 6a`), ). Press(keys.Universal.IncreaseContextInDiffView). + Tap(func() { + t.ExpectToast(Equals("Changed diff context size to 4")) + }). SelectedLines( Contains(`@@ -1,7 +1,7 @@`), Contains(` 1a`), @@ -74,6 +77,9 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{ Contains(` 7a`), ). Press(keys.Universal.DecreaseContextInDiffView). + Tap(func() { + t.ExpectToast(Equals("Changed diff context size to 3")) + }). SelectedLines( Contains(`@@ -1,6 +1,6 @@`), Contains(` 1a`), @@ -85,6 +91,9 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{ Contains(` 6a`), ). Press(keys.Universal.DecreaseContextInDiffView). + Tap(func() { + t.ExpectToast(Equals("Changed diff context size to 2")) + }). SelectedLines( Contains(`@@ -1,5 +1,5 @@`), Contains(` 1a`), @@ -95,6 +104,9 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{ Contains(` 5a`), ). Press(keys.Universal.DecreaseContextInDiffView). + Tap(func() { + t.ExpectToast(Equals("Changed diff context size to 1")) + }). SelectedLines( Contains(`@@ -2,3 +2,3 @@`), Contains(` 2a`), @@ -116,6 +128,9 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{ Contains(` 4a`), ). Press(keys.Universal.IncreaseContextInDiffView). + Tap(func() { + t.ExpectToast(Equals("Changed diff context size to 2")) + }). SelectedLines( Contains(`@@ -1,5 +1,5 @@`), Contains(` 1a`), diff --git a/pkg/integration/tests/tag/crud_annotated.go b/pkg/integration/tests/tag/crud_annotated.go index 930859c902c..12fa166451a 100644 --- a/pkg/integration/tests/tag/crud_annotated.go +++ b/pkg/integration/tests/tag/crud_annotated.go @@ -65,6 +65,7 @@ var CrudAnnotated = NewIntegrationTest(NewIntegrationTestArgs{ Title(Equals("Delete tag 'new-tag'?")). Content(Equals("Are you sure you want to delete the remote tag 'new-tag' from 'origin'?")). Confirm() + t.ExpectToast(Equals("Remote tag deleted")) }). Lines( MatchesRegexp(`new-tag.*message`).IsSelected(), diff --git a/pkg/integration/tests/tag/crud_lightweight.go b/pkg/integration/tests/tag/crud_lightweight.go index 6aab10dd183..dd6614683a1 100644 --- a/pkg/integration/tests/tag/crud_lightweight.go +++ b/pkg/integration/tests/tag/crud_lightweight.go @@ -70,6 +70,7 @@ var CrudLightweight = NewIntegrationTest(NewIntegrationTestArgs{ Title(Equals("Delete tag 'new-tag'?")). Content(Equals("Are you sure you want to delete the remote tag 'new-tag' from 'origin'?")). Confirm() + t.ExpectToast(Equals("Remote tag deleted")) }). Lines( MatchesRegexp(`new-tag.*initial commit`).IsSelected(), diff --git a/pkg/integration/types/types.go b/pkg/integration/types/types.go index 15a2d514f80..a30aeb055ba 100644 --- a/pkg/integration/types/types.go +++ b/pkg/integration/types/types.go @@ -43,4 +43,7 @@ type GuiDriver interface { View(viewName string) *gocui.View SetCaption(caption string) SetCaptionPrefix(prefix string) + // Pop the next toast that was displayed; returns nil if there was none + NextToast() *string + CheckAllToastsAcknowledged() }