diff --git a/pkg/gui/app_status_manager.go b/pkg/gui/app_status_manager.go index e625fcad2..825bb8801 100644 --- a/pkg/gui/app_status_manager.go +++ b/pkg/gui/app_status_manager.go @@ -106,8 +106,8 @@ func (gui *Gui) renderAppStatus() { }) } -// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing -func (gui *Gui) WithWaitingStatus(message string, f func() error) error { +// withWaitingStatus wraps a function and shows a waiting status while the function is still executing +func (gui *Gui) withWaitingStatus(message string, f func() error) error { go utils.Safe(func() { id := gui.statusManager.addWaitingStatus(message) @@ -119,7 +119,7 @@ func (gui *Gui) WithWaitingStatus(message string, f func() error) error { if err := f(); err != nil { gui.OnUIThread(func() error { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) }) } }) diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index feef98431..dca0dc8f0 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -7,6 +7,8 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -62,7 +64,7 @@ func (gui *Gui) refreshBranches() { branches, err := gui.Git.Loaders.Branches.Load(reflogCommits) if err != nil { - _ = gui.surfaceError(err) + _ = gui.PopupHandler.Error(err) } gui.State.Branches = branches @@ -81,7 +83,7 @@ func (gui *Gui) handleBranchPress() error { return nil } if gui.State.Panels.Branches.SelectedLineIdx == 0 { - return gui.createErrorPanel(gui.Tr.AlreadyCheckedOutBranch) + return gui.PopupHandler.ErrorMsg(gui.Tr.AlreadyCheckedOutBranch) } branch := gui.getSelectedBranch() gui.logAction(gui.Tr.Actions.CheckoutBranch) @@ -111,16 +113,16 @@ func (gui *Gui) handleCopyPullRequestURLPress() error { branchExistsOnRemote := gui.Git.Remote.CheckRemoteBranchExists(branch.Name) if !branchExistsOnRemote { - return gui.surfaceError(errors.New(gui.Tr.NoBranchOnRemote)) + return gui.PopupHandler.Error(errors.New(gui.Tr.NoBranchOnRemote)) } url, err := hostingServiceMgr.GetPullRequestURL(branch.Name, "") if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.logAction(gui.Tr.Actions.CopyPullRequestURL) if err := gui.OSCommand.CopyToClipboard(url); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.raiseToast(gui.Tr.PullRequestURLCopiedToClipboard) @@ -129,16 +131,12 @@ func (gui *Gui) handleCopyPullRequestURLPress() error { } func (gui *Gui) handleGitFetch() error { - if err := gui.createLoaderPanel(gui.Tr.FetchWait); err != nil { - return err - } - - go utils.Safe(func() { - err := gui.fetch() - gui.handleCredentialsPopup(err) - _ = gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.PopupHandler.WithLoaderPanel(gui.Tr.FetchWait, func() error { + if err := gui.fetch(); err != nil { + _ = gui.PopupHandler.Error(err) + } + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) }) - return nil } func (gui *Gui) handleForceCheckout() error { @@ -146,15 +144,15 @@ func (gui *Gui) handleForceCheckout() error { message := gui.Tr.SureForceCheckout title := gui.Tr.ForceCheckoutBranch - return gui.ask(askOpts{ - title: title, - prompt: message, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: title, + Prompt: message, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.ForceCheckoutBranch) if err := gui.Git.Branch.Checkout(branch.Name, git_commands.CheckoutOptions{Force: true}); err != nil { - _ = gui.surfaceError(err) + _ = gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) }, }) } @@ -180,7 +178,7 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) gui.State.Panels.Commits.LimitCommits = true } - return gui.WithWaitingStatus(waitingStatus, func() error { + return gui.PopupHandler.WithWaitingStatus(waitingStatus, func() error { if err := gui.Git.Branch.Checkout(ref, cmdOptions); err != nil { // note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option @@ -190,52 +188,52 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") { // offer to autostash changes - return gui.ask(askOpts{ + return gui.PopupHandler.Ask(popup.AskOpts{ - title: gui.Tr.AutoStashTitle, - prompt: gui.Tr.AutoStashPrompt, - handleConfirm: func() error { + Title: gui.Tr.AutoStashTitle, + Prompt: gui.Tr.AutoStashPrompt, + HandleConfirm: func() error { if err := gui.Git.Stash.Save(gui.Tr.StashPrefix + ref); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } if err := gui.Git.Branch.Checkout(ref, cmdOptions); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } onSuccess() if err := gui.Git.Stash.Pop(0); err != nil { - if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.BLOCK_UI}); err != nil { return err } - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.BLOCK_UI}) }, }) } - if err := gui.surfaceError(err); err != nil { + if err := gui.PopupHandler.Error(err); err != nil { return err } } onSuccess() - return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.BLOCK_UI}) }) } func (gui *Gui) handleCheckoutByName() error { - return gui.prompt(promptOpts{ - title: gui.Tr.BranchName + ":", - findSuggestionsFunc: gui.getRefsSuggestionsFunc(), - handleConfirm: func(response string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: gui.Tr.BranchName + ":", + FindSuggestionsFunc: gui.getRefsSuggestionsFunc(), + HandleConfirm: func(response string) error { gui.logAction("Checkout branch") return gui.handleCheckoutRef(response, handleCheckoutRefOptions{ onRefNotFound: func(ref string) error { - return gui.ask(askOpts{ - title: gui.Tr.BranchNotFoundTitle, - prompt: fmt.Sprintf("%s %s%s", gui.Tr.BranchNotFoundPrompt, ref, "?"), - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.BranchNotFoundTitle, + Prompt: fmt.Sprintf("%s %s%s", gui.Tr.BranchNotFoundPrompt, ref, "?"), + HandleConfirm: func() error { return gui.createNewBranchWithName(ref) }, }) @@ -260,11 +258,11 @@ func (gui *Gui) createNewBranchWithName(newBranchName string) error { } if err := gui.Git.Branch.New(newBranchName, branch.Name); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.State.Panels.Branches.SelectedLineIdx = 0 - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) } func (gui *Gui) handleDeleteBranch() error { @@ -278,7 +276,7 @@ func (gui *Gui) deleteBranch(force bool) error { } checkedOutBranch := gui.getCheckedOutBranch() if checkedOutBranch.Name == selectedBranch.Name { - return gui.createErrorPanel(gui.Tr.CantDeleteCheckOutBranch) + return gui.PopupHandler.ErrorMsg(gui.Tr.CantDeleteCheckOutBranch) } return gui.deleteNamedBranch(selectedBranch, force) } @@ -298,19 +296,19 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err }, ) - return gui.ask(askOpts{ - title: title, - prompt: message, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: title, + Prompt: message, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.DeleteBranch) if err := gui.Git.Branch.Delete(selectedBranch.Name, force); err != nil { errMessage := err.Error() if !force && strings.Contains(errMessage, "git branch -D ") { return gui.deleteNamedBranch(selectedBranch, true) } - return gui.createErrorPanel(errMessage) + return gui.PopupHandler.ErrorMsg(errMessage) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}}) }, }) } @@ -321,11 +319,11 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error { } if gui.Git.Branch.IsHeadDetached() { - return gui.createErrorPanel("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on") + return gui.PopupHandler.ErrorMsg("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on") } checkedOutBranchName := gui.getCheckedOutBranch().Name if checkedOutBranchName == branchName { - return gui.createErrorPanel(gui.Tr.CantMergeBranchIntoItself) + return gui.PopupHandler.ErrorMsg(gui.Tr.CantMergeBranchIntoItself) } prompt := utils.ResolvePlaceholderString( gui.Tr.ConfirmMerge, @@ -335,10 +333,10 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error { }, ) - return gui.ask(askOpts{ - title: gui.Tr.MergingTitle, - prompt: prompt, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.MergingTitle, + Prompt: prompt, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.Merge) err := gui.Git.Branch.Merge(branchName, git_commands.MergeOpts{}) return gui.handleGenericMergeCommandResult(err) @@ -367,7 +365,7 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error { checkedOutBranch := gui.getCheckedOutBranch().Name if selectedBranchName == checkedOutBranch { - return gui.createErrorPanel(gui.Tr.CantRebaseOntoSelf) + return gui.PopupHandler.ErrorMsg(gui.Tr.CantRebaseOntoSelf) } prompt := utils.ResolvePlaceholderString( gui.Tr.ConfirmRebase, @@ -377,10 +375,10 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error { }, ) - return gui.ask(askOpts{ - title: gui.Tr.RebasingTitle, - prompt: prompt, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.RebasingTitle, + Prompt: prompt, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.RebaseBranch) err := gui.Git.Rebase.RebaseBranch(selectedBranchName) return gui.handleGenericMergeCommandResult(err) @@ -395,13 +393,13 @@ func (gui *Gui) handleFastForward() error { } if !branch.IsTrackingRemote() { - return gui.createErrorPanel(gui.Tr.FwdNoUpstream) + return gui.PopupHandler.ErrorMsg(gui.Tr.FwdNoUpstream) } if !branch.RemoteBranchStoredLocally() { - return gui.createErrorPanel(gui.Tr.FwdNoLocalUpstream) + return gui.PopupHandler.ErrorMsg(gui.Tr.FwdNoLocalUpstream) } if branch.HasCommitsToPush() { - return gui.createErrorPanel(gui.Tr.FwdCommitsToPush) + return gui.PopupHandler.ErrorMsg(gui.Tr.FwdCommitsToPush) } action := gui.Tr.Actions.FastForwardBranch @@ -413,19 +411,21 @@ func (gui *Gui) handleFastForward() error { "to": branch.Name, }, ) - go utils.Safe(func() { - _ = gui.createLoaderPanel(message) + return gui.PopupHandler.WithLoaderPanel(message, func() error { if gui.State.Panels.Branches.SelectedLineIdx == 0 { _ = gui.pullWithLock(PullFilesOptions{action: action, FastForwardOnly: true}) } else { gui.logAction(action) err := gui.Git.Sync.FastForward(branch.Name, branch.UpstreamRemote, branch.UpstreamBranch) - gui.handleCredentialsPopup(err) - _ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}}) + if err != nil { + _ = gui.PopupHandler.Error(err) + } + _ = gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}}) } + + return nil }) - return nil } func (gui *Gui) handleCreateResetToBranchMenu() error { @@ -444,13 +444,13 @@ func (gui *Gui) handleRenameBranch() error { } promptForNewName := func() error { - return gui.prompt(promptOpts{ - title: gui.Tr.NewBranchNamePrompt + " " + branch.Name + ":", - initialContent: branch.Name, - handleConfirm: func(newBranchName string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: gui.Tr.NewBranchNamePrompt + " " + branch.Name + ":", + InitialContent: branch.Name, + HandleConfirm: func(newBranchName string) error { gui.logAction(gui.Tr.Actions.RenameBranch) if err := gui.Git.Branch.Rename(branch.Name, newBranchName); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } // need to find where the branch is now so that we can re-select it. That means we need to refetch the branches synchronously and then find our branch @@ -478,10 +478,10 @@ func (gui *Gui) handleRenameBranch() error { return promptForNewName() } - return gui.ask(askOpts{ - title: gui.Tr.LcRenameBranch, - prompt: gui.Tr.RenameBranchWarning, - handleConfirm: promptForNewName, + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.LcRenameBranch, + Prompt: gui.Tr.RenameBranchWarning, + HandleConfirm: promptForNewName, }) } @@ -513,10 +513,10 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error { prefilledName = strings.SplitAfterN(item.ID(), "/", 2)[1] } - return gui.prompt(promptOpts{ - title: message, - initialContent: prefilledName, - handleConfirm: func(response string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: message, + InitialContent: prefilledName, + HandleConfirm: func(response string) error { gui.logAction(gui.Tr.Actions.CreateBranch) if err := gui.Git.Branch.New(sanitizedBranchName(response), item.ID()); err != nil { return err @@ -536,7 +536,7 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error { gui.State.Panels.Branches.SelectedLineIdx = 0 - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) }, }) } diff --git a/pkg/gui/cherry_picking.go b/pkg/gui/cherry_picking.go index b4b9439cd..225fc3811 100644 --- a/pkg/gui/cherry_picking.go +++ b/pkg/gui/cherry_picking.go @@ -1,6 +1,9 @@ package gui -import "github.com/jesseduffield/lazygit/pkg/commands/models" +import ( + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/popup" +) // you can only copy from one context at a time, because the order and position of commits matter @@ -143,11 +146,11 @@ func (gui *Gui) HandlePasteCommits() error { return err } - return gui.ask(askOpts{ - title: gui.Tr.CherryPick, - prompt: gui.Tr.SureCherryPick, - handleConfirm: func() error { - return gui.WithWaitingStatus(gui.Tr.CherryPickingStatus, func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.CherryPick, + Prompt: gui.Tr.SureCherryPick, + HandleConfirm: func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.CherryPickingStatus, func() error { gui.logAction(gui.Tr.Actions.CherryPick) err := gui.Git.Rebase.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits) return gui.handleGenericMergeCommandResult(err) diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go index 61f3b72b8..1941802c2 100644 --- a/pkg/gui/commit_files_panel.go +++ b/pkg/gui/commit_files_panel.go @@ -4,6 +4,8 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/gui/filetree" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) func (gui *Gui) getSelectedCommitFileNode() *filetree.CommitFileNode { @@ -65,10 +67,10 @@ func (gui *Gui) handleCheckoutCommitFile() error { gui.logAction(gui.Tr.Actions.CheckoutFile) if err := gui.Git.WorkingTree.CheckoutFile(gui.State.CommitFileTreeViewModel.GetParent(), node.GetPath()); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) } func (gui *Gui) handleDiscardOldFileChange() error { @@ -78,11 +80,11 @@ func (gui *Gui) handleDiscardOldFileChange() error { fileName := gui.getSelectedCommitFileName() - return gui.ask(askOpts{ - title: gui.Tr.DiscardFileChangesTitle, - prompt: gui.Tr.DiscardFileChangesPrompt, - handleConfirm: func() error { - return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.DiscardFileChangesTitle, + Prompt: gui.Tr.DiscardFileChangesPrompt, + HandleConfirm: func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { gui.logAction(gui.Tr.Actions.DiscardOldFileChange) if err := gui.Git.Rebase.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil { if err := gui.handleGenericMergeCommandResult(err); err != nil { @@ -90,7 +92,7 @@ func (gui *Gui) handleDiscardOldFileChange() error { } } - return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.BLOCK_UI}) }) }, }) @@ -109,7 +111,7 @@ func (gui *Gui) refreshCommitFilesView() error { files, err := gui.Git.Loaders.CommitFiles.GetFilesInDiff(from, to, reverse) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.State.CommitFileTreeViewModel.SetParent(to) gui.State.CommitFileTreeViewModel.SetFiles(files) @@ -133,7 +135,7 @@ func (gui *Gui) handleEditCommitFile() error { } if node.File == nil { - return gui.createErrorPanel(gui.Tr.ErrCannotEditDirectory) + return gui.PopupHandler.ErrorMsg(gui.Tr.ErrCannotEditDirectory) } return gui.editFile(node.GetPath()) @@ -167,7 +169,7 @@ func (gui *Gui) handleToggleFileForPatch() error { }) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } if gui.Git.Patch.PatchManager.IsEmpty() { @@ -178,10 +180,10 @@ func (gui *Gui) handleToggleFileForPatch() error { } if gui.Git.Patch.PatchManager.Active() && gui.Git.Patch.PatchManager.To != gui.State.CommitFileTreeViewModel.GetParent() { - return gui.ask(askOpts{ - title: gui.Tr.DiscardPatch, - prompt: gui.Tr.DiscardPatchConfirm, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.DiscardPatch, + Prompt: gui.Tr.DiscardPatchConfirm, + HandleConfirm: func() error { gui.Git.Patch.PatchManager.Reset() return toggleTheFile() }, @@ -226,10 +228,10 @@ func (gui *Gui) enterCommitFile(opts OnFocusOpts) error { } if gui.Git.Patch.PatchManager.Active() && gui.Git.Patch.PatchManager.To != gui.State.CommitFileTreeViewModel.GetParent() { - return gui.ask(askOpts{ - title: gui.Tr.DiscardPatch, - prompt: gui.Tr.DiscardPatchConfirm, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.DiscardPatch, + Prompt: gui.Tr.DiscardPatchConfirm, + HandleConfirm: func() error { gui.Git.Patch.PatchManager.Reset() return enterTheFile() }, diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go index 5f5a8741f..feed1aecc 100644 --- a/pkg/gui/commit_message_panel.go +++ b/pkg/gui/commit_message_panel.go @@ -12,7 +12,7 @@ func (gui *Gui) handleCommitConfirm() error { message := strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent()) gui.State.failedCommitMessage = message if message == "" { - return gui.createErrorPanel(gui.Tr.CommitWithoutMessageErr) + return gui.PopupHandler.ErrorMsg(gui.Tr.CommitWithoutMessageErr) } cmdObj := gui.Git.Commit.CommitCmdObj(message) diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 26015c069..342596964 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -6,6 +6,8 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/loaders" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -29,7 +31,7 @@ func (gui *Gui) onCommitFocus() error { state.LimitCommits = false go utils.Safe(func() { if err := gui.refreshCommitsWithLimit(); err != nil { - _ = gui.surfaceError(err) + _ = gui.PopupHandler.Error(err) } }) } @@ -122,7 +124,7 @@ func (gui *Gui) refreshCommitsWithLimit() error { FilterPath: gui.State.Modes.Filtering.GetPath(), IncludeRebaseCommits: true, RefName: gui.refForLog(), - All: gui.State.ShowWholeGitGraph, + All: gui.ShowWholeGitGraph, }, ) if err != nil { @@ -170,7 +172,7 @@ func (gui *Gui) handleCommitSquashDown() error { } if len(gui.State.Commits) <= 1 { - return gui.createErrorPanel(gui.Tr.YouNoCommitsToSquash) + return gui.PopupHandler.ErrorMsg(gui.Tr.YouNoCommitsToSquash) } applied, err := gui.handleMidRebaseCommand("squash") @@ -181,11 +183,11 @@ func (gui *Gui) handleCommitSquashDown() error { return nil } - return gui.ask(askOpts{ - title: gui.Tr.Squash, - prompt: gui.Tr.SureSquashThisCommit, - handleConfirm: func() error { - return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.Squash, + Prompt: gui.Tr.SureSquashThisCommit, + HandleConfirm: func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.SquashingStatus, func() error { gui.logAction(gui.Tr.Actions.SquashCommitDown) err := gui.Git.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash") return gui.handleGenericMergeCommandResult(err) @@ -200,7 +202,7 @@ func (gui *Gui) handleCommitFixup() error { } if len(gui.State.Commits) <= 1 { - return gui.createErrorPanel(gui.Tr.YouNoCommitsToSquash) + return gui.PopupHandler.ErrorMsg(gui.Tr.YouNoCommitsToSquash) } applied, err := gui.handleMidRebaseCommand("fixup") @@ -211,11 +213,11 @@ func (gui *Gui) handleCommitFixup() error { return nil } - return gui.ask(askOpts{ - title: gui.Tr.Fixup, - prompt: gui.Tr.SureFixupThisCommit, - handleConfirm: func() error { - return gui.WithWaitingStatus(gui.Tr.FixingStatus, func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.Fixup, + Prompt: gui.Tr.SureFixupThisCommit, + HandleConfirm: func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.FixingStatus, func() error { gui.logAction(gui.Tr.Actions.FixupCommit) err := gui.Git.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup") return gui.handleGenericMergeCommandResult(err) @@ -244,20 +246,20 @@ func (gui *Gui) handleRewordCommit() error { message, err := gui.Git.Commit.GetCommitMessage(commit.Sha) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } // TODO: use the commit message panel here - return gui.prompt(promptOpts{ - title: gui.Tr.LcRewordCommit, - initialContent: message, - handleConfirm: func(response string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: gui.Tr.LcRewordCommit, + InitialContent: message, + HandleConfirm: func(response string) error { gui.logAction(gui.Tr.Actions.RewordCommit) if err := gui.Git.Rebase.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, response); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) }, }) } @@ -278,7 +280,7 @@ func (gui *Gui) handleRewordCommitEditor() error { gui.logAction(gui.Tr.Actions.RewordCommit) subProcess, err := gui.Git.Rebase.RewordCommitInEditor(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } if subProcess != nil { return gui.runSubprocessWithSuspenseAndRefresh(subProcess) @@ -301,7 +303,7 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) { // 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 == "reword" { - return true, gui.createErrorPanel(gui.Tr.LcRewordNotSupported) + return true, gui.PopupHandler.ErrorMsg(gui.Tr.LcRewordNotSupported) } gui.logAction("Update rebase TODO") @@ -311,7 +313,7 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) { ) if err := gui.Git.Rebase.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil { - return false, gui.surfaceError(err) + return false, gui.PopupHandler.Error(err) } return true, gui.refreshRebaseCommits() @@ -330,11 +332,11 @@ func (gui *Gui) handleCommitDelete() error { return nil } - return gui.ask(askOpts{ - title: gui.Tr.DeleteCommitTitle, - prompt: gui.Tr.DeleteCommitPrompt, - handleConfirm: func() error { - return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.DeleteCommitTitle, + Prompt: gui.Tr.DeleteCommitPrompt, + HandleConfirm: func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.DeletingStatus, func() error { gui.logAction(gui.Tr.Actions.DropCommit) err := gui.Git.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop") return gui.handleGenericMergeCommandResult(err) @@ -361,13 +363,13 @@ func (gui *Gui) handleCommitMoveDown() error { gui.logCommand(fmt.Sprintf("Moving commit %s down", selectedCommit.ShortSha()), false) if err := gui.Git.Rebase.MoveTodoDown(index); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.State.Panels.Commits.SelectedLineIdx++ return gui.refreshRebaseCommits() } - return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.MovingStatus, func() error { gui.logAction(gui.Tr.Actions.MoveCommitDown) err := gui.Git.Rebase.MoveCommitDown(gui.State.Commits, index) if err == nil { @@ -398,13 +400,13 @@ func (gui *Gui) handleCommitMoveUp() error { ) if err := gui.Git.Rebase.MoveTodoDown(index - 1); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.State.Panels.Commits.SelectedLineIdx-- return gui.refreshRebaseCommits() } - return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.MovingStatus, func() error { gui.logAction(gui.Tr.Actions.MoveCommitUp) err := gui.Git.Rebase.MoveCommitDown(gui.State.Commits, index-1) if err == nil { @@ -427,7 +429,7 @@ func (gui *Gui) handleCommitEdit() error { return nil } - return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { gui.logAction(gui.Tr.Actions.EditCommit) err = gui.Git.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "edit") return gui.handleGenericMergeCommandResult(err) @@ -439,11 +441,11 @@ func (gui *Gui) handleCommitAmendTo() error { return err } - return gui.ask(askOpts{ - title: gui.Tr.AmendCommitTitle, - prompt: gui.Tr.AmendCommitPrompt, - handleConfirm: func() error { - return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.AmendCommitTitle, + Prompt: gui.Tr.AmendCommitPrompt, + HandleConfirm: func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.AmendingStatus, func() error { gui.logAction(gui.Tr.Actions.AmendCommit) err := gui.Git.Rebase.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha) return gui.handleGenericMergeCommandResult(err) @@ -478,17 +480,17 @@ func (gui *Gui) handleCommitRevert() error { if commit.IsMerge() { return gui.createRevertMergeCommitMenu(commit) } else { - return gui.ask(askOpts{ - title: gui.Tr.Actions.RevertCommit, - prompt: utils.ResolvePlaceholderString( + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.Actions.RevertCommit, + Prompt: utils.ResolvePlaceholderString( gui.Tr.ConfirmRevertCommit, map[string]string{ "selectedCommit": commit.ShortSha(), }), - handleConfirm: func() error { + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.RevertCommit) if err := gui.Git.Commit.Revert(commit.Sha); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return gui.afterRevertCommit() }, @@ -497,33 +499,33 @@ func (gui *Gui) handleCommitRevert() error { } func (gui *Gui) createRevertMergeCommitMenu(commit *models.Commit) error { - menuItems := make([]*menuItem, len(commit.Parents)) + menuItems := make([]*popup.MenuItem, len(commit.Parents)) for i, parentSha := range commit.Parents { i := i message, err := gui.Git.Commit.GetCommitMessageFirstLine(parentSha) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - menuItems[i] = &menuItem{ - displayString: fmt.Sprintf("%s: %s", utils.SafeTruncate(parentSha, 8), message), - onPress: func() error { + menuItems[i] = &popup.MenuItem{ + DisplayString: fmt.Sprintf("%s: %s", utils.SafeTruncate(parentSha, 8), message), + OnPress: func() error { parentNumber := i + 1 gui.logAction(gui.Tr.Actions.RevertCommit) if err := gui.Git.Commit.RevertMerge(commit.Sha, parentNumber); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return gui.afterRevertCommit() }, } } - return gui.createMenu(gui.Tr.SelectParentCommitForMerge, menuItems, createMenuOptions{showCancel: true}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: gui.Tr.SelectParentCommitForMerge, Items: menuItems}) } func (gui *Gui) afterRevertCommit() error { gui.State.Panels.Commits.SelectedLineIdx++ - return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []RefreshableView{COMMITS, BRANCHES}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.BLOCK_UI, Scope: []types.RefreshableView{types.COMMITS, types.BRANCHES}}) } func (gui *Gui) handleViewCommitFiles() error { @@ -552,16 +554,16 @@ func (gui *Gui) handleCreateFixupCommit() error { }, ) - return gui.ask(askOpts{ - title: gui.Tr.CreateFixupCommit, - prompt: prompt, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.CreateFixupCommit, + Prompt: prompt, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.CreateFixupCommit) if err := gui.Git.Commit.CreateFixupCommit(commit.Sha); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) }, }) } @@ -583,11 +585,11 @@ func (gui *Gui) handleSquashAllAboveFixupCommits() error { }, ) - return gui.ask(askOpts{ - title: gui.Tr.SquashAboveCommits, - prompt: prompt, - handleConfirm: func() error { - return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.SquashAboveCommits, + Prompt: prompt, + HandleConfirm: func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.SquashingStatus, func() error { gui.logAction(gui.Tr.Actions.SquashAllAboveFixupCommits) err := gui.Git.Rebase.SquashAllAboveFixupCommits(commit.Sha) return gui.handleGenericMergeCommandResult(err) @@ -606,39 +608,40 @@ func (gui *Gui) handleTagCommit() error { } func (gui *Gui) createTagMenu(commitSha string) error { - items := []*menuItem{ - { - displayString: gui.Tr.LcLightweightTag, - onPress: func() error { - return gui.handleCreateLightweightTag(commitSha) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{ + Title: gui.Tr.TagMenuTitle, + Items: []*popup.MenuItem{ + { + DisplayString: gui.Tr.LcLightweightTag, + OnPress: func() error { + return gui.handleCreateLightweightTag(commitSha) + }, + }, + { + DisplayString: gui.Tr.LcAnnotatedTag, + OnPress: func() error { + return gui.handleCreateAnnotatedTag(commitSha) + }, }, }, - { - displayString: gui.Tr.LcAnnotatedTag, - onPress: func() error { - return gui.handleCreateAnnotatedTag(commitSha) - }, - }, - } - - return gui.createMenu(gui.Tr.TagMenuTitle, items, createMenuOptions{showCancel: true}) + }) } func (gui *Gui) afterTagCreate() error { gui.State.Panels.Tags.SelectedLineIdx = 0 // Set to the top - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}}) } func (gui *Gui) handleCreateAnnotatedTag(commitSha string) error { - return gui.prompt(promptOpts{ - title: gui.Tr.TagNameTitle, - handleConfirm: func(tagName string) error { - return gui.prompt(promptOpts{ - title: gui.Tr.TagMessageTitle, - handleConfirm: func(msg string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: gui.Tr.TagNameTitle, + HandleConfirm: func(tagName string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: gui.Tr.TagMessageTitle, + HandleConfirm: func(msg string) error { gui.logAction(gui.Tr.Actions.CreateAnnotatedTag) if err := gui.Git.Tag.CreateAnnotated(tagName, commitSha, msg); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return gui.afterTagCreate() }, @@ -648,12 +651,12 @@ func (gui *Gui) handleCreateAnnotatedTag(commitSha string) error { } func (gui *Gui) handleCreateLightweightTag(commitSha string) error { - return gui.prompt(promptOpts{ - title: gui.Tr.TagNameTitle, - handleConfirm: func(tagName string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: gui.Tr.TagNameTitle, + HandleConfirm: func(tagName string) error { gui.logAction(gui.Tr.Actions.CreateLightweightTag) if err := gui.Git.Tag.CreateLightweight(tagName, commitSha); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return gui.afterTagCreate() }, @@ -666,10 +669,10 @@ func (gui *Gui) handleCheckoutCommit() error { return nil } - return gui.ask(askOpts{ - title: gui.Tr.LcCheckoutCommit, - prompt: gui.Tr.SureCheckoutThisCommit, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.LcCheckoutCommit, + Prompt: gui.Tr.SureCheckoutThisCommit, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.CheckoutCommit) return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{}) }, @@ -679,7 +682,7 @@ func (gui *Gui) handleCheckoutCommit() error { func (gui *Gui) handleCreateCommitResetMenu() error { commit := gui.getSelectedLocalCommit() if commit == nil { - return gui.createErrorPanel(gui.Tr.NoCommitsThisBranch) + return gui.PopupHandler.ErrorMsg(gui.Tr.NoCommitsThisBranch) } return gui.createResetMenu(commit.Sha) @@ -689,7 +692,7 @@ func (gui *Gui) handleOpenSearchForCommitsPanel(string) error { // we usually lazyload these commits but now that we're searching we need to load them now if gui.State.Panels.Commits.LimitCommits { gui.State.Panels.Commits.LimitCommits = false - if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS}}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS}}); err != nil { return err } } @@ -701,7 +704,7 @@ func (gui *Gui) handleGotoBottomForCommitsPanel() error { // we usually lazyload these commits but now that we're searching we need to load them now if gui.State.Panels.Commits.LimitCommits { gui.State.Panels.Commits.LimitCommits = false - if err := gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}}); err != nil { return err } } @@ -723,12 +726,12 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error { message, err := gui.Git.Commit.GetCommitMessage(commit.Sha) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.logAction(gui.Tr.Actions.CopyCommitMessageToClipboard) if err := gui.OSCommand.CopyToClipboard(message); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.raiseToast(gui.Tr.CommitMessageCopiedToClipboard) @@ -737,87 +740,87 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error { } func (gui *Gui) handleOpenLogMenu() error { - return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{ - { - displayString: gui.Tr.ToggleShowGitGraphAll, - onPress: func() error { - gui.State.ShowWholeGitGraph = !gui.State.ShowWholeGitGraph + return gui.PopupHandler.Menu(popup.CreateMenuOptions{ + Title: gui.Tr.LogMenuTitle, + Items: []*popup.MenuItem{ + { + DisplayString: gui.Tr.ToggleShowGitGraphAll, + OnPress: func() error { + gui.ShowWholeGitGraph = !gui.ShowWholeGitGraph - if gui.State.ShowWholeGitGraph { - gui.State.Panels.Commits.LimitCommits = false - } + if gui.ShowWholeGitGraph { + gui.State.Panels.Commits.LimitCommits = false + } - return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error { - return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}}) - }) - }, - }, - { - displayString: gui.Tr.ShowGitGraph, - opensMenu: true, - onPress: func() error { - onSelect := func(value string) { - gui.UserConfig.Git.Log.ShowGraph = value - gui.render() - } - return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{ - { - displayString: "always", - onPress: func() error { - onSelect("always") - return nil - }, - }, - { - displayString: "never", - onPress: func() error { - onSelect("never") - return nil - }, - }, - { - displayString: "when maximised", - onPress: func() error { - onSelect("when-maximised") - return nil - }, - }, - }, createMenuOptions{showCancel: true}) - }, - }, - { - displayString: gui.Tr.SortCommits, - opensMenu: true, - onPress: func() error { - onSelect := func(value string) error { - gui.UserConfig.Git.Log.Order = value - return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error { - return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}}) + return gui.PopupHandler.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error { + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}}) }) - } - return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{ - { - displayString: "topological (topo-order)", - onPress: func() error { - return onSelect("topo-order") + }, + }, + { + DisplayString: gui.Tr.ShowGitGraph, + OpensMenu: true, + OnPress: func() error { + onPress := func(value string) func() error { + return func() error { + gui.UserConfig.Git.Log.ShowGraph = value + gui.render() + return nil + } + } + return gui.PopupHandler.Menu(popup.CreateMenuOptions{ + Title: gui.Tr.LogMenuTitle, + Items: []*popup.MenuItem{ + { + DisplayString: "always", + OnPress: onPress("always"), + }, + { + DisplayString: "never", + OnPress: onPress("never"), + }, + { + DisplayString: "when maximised", + OnPress: onPress("when-maximised"), + }, }, - }, - { - displayString: "date-order", - onPress: func() error { - return onSelect("date-order") + }) + }, + }, + { + DisplayString: gui.Tr.SortCommits, + OpensMenu: true, + OnPress: func() error { + onPress := func(value string) func() error { + return func() error { + gui.UserConfig.Git.Log.Order = value + return gui.PopupHandler.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error { + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}}) + }) + } + } + + return gui.PopupHandler.Menu(popup.CreateMenuOptions{ + Title: gui.Tr.LogMenuTitle, + Items: []*popup.MenuItem{ + { + DisplayString: "topological (topo-order)", + OnPress: onPress("topo-order"), + }, + { + DisplayString: "date-order", + OnPress: onPress("date-order"), + }, + { + DisplayString: "author-date-order", + OnPress: onPress("author-date-order"), + }, }, - }, - { - displayString: "author-date-order", - onPress: func() error { - return onSelect("author-date-order") - }, - }, - }, createMenuOptions{showCancel: true}) + }) + }, }, }, - }, createMenuOptions{showCancel: true}) + }) } func (gui *Gui) handleOpenCommitInBrowser() error { @@ -830,12 +833,12 @@ func (gui *Gui) handleOpenCommitInBrowser() error { url, err := hostingServiceMgr.GetCommitURL(commit.Sha) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.logAction(gui.Tr.Actions.OpenCommitInBrowser) if err := gui.OSCommand.OpenLink(url); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return nil diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index b092b1d10..0d5457101 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -5,53 +5,12 @@ import ( "strings" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/utils" ) -type createPopupPanelOpts struct { - hasLoader bool - editable bool - title string - prompt string - handleConfirm func() error - handleConfirmPrompt func(string) error - handleClose func() error - - // when handlersManageFocus is true, do not return from the confirmation context automatically. It's expected that the handlers will manage focus, whether that means switching to another context, or manually returning the context. - handlersManageFocus bool - - findSuggestionsFunc func(string) []*types.Suggestion -} - -type askOpts struct { - title string - prompt string - handleConfirm func() error - handleClose func() error - handlersManageFocus bool -} - -type promptOpts struct { - title string - initialContent string - findSuggestionsFunc func(string) []*types.Suggestion - handleConfirm func(string) error -} - -func (gui *Gui) ask(opts askOpts) error { - return gui.PopupHandler.Ask(opts) -} - -func (gui *Gui) prompt(opts promptOpts) error { - return gui.PopupHandler.Prompt(opts) -} - -func (gui *Gui) createLoaderPanel(prompt string) error { - return gui.PopupHandler.Loader(prompt) -} - func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() error) func() error { return func() error { if err := gui.closeConfirmationPrompt(handlersManageFocus); err != nil { @@ -60,7 +19,7 @@ func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function f if function != nil { if err := function(); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } } @@ -76,7 +35,7 @@ func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, func if function != nil { if err := function(getResponse()); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } } @@ -179,31 +138,31 @@ func (gui *Gui) prepareConfirmationPanel( return nil } -func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error { +func (gui *Gui) createPopupPanel(opts popup.CreatePopupPanelOpts) error { // remove any previous keybindings gui.clearConfirmationViewKeyBindings() err := gui.prepareConfirmationPanel( - opts.title, - opts.prompt, - opts.hasLoader, - opts.findSuggestionsFunc, - opts.editable, + opts.Title, + opts.Prompt, + opts.HasLoader, + opts.FindSuggestionsFunc, + opts.Editable, ) if err != nil { return err } confirmationView := gui.Views.Confirmation - confirmationView.Editable = opts.editable + confirmationView.Editable = opts.Editable confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor) - if opts.editable { + if opts.Editable { textArea := confirmationView.TextArea textArea.Clear() - textArea.TypeString(opts.prompt) + textArea.TypeString(opts.Prompt) confirmationView.RenderTextArea() } else { - if err := gui.renderString(confirmationView, opts.prompt); err != nil { + if err := gui.renderString(confirmationView, opts.Prompt); err != nil { return err } } @@ -215,7 +174,7 @@ func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error { return gui.pushContext(gui.State.Contexts.Confirmation) } -func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error { +func (gui *Gui) setKeyBindings(opts popup.CreatePopupPanelOpts) error { actions := utils.ResolvePlaceholderString( gui.Tr.CloseConfirm, map[string]string{ @@ -226,10 +185,10 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error { _ = gui.renderString(gui.Views.Options, actions) var onConfirm func() error - if opts.handleConfirmPrompt != nil { - onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() }) + if opts.HandleConfirmPrompt != nil { + onConfirm = gui.wrappedPromptConfirmationFunction(opts.HandlersManageFocus, opts.HandleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() }) } else { - onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm) + onConfirm = gui.wrappedConfirmationFunction(opts.HandlersManageFocus, opts.HandleConfirm) } type confirmationKeybinding struct { @@ -240,8 +199,8 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error { keybindingConfig := gui.UserConfig.Keybinding onSuggestionConfirm := gui.wrappedPromptConfirmationFunction( - opts.handlersManageFocus, - opts.handleConfirmPrompt, + opts.HandlersManageFocus, + opts.HandleConfirmPrompt, gui.getSelectedSuggestionValue, ) @@ -259,7 +218,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error { { viewName: "confirmation", key: gui.getKey(keybindingConfig.Universal.Return), - handler: gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose), + handler: gui.wrappedConfirmationFunction(opts.HandlersManageFocus, opts.HandleClose), }, { viewName: "confirmation", @@ -284,7 +243,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error { { viewName: "suggestions", key: gui.getKey(keybindingConfig.Universal.Return), - handler: gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose), + handler: gui.wrappedConfirmationFunction(opts.HandlersManageFocus, opts.HandleClose), }, { viewName: "suggestions", @@ -317,19 +276,3 @@ func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View) return f() } } - -func (gui *Gui) createErrorPanel(message string) error { - return gui.PopupHandler.Error(message) -} - -func (gui *Gui) surfaceError(err error) error { - if err == nil { - return nil - } - - if err == gocui.ErrQuit { - return err - } - - return gui.createErrorPanel(err.Error()) -} diff --git a/pkg/gui/context_config.go b/pkg/gui/context_config.go index f567f5a5c..e884d32bd 100644 --- a/pkg/gui/context_config.go +++ b/pkg/gui/context_config.go @@ -174,12 +174,13 @@ func (gui *Gui) contextTree() ContextTree { OnGetOptionsMap: gui.getMergingOptions, }, Credentials: &BasicContext{ - OnFocus: OnFocusWrapper(gui.handleCredentialsViewFocused), + OnFocus: OnFocusWrapper(gui.handleAskFocused), Kind: PERSISTENT_POPUP, ViewName: "credentials", Key: CREDENTIALS_CONTEXT_KEY, }, Confirmation: &BasicContext{ + OnFocus: OnFocusWrapper(gui.handleAskFocused), Kind: TEMPORARY_POPUP, ViewName: "confirmation", Key: CONFIRMATION_CONTEXT_KEY, diff --git a/pkg/gui/controllers/submodules_controller.go b/pkg/gui/controllers/submodules_controller.go new file mode 100644 index 000000000..e5eaf98a0 --- /dev/null +++ b/pkg/gui/controllers/submodules_controller.go @@ -0,0 +1,243 @@ +package controllers + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/common" + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +// if Go let me do private struct embedding of structs with public fields (which it should) +// I would just do that. But alas. +type ControllerCommon struct { + *common.Common + IGuiCommon +} + +type SubmodulesController struct { + // I've said publicly that I'm against single-letter variable names but in this + // case I would actually prefer a _zero_ letter variable name in the form of + // struct embedding, but Go does not allow hiding public fields in an embedded struct + // to the client + c *ControllerCommon + enterSubmoduleFn func(submodule *models.SubmoduleConfig) error + getSelectedSubmodule func() *models.SubmoduleConfig + git *commands.GitCommand + submodules []*models.SubmoduleConfig +} + +func NewSubmodulesController( + c *ControllerCommon, + enterSubmoduleFn func(submodule *models.SubmoduleConfig) error, + git *commands.GitCommand, + submodules []*models.SubmoduleConfig, + getSelectedSubmodule func() *models.SubmoduleConfig, +) *SubmodulesController { + return &SubmodulesController{ + c: c, + enterSubmoduleFn: enterSubmoduleFn, + git: git, + submodules: submodules, + getSelectedSubmodule: getSelectedSubmodule, + } +} + +func (self *SubmodulesController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig) []*types.Binding { + return []*types.Binding{ + { + Key: getKey(config.Universal.GoInto), + Handler: self.forSubmodule(self.enter), + Description: self.c.Tr.LcEnterSubmodule, + }, + { + Key: getKey(config.Universal.Remove), + Handler: self.forSubmodule(self.remove), + Description: self.c.Tr.LcRemoveSubmodule, + }, + { + Key: getKey(config.Submodules.Update), + Handler: self.forSubmodule(self.update), + Description: self.c.Tr.LcSubmoduleUpdate, + }, + { + Key: getKey(config.Universal.New), + Handler: self.add, + Description: self.c.Tr.LcAddSubmodule, + }, + { + Key: getKey(config.Universal.Edit), + Handler: self.forSubmodule(self.editURL), + Description: self.c.Tr.LcEditSubmoduleUrl, + }, + { + Key: getKey(config.Submodules.Init), + Handler: self.forSubmodule(self.init), + Description: self.c.Tr.LcInitSubmodule, + }, + { + Key: getKey(config.Submodules.BulkMenu), + Handler: self.openBulkActionsMenu, + Description: self.c.Tr.LcViewBulkSubmoduleOptions, + OpensMenu: true, + }, + } +} + +func (self *SubmodulesController) enter(submodule *models.SubmoduleConfig) error { + return self.enterSubmoduleFn(submodule) +} + +func (self *SubmodulesController) add() error { + return self.c.Prompt(popup.PromptOpts{ + Title: self.c.Tr.LcNewSubmoduleUrl, + HandleConfirm: func(submoduleUrl string) error { + nameSuggestion := filepath.Base(strings.TrimSuffix(submoduleUrl, filepath.Ext(submoduleUrl))) + + return self.c.Prompt(popup.PromptOpts{ + Title: self.c.Tr.LcNewSubmoduleName, + InitialContent: nameSuggestion, + HandleConfirm: func(submoduleName string) error { + + return self.c.Prompt(popup.PromptOpts{ + Title: self.c.Tr.LcNewSubmodulePath, + InitialContent: submoduleName, + HandleConfirm: func(submodulePath string) error { + return self.c.WithWaitingStatus(self.c.Tr.LcAddingSubmoduleStatus, func() error { + self.c.LogAction(self.c.Tr.Actions.AddSubmodule) + err := self.git.Submodule.Add(submoduleName, submodulePath, submoduleUrl) + if err != nil { + _ = self.c.Error(err) + } + + return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}}) + }) + }, + }) + }, + }) + }, + }) +} + +func (self *SubmodulesController) editURL(submodule *models.SubmoduleConfig) error { + return self.c.Prompt(popup.PromptOpts{ + Title: fmt.Sprintf(self.c.Tr.LcUpdateSubmoduleUrl, submodule.Name), + InitialContent: submodule.Url, + HandleConfirm: func(newUrl string) error { + return self.c.WithWaitingStatus(self.c.Tr.LcUpdatingSubmoduleUrlStatus, func() error { + self.c.LogAction(self.c.Tr.Actions.UpdateSubmoduleUrl) + err := self.git.Submodule.UpdateUrl(submodule.Name, submodule.Path, newUrl) + if err != nil { + _ = self.c.Error(err) + } + + return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}}) + }) + }, + }) +} + +func (self *SubmodulesController) init(submodule *models.SubmoduleConfig) error { + return self.c.WithWaitingStatus(self.c.Tr.LcInitializingSubmoduleStatus, func() error { + self.c.LogAction(self.c.Tr.Actions.InitialiseSubmodule) + err := self.git.Submodule.Init(submodule.Path) + if err != nil { + _ = self.c.Error(err) + } + + return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}}) + }) +} + +func (self *SubmodulesController) openBulkActionsMenu() error { + return self.c.Menu(popup.CreateMenuOptions{ + Title: self.c.Tr.LcBulkSubmoduleOptions, + Items: []*popup.MenuItem{ + { + DisplayStrings: []string{self.c.Tr.LcBulkInitSubmodules, style.FgGreen.Sprint(self.git.Submodule.BulkInitCmdObj().ToString())}, + OnPress: func() error { + return self.c.WithWaitingStatus(self.c.Tr.LcRunningCommand, func() error { + self.c.LogAction(self.c.Tr.Actions.BulkInitialiseSubmodules) + err := self.git.Submodule.BulkInitCmdObj().Run() + if err != nil { + return self.c.Error(err) + } + + return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}}) + }) + }, + }, + { + DisplayStrings: []string{self.c.Tr.LcBulkUpdateSubmodules, style.FgYellow.Sprint(self.git.Submodule.BulkUpdateCmdObj().ToString())}, + OnPress: func() error { + return self.c.WithWaitingStatus(self.c.Tr.LcRunningCommand, func() error { + self.c.LogAction(self.c.Tr.Actions.BulkUpdateSubmodules) + if err := self.git.Submodule.BulkUpdateCmdObj().Run(); err != nil { + return self.c.Error(err) + } + + return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}}) + }) + }, + }, + { + DisplayStrings: []string{self.c.Tr.LcBulkDeinitSubmodules, style.FgRed.Sprint(self.git.Submodule.BulkDeinitCmdObj().ToString())}, + OnPress: func() error { + return self.c.WithWaitingStatus(self.c.Tr.LcRunningCommand, func() error { + self.c.LogAction(self.c.Tr.Actions.BulkDeinitialiseSubmodules) + if err := self.git.Submodule.BulkDeinitCmdObj().Run(); err != nil { + return self.c.Error(err) + } + + return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}}) + }) + }, + }, + }, + }) +} + +func (self *SubmodulesController) update(submodule *models.SubmoduleConfig) error { + return self.c.WithWaitingStatus(self.c.Tr.LcUpdatingSubmoduleStatus, func() error { + self.c.LogAction(self.c.Tr.Actions.UpdateSubmodule) + err := self.git.Submodule.Update(submodule.Path) + if err != nil { + _ = self.c.Error(err) + } + + return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}}) + }) +} + +func (self *SubmodulesController) remove(submodule *models.SubmoduleConfig) error { + return self.c.Ask(popup.AskOpts{ + Title: self.c.Tr.RemoveSubmodule, + Prompt: fmt.Sprintf(self.c.Tr.RemoveSubmodulePrompt, submodule.Name), + HandleConfirm: func() error { + self.c.LogAction(self.c.Tr.Actions.RemoveSubmodule) + if err := self.git.Submodule.Delete(submodule); err != nil { + return self.c.Error(err) + } + + return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES, types.FILES}}) + }, + }) +} + +func (self *SubmodulesController) forSubmodule(callback func(*models.SubmoduleConfig) error) func() error { + return func() error { + submodule := self.getSelectedSubmodule() + if submodule == nil { + return nil + } + + return callback(submodule) + } +} diff --git a/pkg/gui/controllers/types.go b/pkg/gui/controllers/types.go new file mode 100644 index 000000000..75abc1704 --- /dev/null +++ b/pkg/gui/controllers/types.go @@ -0,0 +1,13 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type IGuiCommon interface { + popup.IPopupHandler + + LogAction(string) + Refresh(types.RefreshOptions) error +} diff --git a/pkg/gui/credentials_panel.go b/pkg/gui/credentials_panel.go index 984591a62..b7981338d 100644 --- a/pkg/gui/credentials_panel.go +++ b/pkg/gui/credentials_panel.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -48,15 +49,16 @@ func (gui *Gui) handleSubmitCredential() error { return err } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) } func (gui *Gui) handleCloseCredentialsView() error { + gui.Views.Credentials.ClearTextArea() gui.credentials <- "" return gui.returnFromContext() } -func (gui *Gui) handleCredentialsViewFocused() error { +func (gui *Gui) handleAskFocused() error { keybindingConfig := gui.UserConfig.Keybinding message := utils.ResolvePlaceholderString( @@ -69,18 +71,3 @@ func (gui *Gui) handleCredentialsViewFocused() error { return gui.renderString(gui.Views.Options, message) } - -// handleCredentialsPopup handles the views after executing a command that might ask for credentials -func (gui *Gui) handleCredentialsPopup(cmdErr error) { - if cmdErr != nil { - errMessage := cmdErr.Error() - if strings.Contains(errMessage, "Invalid username, password or passphrase") { - errMessage = gui.Tr.PassUnameWrong - } - _ = gui.returnFromContext() - // we are not logging this error because it may contain a password or a passphrase - _ = gui.createErrorPanel(errMessage) - } else { - _ = gui.closeConfirmationPrompt(false) - } -} diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 02293dd65..44548ef72 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -12,7 +12,9 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -62,18 +64,18 @@ func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (s func (gui *Gui) inputPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error { title, err := gui.resolveTemplate(prompt.Title, promptResponses) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.prompt(promptOpts{ - title: title, - initialContent: initialValue, - handleConfirm: func(str string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: title, + InitialContent: initialValue, + HandleConfirm: func(str string) error { promptResponses[responseIdx] = str return wrappedF() }, @@ -82,7 +84,7 @@ func (gui *Gui) inputPrompt(prompt config.CustomCommandPrompt, promptResponses [ func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error { // need to make a menu here some how - menuItems := make([]*menuItem, len(prompt.Options)) + menuItems := make([]*popup.MenuItem, len(prompt.Options)) for i, option := range prompt.Options { option := option @@ -93,22 +95,22 @@ func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses [] } name, err := gui.resolveTemplate(nameTemplate, promptResponses) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } description, err := gui.resolveTemplate(option.Description, promptResponses) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } value, err := gui.resolveTemplate(option.Value, promptResponses) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - menuItems[i] = &menuItem{ - displayStrings: []string{name, style.FgYellow.Sprint(description)}, - onPress: func() error { + menuItems[i] = &popup.MenuItem{ + DisplayStrings: []string{name, style.FgYellow.Sprint(description)}, + OnPress: func() error { promptResponses[responseIdx] = value return wrappedF() }, @@ -117,30 +119,30 @@ func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses [] title, err := gui.resolveTemplate(prompt.Title, promptResponses) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: title, Items: menuItems}) } func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, labelFormat string) ([]commandMenuEntry, error) { reg, err := regexp.Compile(filter) if err != nil { - return nil, gui.surfaceError(errors.New("unable to parse filter regex, error: " + err.Error())) + return nil, gui.PopupHandler.Error(errors.New("unable to parse filter regex, error: " + err.Error())) } buff := bytes.NewBuffer(nil) valueTemp, err := template.New("format").Parse(valueFormat) if err != nil { - return nil, gui.surfaceError(errors.New("unable to parse value format, error: " + err.Error())) + return nil, gui.PopupHandler.Error(errors.New("unable to parse value format, error: " + err.Error())) } colorFuncMap := style.TemplateFuncMapAddColors(template.FuncMap{}) descTemp, err := template.New("format").Funcs(colorFuncMap).Parse(labelFormat) if err != nil { - return nil, gui.surfaceError(errors.New("unable to parse label format, error: " + err.Error())) + return nil, gui.PopupHandler.Error(errors.New("unable to parse label format, error: " + err.Error())) } candidates := []commandMenuEntry{} @@ -165,7 +167,7 @@ func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, label err = valueTemp.Execute(buff, tmplData) if err != nil { - return candidates, gui.surfaceError(err) + return candidates, gui.PopupHandler.Error(err) } entry := commandMenuEntry{ value: strings.TrimSpace(buff.String()), @@ -175,7 +177,7 @@ func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, label buff.Reset() err = descTemp.Execute(buff, tmplData) if err != nil { - return candidates, gui.surfaceError(err) + return candidates, gui.PopupHandler.Error(err) } entry.label = strings.TrimSpace(buff.String()) } else { @@ -193,33 +195,33 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR // Collect cmd to run from config cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } // Collect Filter regexp filter, err := gui.resolveTemplate(prompt.Filter, promptResponses) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } // Run and save output message, err := gui.Git.Custom.RunWithOutput(cmdStr) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } // Need to make a menu out of what the cmd has displayed candidates, err := gui.GenerateMenuCandidates(message, filter, prompt.ValueFormat, prompt.LabelFormat) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - menuItems := make([]*menuItem, len(candidates)) + menuItems := make([]*popup.MenuItem, len(candidates)) for i := range candidates { i := i - menuItems[i] = &menuItem{ - displayStrings: []string{candidates[i].label}, - onPress: func() error { + menuItems[i] = &popup.MenuItem{ + DisplayStrings: []string{candidates[i].label}, + OnPress: func() error { promptResponses[responseIdx] = candidates[i].value return wrappedF() }, @@ -228,10 +230,10 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR title, err := gui.resolveTemplate(prompt.Title, promptResponses) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: title, Items: menuItems}) } func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand) func() error { @@ -241,7 +243,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand f := func() error { cmdStr, err := gui.resolveTemplate(customCommand.Command, promptResponses) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } if customCommand.Subprocess { @@ -252,7 +254,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand if loadingText == "" { loadingText = gui.Tr.LcRunningCustomCommandStatus } - return gui.WithWaitingStatus(loadingText, func() error { + return gui.PopupHandler.WithWaitingStatus(loadingText, func() error { gui.logAction(gui.Tr.Actions.CustomCommand) cmdObj := gui.OSCommand.Cmd.NewShell(cmdStr) if customCommand.Stream { @@ -260,9 +262,9 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand } err := cmdObj.Run() if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{}) + return gui.refreshSidePanels(types.RefreshOptions{}) }) } @@ -291,7 +293,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand return gui.menuPromptFromCommand(prompt, promptResponses, idx, wrappedF) } default: - return gui.createErrorPanel("custom command prompt must have a type of 'input', 'menu' or 'menuFromCommand'") + return gui.PopupHandler.ErrorMsg("custom command prompt must have a type of 'input', 'menu' or 'menuFromCommand'") } } @@ -300,8 +302,8 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand } } -func (gui *Gui) GetCustomCommandKeybindings() []*Binding { - bindings := []*Binding{} +func (gui *Gui) GetCustomCommandKeybindings() []*types.Binding { + bindings := []*types.Binding{} customCommands := gui.UserConfig.CustomCommands for _, customCommand := range customCommands { @@ -334,7 +336,7 @@ func (gui *Gui) GetCustomCommandKeybindings() []*Binding { description = customCommand.Command } - bindings = append(bindings, &Binding{ + bindings = append(bindings, &types.Binding{ ViewName: viewName, Contexts: contexts, Key: gui.getKey(customCommand.Key), diff --git a/pkg/gui/diff_context_size.go b/pkg/gui/diff_context_size.go index 3b6f6b0a9..e5ea340a6 100644 --- a/pkg/gui/diff_context_size.go +++ b/pkg/gui/diff_context_size.go @@ -28,7 +28,7 @@ func isShowingDiff(gui *Gui) bool { func (gui *Gui) IncreaseContextInDiffView() error { if isShowingDiff(gui) { if err := gui.CheckCanChangeContext(); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.UserConfig.Git.DiffContextSize = gui.UserConfig.Git.DiffContextSize + 1 @@ -43,7 +43,7 @@ func (gui *Gui) DecreaseContextInDiffView() error { if isShowingDiff(gui) && old_size > 1 { if err := gui.CheckCanChangeContext(); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.UserConfig.Git.DiffContextSize = old_size - 1 diff --git a/pkg/gui/diff_context_size_test.go b/pkg/gui/diff_context_size_test.go index b459e40d0..4f53cd3ff 100644 --- a/pkg/gui/diff_context_size_test.go +++ b/pkg/gui/diff_context_size_test.go @@ -5,6 +5,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/patch" + "github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/stretchr/testify/assert" ) @@ -144,8 +145,8 @@ func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *test gui.Git.Patch.PatchManager.Start("from", "to", false, false) errorCount := 0 - gui.PopupHandler = &TestPopupHandler{ - onError: func(message string) error { + gui.PopupHandler = &popup.TestPopupHandler{ + OnErrorMsg: func(message string) error { assert.Equal(t, gui.Tr.CantChangeContextSizeError, message) errorCount += 1 return nil @@ -166,8 +167,8 @@ func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *test gui.Git.Patch.PatchManager.Start("from", "to", false, false) errorCount := 0 - gui.PopupHandler = &TestPopupHandler{ - onError: func(message string) error { + gui.PopupHandler = &popup.TestPopupHandler{ + OnErrorMsg: func(message string) error { assert.Equal(t, gui.Tr.CantChangeContextSizeError, message) errorCount += 1 return nil diff --git a/pkg/gui/diffing.go b/pkg/gui/diffing.go index 2721a1880..232f9130b 100644 --- a/pkg/gui/diffing.go +++ b/pkg/gui/diffing.go @@ -5,11 +5,13 @@ import ( "strings" "github.com/jesseduffield/lazygit/pkg/gui/modes/diffing" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) func (gui *Gui) exitDiffMode() error { gui.State.Modes.Diffing = diffing.New() - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) } func (gui *Gui) renderDiff() error { @@ -105,31 +107,31 @@ func (gui *Gui) diffStr() string { func (gui *Gui) handleCreateDiffingMenuPanel() error { names := gui.currentDiffTerminals() - menuItems := []*menuItem{} + menuItems := []*popup.MenuItem{} for _, name := range names { name := name - menuItems = append(menuItems, []*menuItem{ + menuItems = append(menuItems, []*popup.MenuItem{ { - displayString: fmt.Sprintf("%s %s", gui.Tr.LcDiff, name), - onPress: func() error { + DisplayString: fmt.Sprintf("%s %s", gui.Tr.LcDiff, name), + OnPress: func() error { gui.State.Modes.Diffing.Ref = name // can scope this down based on current view but too lazy right now - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) }, }, }...) } - menuItems = append(menuItems, []*menuItem{ + menuItems = append(menuItems, []*popup.MenuItem{ { - displayString: gui.Tr.LcEnterRefToDiff, - onPress: func() error { - return gui.prompt(promptOpts{ - title: gui.Tr.LcEnteRefName, - findSuggestionsFunc: gui.getRefsSuggestionsFunc(), - handleConfirm: func(response string) error { + DisplayString: gui.Tr.LcEnterRefToDiff, + OnPress: func() error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: gui.Tr.LcEnteRefName, + FindSuggestionsFunc: gui.getRefsSuggestionsFunc(), + HandleConfirm: func(response string) error { gui.State.Modes.Diffing.Ref = strings.TrimSpace(response) - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) }, }) }, @@ -137,23 +139,23 @@ func (gui *Gui) handleCreateDiffingMenuPanel() error { }...) if gui.State.Modes.Diffing.Active() { - menuItems = append(menuItems, []*menuItem{ + menuItems = append(menuItems, []*popup.MenuItem{ { - displayString: gui.Tr.LcSwapDiff, - onPress: func() error { + DisplayString: gui.Tr.LcSwapDiff, + OnPress: func() error { gui.State.Modes.Diffing.Reverse = !gui.State.Modes.Diffing.Reverse - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) }, }, { - displayString: gui.Tr.LcExitDiffMode, - onPress: func() error { + DisplayString: gui.Tr.LcExitDiffMode, + OnPress: func() error { gui.State.Modes.Diffing = diffing.New() - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) }, }, }...) } - return gui.createMenu(gui.Tr.DiffingMenuTitle, menuItems, createMenuOptions{showCancel: true}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: gui.Tr.DiffingMenuTitle, Items: menuItems}) } diff --git a/pkg/gui/discard_changes_menu_panel.go b/pkg/gui/discard_changes_menu_panel.go index 673f057e8..7624730e0 100644 --- a/pkg/gui/discard_changes_menu_panel.go +++ b/pkg/gui/discard_changes_menu_panel.go @@ -1,36 +1,41 @@ package gui +import ( + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + func (gui *Gui) handleCreateDiscardMenu() error { node := gui.getSelectedFileNode() if node == nil { return nil } - var menuItems []*menuItem + var menuItems []*popup.MenuItem if node.File == nil { - menuItems = []*menuItem{ + menuItems = []*popup.MenuItem{ { - displayString: gui.Tr.LcDiscardAllChanges, - onPress: func() error { + DisplayString: gui.Tr.LcDiscardAllChanges, + OnPress: func() error { gui.logAction(gui.Tr.Actions.DiscardAllChangesInDirectory) if err := gui.Git.WorkingTree.DiscardAllDirChanges(node); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) }, }, } if node.GetHasStagedChanges() && node.GetHasUnstagedChanges() { - menuItems = append(menuItems, &menuItem{ - displayString: gui.Tr.LcDiscardUnstagedChanges, - onPress: func() error { + menuItems = append(menuItems, &popup.MenuItem{ + DisplayString: gui.Tr.LcDiscardUnstagedChanges, + OnPress: func() error { gui.logAction(gui.Tr.Actions.DiscardUnstagedChangesInDirectory) if err := gui.Git.WorkingTree.DiscardUnstagedDirChanges(node); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) }, }) } @@ -41,43 +46,43 @@ func (gui *Gui) handleCreateDiscardMenu() error { if file.IsSubmodule(submodules) { submodule := file.SubmoduleConfig(submodules) - menuItems = []*menuItem{ + menuItems = []*popup.MenuItem{ { - displayString: gui.Tr.LcSubmoduleStashAndReset, - onPress: func() error { - return gui.handleResetSubmodule(submodule) + DisplayString: gui.Tr.LcSubmoduleStashAndReset, + OnPress: func() error { + return gui.resetSubmodule(submodule) }, }, } } else { - menuItems = []*menuItem{ + menuItems = []*popup.MenuItem{ { - displayString: gui.Tr.LcDiscardAllChanges, - onPress: func() error { + DisplayString: gui.Tr.LcDiscardAllChanges, + OnPress: func() error { gui.logAction(gui.Tr.Actions.DiscardAllChangesInFile) if err := gui.Git.WorkingTree.DiscardAllFileChanges(file); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) }, }, } if file.HasStagedChanges && file.HasUnstagedChanges { - menuItems = append(menuItems, &menuItem{ - displayString: gui.Tr.LcDiscardUnstagedChanges, - onPress: func() error { + menuItems = append(menuItems, &popup.MenuItem{ + DisplayString: gui.Tr.LcDiscardUnstagedChanges, + OnPress: func() error { gui.logAction(gui.Tr.Actions.DiscardAllUnstagedChangesInFile) if err := gui.Git.WorkingTree.DiscardUnstagedFileChanges(file); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) }, }) } } } - return gui.createMenu(node.GetPath(), menuItems, createMenuOptions{showCancel: true}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: node.GetPath(), Items: menuItems}) } diff --git a/pkg/gui/extras_panel.go b/pkg/gui/extras_panel.go index 7d68bb1ec..bd65dea87 100644 --- a/pkg/gui/extras_panel.go +++ b/pkg/gui/extras_panel.go @@ -3,34 +3,36 @@ package gui import ( "io" + "github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/style" ) func (gui *Gui) handleCreateExtrasMenuPanel() error { - menuItems := []*menuItem{ - { - displayString: gui.Tr.ToggleShowCommandLog, - onPress: func() error { - currentContext := gui.currentStaticContext() - if gui.ShowExtrasWindow && currentContext.GetKey() == COMMAND_LOG_CONTEXT_KEY { - if err := gui.returnFromContext(); err != nil { - return err + return gui.PopupHandler.Menu(popup.CreateMenuOptions{ + Title: gui.Tr.CommandLog, + Items: []*popup.MenuItem{ + { + DisplayString: gui.Tr.ToggleShowCommandLog, + OnPress: func() error { + currentContext := gui.currentStaticContext() + if gui.ShowExtrasWindow && currentContext.GetKey() == COMMAND_LOG_CONTEXT_KEY { + if err := gui.returnFromContext(); err != nil { + return err + } } - } - show := !gui.ShowExtrasWindow - gui.ShowExtrasWindow = show - gui.Config.GetAppState().HideCommandLog = !show - _ = gui.Config.SaveAppState() - return nil + show := !gui.ShowExtrasWindow + gui.ShowExtrasWindow = show + gui.Config.GetAppState().HideCommandLog = !show + _ = gui.Config.SaveAppState() + return nil + }, + }, + { + DisplayString: gui.Tr.FocusCommandLog, + OnPress: gui.handleFocusCommandLog, }, }, - { - displayString: gui.Tr.FocusCommandLog, - onPress: gui.handleFocusCommandLog, - }, - } - - return gui.createMenu(gui.Tr.CommandLog, menuItems, createMenuOptions{showCancel: true}) + }) } func (gui *Gui) handleFocusCommandLog() error { diff --git a/pkg/gui/file_watching.go b/pkg/gui/file_watching.go index f5749a97d..9f4e84a0f 100644 --- a/pkg/gui/file_watching.go +++ b/pkg/gui/file_watching.go @@ -6,6 +6,7 @@ import ( "github.com/fsnotify/fsnotify" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sirupsen/logrus" ) @@ -117,7 +118,7 @@ func (gui *Gui) watchFilesForChanges() { } // only refresh if we're not already if !gui.State.IsRefreshingFiles { - _ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) + _ = gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) } // watch for errors diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index fa8cfa79c..b442a566f 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -12,6 +12,8 @@ import ( "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/filetree" "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -67,7 +69,7 @@ func (gui *Gui) filesRenderToMain() error { gui.resetMergeStateWithLock() - cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView) + cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.IgnoreWhitespaceInDiffView) refreshOpts := refreshMainOpts{main: &viewUpdateOpts{ title: gui.Tr.UnstagedChanges, @@ -76,7 +78,7 @@ func (gui *Gui) filesRenderToMain() error { if node.GetHasUnstagedChanges() { if node.GetHasStagedChanges() { - cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, true, gui.State.IgnoreWhitespaceInDiffView) + cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, true, gui.IgnoreWhitespaceInDiffView) refreshOpts.secondary = &viewUpdateOpts{ title: gui.Tr.StagedChanges, @@ -191,7 +193,7 @@ func (gui *Gui) enterFile(opts OnFocusOpts) error { return gui.switchToMerge() } if file.HasMergeConflicts { - return gui.createErrorPanel(gui.Tr.FileStagingRequirements) + return gui.PopupHandler.ErrorMsg(gui.Tr.FileStagingRequirements) } return gui.pushContext(gui.State.Contexts.Staging, opts) @@ -213,36 +215,36 @@ func (gui *Gui) handleFilePress() error { if file.HasUnstagedChanges { gui.logAction(gui.Tr.Actions.StageFile) if err := gui.Git.WorkingTree.StageFile(file.Name); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } } else { gui.logAction(gui.Tr.Actions.UnstageFile) if err := gui.Git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } } } else { // if any files within have inline merge conflicts we can't stage or unstage, // or it'll end up with those >>>>>> lines actually staged if node.GetHasInlineMergeConflicts() { - return gui.createErrorPanel(gui.Tr.ErrStageDirWithInlineMergeConflicts) + return gui.PopupHandler.ErrorMsg(gui.Tr.ErrStageDirWithInlineMergeConflicts) } if node.GetHasUnstagedChanges() { gui.logAction(gui.Tr.Actions.StageFile) if err := gui.Git.WorkingTree.StageFile(node.Path); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } } else { // pretty sure it doesn't matter that we're always passing true here gui.logAction(gui.Tr.Actions.UnstageFile) if err := gui.Git.WorkingTree.UnStageFile([]string{node.Path}, true); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } } } - if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil { return err } @@ -273,10 +275,10 @@ func (gui *Gui) handleStageAll() error { err = gui.Git.WorkingTree.StageAll() } if err != nil { - _ = gui.surfaceError(err) + _ = gui.PopupHandler.Error(err) } - if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil { return err } @@ -290,7 +292,7 @@ func (gui *Gui) handleIgnoreFile() error { } if node.GetPath() == ".gitignore" { - return gui.createErrorPanel("Cannot ignore .gitignore") + return gui.PopupHandler.ErrorMsg("Cannot ignore .gitignore") } unstageFiles := func() error { @@ -306,10 +308,10 @@ func (gui *Gui) handleIgnoreFile() error { } if node.GetIsTracked() { - return gui.ask(askOpts{ - title: gui.Tr.IgnoreTracked, - prompt: gui.Tr.IgnoreTrackedPrompt, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.IgnoreTracked, + Prompt: gui.Tr.IgnoreTrackedPrompt, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.IgnoreFile) // not 100% sure if this is necessary but I'll assume it is if err := unstageFiles(); err != nil { @@ -323,7 +325,7 @@ func (gui *Gui) handleIgnoreFile() error { if err := gui.Git.WorkingTree.Ignore(node.GetPath()); err != nil { return err } - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}) }, }) } @@ -335,16 +337,16 @@ func (gui *Gui) handleIgnoreFile() error { } if err := gui.Git.WorkingTree.Ignore(node.GetPath()); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}) } func (gui *Gui) handleWIPCommitPress() error { skipHookPrefix := gui.UserConfig.Git.SkipHookPrefix if skipHookPrefix == "" { - return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured) + return gui.PopupHandler.ErrorMsg(gui.Tr.SkipHookPrefixNotConfigured) } textArea := gui.Views.CommitMessage.TextArea @@ -381,11 +383,11 @@ func (gui *Gui) prepareFilesForCommit() error { func (gui *Gui) handleCommitPress() error { if err := gui.prepareFilesForCommit(); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } if gui.State.FileTreeViewModel.GetItemsLength() == 0 { - return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle) + return gui.PopupHandler.ErrorMsg(gui.Tr.NoFilesStagedTitle) } if len(gui.stagedFiles()) == 0 { @@ -403,7 +405,7 @@ func (gui *Gui) handleCommitPress() error { prefixReplace := commitPrefixConfig.Replace rgx, err := regexp.Compile(prefixPattern) if err != nil { - return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.LcCommitPrefixPatternError, err.Error())) + return gui.PopupHandler.ErrorMsg(fmt.Sprintf("%s: %s", gui.Tr.LcCommitPrefixPatternError, err.Error())) } prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace) gui.Views.CommitMessage.ClearTextArea() @@ -421,16 +423,16 @@ func (gui *Gui) handleCommitPress() error { } func (gui *Gui) promptToStageAllAndRetry(retry func() error) error { - return gui.ask(askOpts{ - title: gui.Tr.NoFilesStagedTitle, - prompt: gui.Tr.NoFilesStagedPrompt, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.NoFilesStagedTitle, + Prompt: gui.Tr.NoFilesStagedPrompt, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.StageAllFiles) if err := gui.Git.WorkingTree.StageAll(); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } if err := gui.refreshFilesAndSubmodules(); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return retry() @@ -440,7 +442,7 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error { func (gui *Gui) handleAmendCommitPress() error { if gui.State.FileTreeViewModel.GetItemsLength() == 0 { - return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle) + return gui.PopupHandler.ErrorMsg(gui.Tr.NoFilesStagedTitle) } if len(gui.stagedFiles()) == 0 { @@ -448,13 +450,13 @@ func (gui *Gui) handleAmendCommitPress() error { } if len(gui.State.Commits) == 0 { - return gui.createErrorPanel(gui.Tr.NoCommitToAmend) + return gui.PopupHandler.ErrorMsg(gui.Tr.NoCommitToAmend) } - return gui.ask(askOpts{ - title: strings.Title(gui.Tr.AmendLastCommit), - prompt: gui.Tr.SureToAmend, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: strings.Title(gui.Tr.AmendLastCommit), + Prompt: gui.Tr.SureToAmend, + HandleConfirm: func() error { cmdObj := gui.Git.Commit.AmendHeadCmdObj() gui.logAction(gui.Tr.Actions.AmendCommit) return gui.withGpgHandling(cmdObj, gui.Tr.AmendingStatus, nil) @@ -466,7 +468,7 @@ func (gui *Gui) handleAmendCommitPress() error { // their editor rather than via the popup panel func (gui *Gui) handleCommitEditorPress() error { if gui.State.FileTreeViewModel.GetItemsLength() == 0 { - return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle) + return gui.PopupHandler.ErrorMsg(gui.Tr.NoFilesStagedTitle) } if len(gui.stagedFiles()) == 0 { @@ -480,28 +482,29 @@ func (gui *Gui) handleCommitEditorPress() error { } func (gui *Gui) handleStatusFilterPressed() error { - menuItems := []*menuItem{ - { - displayString: gui.Tr.FilterStagedFiles, - onPress: func() error { - return gui.setStatusFiltering(filetree.DisplayStaged) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{ + Title: gui.Tr.FilteringMenuTitle, + Items: []*popup.MenuItem{ + { + DisplayString: gui.Tr.FilterStagedFiles, + OnPress: func() error { + return gui.setStatusFiltering(filetree.DisplayStaged) + }, + }, + { + DisplayString: gui.Tr.FilterUnstagedFiles, + OnPress: func() error { + return gui.setStatusFiltering(filetree.DisplayUnstaged) + }, + }, + { + DisplayString: gui.Tr.ResetCommitFilterState, + OnPress: func() error { + return gui.setStatusFiltering(filetree.DisplayAll) + }, }, }, - { - displayString: gui.Tr.FilterUnstagedFiles, - onPress: func() error { - return gui.setStatusFiltering(filetree.DisplayUnstaged) - }, - }, - { - displayString: gui.Tr.ResetCommitFilterState, - onPress: func() error { - return gui.setStatusFiltering(filetree.DisplayAll) - }, - }, - } - - return gui.createMenu(gui.Tr.FilteringMenuTitle, menuItems, createMenuOptions{showCancel: true}) + }) } func (gui *Gui) setStatusFiltering(filter filetree.FileTreeDisplayFilter) error { @@ -517,7 +520,7 @@ func (gui *Gui) editFile(filename string) error { func (gui *Gui) editFileAtLine(filename string, lineNumber int) error { cmdStr, err := gui.Git.File.GetEditCmdStr(filename, lineNumber) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.logAction(gui.Tr.Actions.EditFile) @@ -533,7 +536,7 @@ func (gui *Gui) handleFileEdit() error { } if node.File == nil { - return gui.createErrorPanel(gui.Tr.ErrCannotEditDirectory) + return gui.PopupHandler.ErrorMsg(gui.Tr.ErrCannotEditDirectory) } return gui.editFile(node.GetPath()) @@ -549,7 +552,7 @@ func (gui *Gui) handleFileOpen() error { } func (gui *Gui) handleRefreshFiles() error { - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}) } func (gui *Gui) refreshStateFiles() error { @@ -666,10 +669,10 @@ func (gui *Gui) refreshStateFiles() error { func (gui *Gui) promptToContinueRebase() error { gui.takeOverMergeConflictScrolling() - return gui.ask(askOpts{ - title: "continue", - prompt: gui.Tr.ConflictsResolved, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: "continue", + Prompt: gui.Tr.ConflictsResolved, + HandleConfirm: func() error { return gui.genericMergeCommand(REBASE_OPTION_CONTINUE) }, }) @@ -730,15 +733,15 @@ func (gui *Gui) handlePullFiles() error { if !currentBranch.IsTrackingRemote() { suggestedRemote := getSuggestedRemote(gui.State.Remotes) - return gui.prompt(promptOpts{ - title: gui.Tr.EnterUpstream, - initialContent: suggestedRemote + " " + currentBranch.Name, - findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "), - handleConfirm: func(upstream string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: gui.Tr.EnterUpstream, + InitialContent: suggestedRemote + " " + currentBranch.Name, + FindSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "), + HandleConfirm: func(upstream string) error { var upstreamBranch, upstreamRemote string split := strings.Split(upstream, " ") if len(split) != 2 { - return gui.createErrorPanel(gui.Tr.InvalidUpstream) + return gui.PopupHandler.ErrorMsg(gui.Tr.InvalidUpstream) } upstreamRemote = split[0] @@ -749,7 +752,7 @@ func (gui *Gui) handlePullFiles() error { if strings.Contains(errorMessage, "does not exist") { errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream) } - return gui.createErrorPanel(errorMessage) + return gui.PopupHandler.ErrorMsg(errorMessage) } return gui.pullFiles(PullFilesOptions{UpstreamRemote: upstreamRemote, UpstreamBranch: upstreamBranch, action: action}) }, @@ -767,14 +770,9 @@ type PullFilesOptions struct { } func (gui *Gui) pullFiles(opts PullFilesOptions) error { - if err := gui.createLoaderPanel(gui.Tr.PullWait); err != nil { - return err - } - - // TODO: this doesn't look like a good idea. Why the goroutine? - go utils.Safe(func() { _ = gui.pullWithLock(opts) }) - - return nil + return gui.PopupHandler.WithLoaderPanel(gui.Tr.PullWait, func() error { + return gui.pullWithLock(opts) + }) } func (gui *Gui) pullWithLock(opts PullFilesOptions) error { @@ -804,10 +802,7 @@ type pushOpts struct { } func (gui *Gui) push(opts pushOpts) error { - if err := gui.createLoaderPanel(gui.Tr.PushWait); err != nil { - return err - } - go utils.Safe(func() { + return gui.PopupHandler.WithLoaderPanel(gui.Tr.PushWait, func() error { gui.logAction(gui.Tr.Actions.Push) err := gui.Git.Sync.Push(git_commands.PushOpts{ Force: opts.force, @@ -816,28 +811,29 @@ func (gui *Gui) push(opts pushOpts) error { SetUpstream: opts.setUpstream, }) - if err != nil && !opts.force && strings.Contains(err.Error(), "Updates were rejected") { - forcePushDisabled := gui.UserConfig.Git.DisableForcePushing - if forcePushDisabled { - _ = gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled) - return - } - _ = gui.ask(askOpts{ - title: gui.Tr.ForcePush, - prompt: gui.Tr.ForcePushPrompt, - handleConfirm: func() error { - newOpts := opts - newOpts.force = true + if err != nil { + if !opts.force && strings.Contains(err.Error(), "Updates were rejected") { + forcePushDisabled := gui.UserConfig.Git.DisableForcePushing + if forcePushDisabled { + _ = gui.PopupHandler.ErrorMsg(gui.Tr.UpdatesRejectedAndForcePushDisabled) + return nil + } + _ = gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.ForcePush, + Prompt: gui.Tr.ForcePushPrompt, + HandleConfirm: func() error { + newOpts := opts + newOpts.force = true - return gui.push(newOpts) - }, - }) - return + return gui.push(newOpts) + }, + }) + return nil + } + _ = gui.PopupHandler.Error(err) } - gui.handleCredentialsPopup(err) - _ = gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) }) - return nil } func (gui *Gui) pushFiles() error { @@ -870,11 +866,11 @@ func (gui *Gui) pushFiles() error { if gui.Git.Config.GetPushToCurrent() { return gui.push(pushOpts{setUpstream: true}) } else { - return gui.prompt(promptOpts{ - title: gui.Tr.EnterUpstream, - initialContent: suggestedRemote + " " + currentBranch.Name, - findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "), - handleConfirm: func(upstream string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: gui.Tr.EnterUpstream, + InitialContent: suggestedRemote + " " + currentBranch.Name, + FindSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "), + HandleConfirm: func(upstream string) error { var upstreamBranch, upstreamRemote string split := strings.Split(upstream, " ") if len(split) == 2 { @@ -914,13 +910,13 @@ func getSuggestedRemote(remotes []*models.Remote) string { func (gui *Gui) requestToForcePush(opts pushOpts) error { forcePushDisabled := gui.UserConfig.Git.DisableForcePushing if forcePushDisabled { - return gui.createErrorPanel(gui.Tr.ForcePushDisabled) + return gui.PopupHandler.ErrorMsg(gui.Tr.ForcePushDisabled) } - return gui.ask(askOpts{ - title: gui.Tr.ForcePush, - prompt: gui.Tr.ForcePushPrompt, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.ForcePush, + Prompt: gui.Tr.ForcePushPrompt, + HandleConfirm: func() error { return gui.push(opts) }, }) @@ -950,16 +946,16 @@ func (gui *Gui) switchToMerge() error { func (gui *Gui) openFile(filename string) error { gui.logAction(gui.Tr.Actions.OpenFile) if err := gui.OSCommand.OpenFile(filename); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return nil } func (gui *Gui) handleCustomCommand() error { - return gui.prompt(promptOpts{ - title: gui.Tr.CustomCommand, - findSuggestionsFunc: gui.getCustomCommandsHistorySuggestionsFunc(), - handleConfirm: func(command string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: gui.Tr.CustomCommand, + FindSuggestionsFunc: gui.getCustomCommandsHistorySuggestionsFunc(), + HandleConfirm: func(command string) error { gui.Config.GetAppState().CustomCommandsHistory = utils.Limit( utils.Uniq( append(gui.Config.GetAppState().CustomCommandsHistory, command), @@ -981,24 +977,25 @@ func (gui *Gui) handleCustomCommand() error { } func (gui *Gui) handleCreateStashMenu() error { - menuItems := []*menuItem{ - { - displayString: gui.Tr.LcStashAllChanges, - onPress: func() error { - gui.logAction(gui.Tr.Actions.StashAllChanges) - return gui.handleStashSave(gui.Git.Stash.Save) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{ + Title: gui.Tr.LcStashOptions, + Items: []*popup.MenuItem{ + { + DisplayString: gui.Tr.LcStashAllChanges, + OnPress: func() error { + gui.logAction(gui.Tr.Actions.StashAllChanges) + return gui.handleStashSave(gui.Git.Stash.Save) + }, + }, + { + DisplayString: gui.Tr.LcStashStagedChanges, + OnPress: func() error { + gui.logAction(gui.Tr.Actions.StashStagedChanges) + return gui.handleStashSave(gui.Git.Stash.SaveStagedChanges) + }, }, }, - { - displayString: gui.Tr.LcStashStagedChanges, - onPress: func() error { - gui.logAction(gui.Tr.Actions.StashStagedChanges) - return gui.handleStashSave(gui.Git.Stash.SaveStagedChanges) - }, - }, - } - - return gui.createMenu(gui.Tr.LcStashOptions, menuItems, createMenuOptions{showCancel: true}) + }) } func (gui *Gui) handleStashChanges() error { @@ -1052,10 +1049,10 @@ func (gui *Gui) handleToggleFileTreeView() error { } func (gui *Gui) handleOpenMergeTool() error { - return gui.ask(askOpts{ - title: gui.Tr.MergeToolTitle, - prompt: gui.Tr.MergeToolPrompt, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.MergeToolTitle, + Prompt: gui.Tr.MergeToolPrompt, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.OpenMergeTool) return gui.runSubprocessWithSuspenseAndRefresh( gui.Git.WorkingTree.OpenMergeToolCmdObj(), @@ -1063,3 +1060,35 @@ func (gui *Gui) handleOpenMergeTool() error { }, }) } + +func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.LcResettingSubmoduleStatus, func() error { + gui.logAction(gui.Tr.Actions.ResetSubmodule) + + file := gui.fileForSubmodule(submodule) + if file != nil { + if err := gui.Git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { + return gui.PopupHandler.Error(err) + } + } + + if err := gui.Git.Submodule.Stash(submodule); err != nil { + return gui.PopupHandler.Error(err) + } + if err := gui.Git.Submodule.Reset(submodule); err != nil { + return gui.PopupHandler.Error(err) + } + + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.SUBMODULES}}) + }) +} + +func (gui *Gui) fileForSubmodule(submodule *models.SubmoduleConfig) *models.File { + for _, file := range gui.State.FileManager.GetAllFiles() { + if file.IsSubmodule([]*models.SubmoduleConfig{submodule}) { + return file + } + } + + return nil +} diff --git a/pkg/gui/filtering.go b/pkg/gui/filtering.go index 1f5c5032a..df007b69a 100644 --- a/pkg/gui/filtering.go +++ b/pkg/gui/filtering.go @@ -1,11 +1,16 @@ package gui +import ( + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + func (gui *Gui) validateNotInFilterMode() (bool, error) { if gui.State.Modes.Filtering.Active() { - err := gui.ask(askOpts{ - title: gui.Tr.MustExitFilterModeTitle, - prompt: gui.Tr.MustExitFilterModePrompt, - handleConfirm: gui.exitFilterMode, + err := gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.MustExitFilterModeTitle, + Prompt: gui.Tr.MustExitFilterModePrompt, + HandleConfirm: gui.exitFilterMode, }) return false, err @@ -23,7 +28,7 @@ func (gui *Gui) clearFiltering() error { gui.State.ScreenMode = SCREEN_NORMAL } - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{COMMITS}}) + return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}) } func (gui *Gui) setFiltering(path string) error { @@ -36,7 +41,7 @@ func (gui *Gui) setFiltering(path string) error { return err } - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{COMMITS}, then: func() { + return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}, Then: func() { gui.State.Contexts.BranchCommits.GetPanelState().SetSelectedLineIdx(0) }}) } diff --git a/pkg/gui/filtering_menu_panel.go b/pkg/gui/filtering_menu_panel.go index 2955f6e8d..dcdf1ec40 100644 --- a/pkg/gui/filtering_menu_panel.go +++ b/pkg/gui/filtering_menu_panel.go @@ -3,6 +3,8 @@ package gui import ( "fmt" "strings" + + "github.com/jesseduffield/lazygit/pkg/gui/popup" ) func (gui *Gui) handleCreateFilteringMenuPanel() error { @@ -20,24 +22,24 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error { } } - menuItems := []*menuItem{} + menuItems := []*popup.MenuItem{} if fileName != "" { - menuItems = append(menuItems, &menuItem{ - displayString: fmt.Sprintf("%s '%s'", gui.Tr.LcFilterBy, fileName), - onPress: func() error { + menuItems = append(menuItems, &popup.MenuItem{ + DisplayString: fmt.Sprintf("%s '%s'", gui.Tr.LcFilterBy, fileName), + OnPress: func() error { return gui.setFiltering(fileName) }, }) } - menuItems = append(menuItems, &menuItem{ - displayString: gui.Tr.LcFilterPathOption, - onPress: func() error { - return gui.prompt(promptOpts{ - findSuggestionsFunc: gui.getFilePathSuggestionsFunc(), - title: gui.Tr.EnterFileName, - handleConfirm: func(response string) error { + menuItems = append(menuItems, &popup.MenuItem{ + DisplayString: gui.Tr.LcFilterPathOption, + OnPress: func() error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + FindSuggestionsFunc: gui.getFilePathSuggestionsFunc(), + Title: gui.Tr.EnterFileName, + HandleConfirm: func(response string) error { return gui.setFiltering(strings.TrimSpace(response)) }, }) @@ -45,11 +47,11 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error { }) if gui.State.Modes.Filtering.Active() { - menuItems = append(menuItems, &menuItem{ - displayString: gui.Tr.LcExitFilterMode, - onPress: gui.clearFiltering, + menuItems = append(menuItems, &popup.MenuItem{ + DisplayString: gui.Tr.LcExitFilterMode, + OnPress: gui.clearFiltering, }) } - return gui.createMenu(gui.Tr.FilteringMenuTitle, menuItems, createMenuOptions{showCancel: true}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: gui.Tr.FilteringMenuTitle, Items: menuItems}) } diff --git a/pkg/gui/find_suggestions.go b/pkg/gui/find_suggestions.go index 343e087e8..75215d673 100644 --- a/pkg/gui/find_suggestions.go +++ b/pkg/gui/find_suggestions.go @@ -84,7 +84,7 @@ func (gui *Gui) getBranchNameSuggestionsFunc() func(string) []*types.Suggestion // Notably, unlike other suggestion functions we're not showing all the options // if nothing has been typed because there'll be too much to display efficiently func (gui *Gui) getFilePathSuggestionsFunc() func(string) []*types.Suggestion { - _ = gui.WithWaitingStatus(gui.Tr.LcLoadingFileSuggestions, func() error { + _ = gui.PopupHandler.WithWaitingStatus(gui.Tr.LcLoadingFileSuggestions, func() error { trie := patricia.NewTrie() // load every non-gitignored file in the repo ignore, err := gitignore.FromGit() diff --git a/pkg/gui/git_flow.go b/pkg/gui/git_flow.go index e89c7637c..ab58adfe8 100644 --- a/pkg/gui/git_flow.go +++ b/pkg/gui/git_flow.go @@ -3,6 +3,7 @@ package gui import ( "fmt" + "github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -13,16 +14,16 @@ func (gui *Gui) handleCreateGitFlowMenu() error { } if !gui.Git.Flow.GitFlowEnabled() { - return gui.createErrorPanel("You need to install git-flow and enable it in this repo to use git-flow features") + return gui.PopupHandler.ErrorMsg("You need to install git-flow and enable it in this repo to use git-flow features") } startHandler := func(branchType string) func() error { return func() error { title := utils.ResolvePlaceholderString(gui.Tr.NewGitFlowBranchPrompt, map[string]string{"branchType": branchType}) - return gui.prompt(promptOpts{ - title: title, - handleConfirm: func(name string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: title, + HandleConfirm: func(name string) error { gui.logAction(gui.Tr.Actions.GitFlowStart) return gui.runSubprocessWithSuspenseAndRefresh( gui.Git.Flow.StartCmdObj(branchType, name), @@ -32,39 +33,40 @@ func (gui *Gui) handleCreateGitFlowMenu() error { } } - menuItems := []*menuItem{ - { - // not localising here because it's one to one with the actual git flow commands - displayString: fmt.Sprintf("finish branch '%s'", branch.Name), - onPress: func() error { - return gui.gitFlowFinishBranch(branch.Name) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{ + Title: "git flow", + Items: []*popup.MenuItem{ + { + // not localising here because it's one to one with the actual git flow commands + DisplayString: fmt.Sprintf("finish branch '%s'", branch.Name), + OnPress: func() error { + return gui.gitFlowFinishBranch(branch.Name) + }, + }, + { + DisplayString: "start feature", + OnPress: startHandler("feature"), + }, + { + DisplayString: "start hotfix", + OnPress: startHandler("hotfix"), + }, + { + DisplayString: "start bugfix", + OnPress: startHandler("bugfix"), + }, + { + DisplayString: "start release", + OnPress: startHandler("release"), }, }, - { - displayString: "start feature", - onPress: startHandler("feature"), - }, - { - displayString: "start hotfix", - onPress: startHandler("hotfix"), - }, - { - displayString: "start bugfix", - onPress: startHandler("bugfix"), - }, - { - displayString: "start release", - onPress: startHandler("release"), - }, - } - - return gui.createMenu("git flow", menuItems, createMenuOptions{}) + }) } func (gui *Gui) gitFlowFinishBranch(branchName string) error { cmdObj, err := gui.Git.Flow.FinishCmdObj(branchName) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.logAction(gui.Tr.Actions.GitFlowFinish) diff --git a/pkg/gui/global_handlers.go b/pkg/gui/global_handlers.go index 22b43b6b7..1b4394519 100644 --- a/pkg/gui/global_handlers.go +++ b/pkg/gui/global_handlers.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -176,7 +177,7 @@ func (gui *Gui) scrollDownConfirmationPanel() error { } func (gui *Gui) handleRefresh() error { - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) } func (gui *Gui) handleMouseDownMain() error { @@ -218,10 +219,10 @@ func (gui *Gui) fetch() (err error) { err = gui.Git.Sync.Fetch(git_commands.FetchOptions{}) if err != nil && strings.Contains(err.Error(), "exit status 128") { - _ = gui.createErrorPanel(gui.Tr.PassUnameWrong) + _ = gui.PopupHandler.ErrorMsg(gui.Tr.PassUnameWrong) } - _ = gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC}) + _ = gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC}) return err } @@ -232,7 +233,7 @@ func (gui *Gui) backgroundFetch() (err error) { err = gui.Git.Sync.Fetch(git_commands.FetchOptions{Background: true}) - _ = gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC}) + _ = gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC}) return err } @@ -247,7 +248,7 @@ func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error { gui.logAction(gui.Tr.Actions.CopyToClipboard) if err := gui.OSCommand.CopyToClipboard(itemId); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } truncatedItemId := utils.TruncateWithEllipsis(strings.Replace(itemId, "\n", " ", -1), 50) diff --git a/pkg/gui/gpg.go b/pkg/gui/gpg.go index ca7e4b842..1384055bc 100644 --- a/pkg/gui/gpg.go +++ b/pkg/gui/gpg.go @@ -5,6 +5,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) // Currently there is a bug where if we switch to a subprocess from within @@ -23,7 +24,7 @@ func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, return err } } - if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}); err != nil { return err } @@ -34,7 +35,7 @@ func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, } func (gui *Gui) RunAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error { - return gui.WithWaitingStatus(waitingStatus, func() error { + return gui.PopupHandler.WithWaitingStatus(waitingStatus, func() error { cmdObj := gui.OSCommand.Cmd.NewShell(cmdObj.ToString()) cmdObj.AddEnvVars("TERM=dumb") cmdWriter := gui.getCmdWriter() @@ -46,8 +47,8 @@ func (gui *Gui) RunAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, on if _, err := cmd.Stdout.Write([]byte(fmt.Sprintf("%s\n", style.FgRed.Sprint(err.Error())))); err != nil { gui.Log.Error(err) } - _ = gui.refreshSidePanels(refreshOptions{mode: ASYNC}) - return gui.surfaceError( + _ = gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) + return gui.PopupHandler.Error( fmt.Errorf( gui.Tr.GitCommandFailed, gui.UserConfig.Keybinding.Universal.ExtrasMenu, ), @@ -60,6 +61,6 @@ func (gui *Gui) RunAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, on } } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) }) } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 981a81987..2bfd7f2ac 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -18,6 +18,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui/controllers" "github.com/jesseduffield/lazygit/pkg/gui/filetree" "github.com/jesseduffield/lazygit/pkg/gui/lbl" "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" @@ -75,11 +76,11 @@ type Gui struct { OSCommand *oscommands.OSCommand // this is the state of the GUI for the current repo - State *guiState + State *GuiRepoState // this is a mapping of repos to gui states, so that we can restore the original // gui state when returning from a subrepo - RepoStateMap map[Repo]*guiState + RepoStateMap map[Repo]*GuiRepoState Config config.AppConfigurer Updater *updates.Updater statusManager *statusManager @@ -101,7 +102,7 @@ type Gui struct { // when you enter into a submodule we'll append the superproject's path to this array // so that you can return to the superproject - RepoPathStack []string + RepoPathStack *utils.StringStack // this tells us whether our views have been initially set up ViewsSetup bool @@ -121,10 +122,21 @@ type Gui struct { suggestionsAsyncHandler *tasks.AsyncHandler - PopupHandler PopupHandler + PopupHandler popup.IPopupHandler IsNewRepo bool + Controllers Controllers + + // flag as to whether or not the diff view should ignore whitespace + IgnoreWhitespaceInDiffView bool + + // if this is true, we'll load our commits using `git log --all` + ShowWholeGitGraph bool + RetainOriginalDir bool + + PrevLayout PrevLayout + // this is the initial dir we are in upon opening lazygit. We hold onto this // in case we want to restore it before quitting for users who have set up // the feature for changing directory upon quit. @@ -134,6 +146,80 @@ type Gui struct { InitialDir string } +// we keep track of some stuff from one render to the next to see if certain +// things have changed +type PrevLayout struct { + Information string + MainWidth int + MainHeight int +} + +type GuiRepoState struct { + // the file panels (files and commit files) can render as a tree, so we have + // managers for them which handle rendering a flat list of files in tree form + FileTreeViewModel *filetree.FileTreeViewModel + CommitFileTreeViewModel *filetree.CommitFileTreeViewModel + Submodules []*models.SubmoduleConfig + Branches []*models.Branch + Commits []*models.Commit + StashEntries []*models.StashEntry + SubCommits []*models.Commit + Remotes []*models.Remote + RemoteBranches []*models.RemoteBranch + Tags []*models.Tag + // FilteredReflogCommits are the ones that appear in the reflog panel. + // when in filtering mode we only include the ones that match the given path + FilteredReflogCommits []*models.Commit + // ReflogCommits are the ones used by the branches panel to obtain recency values + // if we're not in filtering mode, CommitFiles and FilteredReflogCommits will be + // one and the same + ReflogCommits []*models.Commit + + // Suggestions will sometimes appear when typing into a prompt + Suggestions []*types.Suggestion + MenuItems []*popup.MenuItem + BisectInfo *git_commands.BisectInfo + Updating bool + Panels *panelStates + SplitMainPanel bool + MainContext ContextKey // used to keep the main and secondary views' contexts in sync + + IsRefreshingFiles bool + Searching searchingState + Ptmx *os.File + StartupStage StartupStage // Allows us to not load everything at once + + Modes Modes + + ContextManager ContextManager + Contexts ContextTree + ViewContextMap map[string]Context + ViewTabContextMap map[string][]tabContext + + // WindowViewNameMap is a mapping of windows to the current view of that window. + // Some views move between windows for example the commitFiles view and when cycling through + // side windows we need to know which view to give focus to for a given window + WindowViewNameMap map[string]string + + // tells us whether we've set up our views for the current repo. We'll need to + // do this whenever we switch back and forth between repos to get the views + // back in sync with the repo state + ViewsSetup bool + + // for displaying suggestions while typing in a file name + FilesTrie *patricia.Trie + + // this is the message of the last failed commit attempt + failedCommitMessage string + + // TODO: move these into the gui struct + ScreenMode WindowMaximisation +} + +type Controllers struct { + Submodules *controllers.SubmodulesController +} + type listPanelState struct { SelectedLineIdx int } @@ -296,75 +382,6 @@ type guiMutexes struct { SubprocessMutex sync.Mutex } -type guiState struct { - // the file panels (files and commit files) can render as a tree, so we have - // managers for them which handle rendering a flat list of files in tree form - FileTreeViewModel *filetree.FileTreeViewModel - CommitFileTreeViewModel *filetree.CommitFileTreeViewModel - - Submodules []*models.SubmoduleConfig - Branches []*models.Branch - Commits []*models.Commit - StashEntries []*models.StashEntry - // Suggestions will sometimes appear when typing into a prompt - Suggestions []*types.Suggestion - // FilteredReflogCommits are the ones that appear in the reflog panel. - // when in filtering mode we only include the ones that match the given path - FilteredReflogCommits []*models.Commit - // ReflogCommits are the ones used by the branches panel to obtain recency values - // if we're not in filtering mode, CommitFiles and FilteredReflogCommits will be - // one and the same - ReflogCommits []*models.Commit - SubCommits []*models.Commit - Remotes []*models.Remote - RemoteBranches []*models.RemoteBranch - Tags []*models.Tag - MenuItems []*menuItem - BisectInfo *git_commands.BisectInfo - - Updating bool - Panels *panelStates - SplitMainPanel bool - MainContext ContextKey // used to keep the main and secondary views' contexts in sync - RetainOriginalDir bool - IsRefreshingFiles bool - Searching searchingState - // if this is true, we'll load our commits using `git log --all` - ShowWholeGitGraph bool - ScreenMode WindowMaximisation - Ptmx *os.File - PrevMainWidth int - PrevMainHeight int - OldInformation string - StartupStage StartupStage // Allows us to not load everything at once - - Modes Modes - - ContextManager ContextManager - Contexts ContextTree - ViewContextMap map[string]Context - ViewTabContextMap map[string][]tabContext - - // WindowViewNameMap is a mapping of windows to the current view of that window. - // Some views move between windows for example the commitFiles view and when cycling through - // side windows we need to know which view to give focus to for a given window - WindowViewNameMap map[string]string - - // tells us whether we've set up our views for the current repo. We'll need to - // do this whenever we switch back and forth between repos to get the views - // back in sync with the repo state - ViewsSetup bool - - // flag as to whether or not the diff view should ignore whitespace - IgnoreWhitespaceInDiffView bool - - // for displaying suggestions while typing in a file name - FilesTrie *patricia.Trie - - // this is the message of the last failed commit attempt - failedCommitMessage string -} - // reuseState determines if we pull the repo state from our repo state map or // just re-initialize it. For now we're only re-using state when we're going // in and out of submodules, for the sake of having the cursor back on the submodule @@ -400,14 +417,13 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) { initialContext = contexts.BranchCommits } - gui.State = &guiState{ + gui.State = &GuiRepoState{ FileTreeViewModel: filetree.NewFileTreeViewModel(make([]*models.File, 0), gui.Log, showTree), CommitFileTreeViewModel: filetree.NewCommitFileTreeViewModel(make([]*models.CommitFile, 0), gui.Log, showTree), Commits: make([]*models.Commit, 0), FilteredReflogCommits: make([]*models.Commit, 0), ReflogCommits: make([]*models.Commit, 0), StashEntries: make([]*models.StashEntry, 0), - BisectInfo: gui.Git.Bisect.GetInfo(), Panels: &panelStates{ // TODO: work out why some of these are -1 and some are 0. Last time I checked there was a good reason but I'm less certain now Files: &filePanelState{listPanelState{SelectedLineIdx: -1}}, @@ -446,6 +462,21 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) { gui.RepoStateMap[Repo(currentDir)] = gui.State } +type guiCommon struct { + gui *Gui + popup.IPopupHandler +} + +var _ controllers.IGuiCommon = &guiCommon{} + +func (self *guiCommon) LogAction(msg string) { + self.gui.logAction(msg) +} + +func (self *guiCommon) Refresh(opts types.RefreshOptions) error { + return self.gui.refreshSidePanels(opts) +} + // for now the split view will always be on // NewGui builds a new gui handler func NewGui( @@ -464,8 +495,8 @@ func NewGui( statusManager: &statusManager{}, viewBufferManagerMap: map[string]*tasks.ViewBufferManager{}, showRecentRepos: showRecentRepos, - RepoPathStack: []string{}, - RepoStateMap: map[Repo]*guiState{}, + RepoPathStack: &utils.StringStack{}, + RepoStateMap: map[Repo]*GuiRepoState{}, CmdLog: []string{}, suggestionsAsyncHandler: tasks.NewAsyncHandler(), @@ -501,11 +532,31 @@ func NewGui( gui.watchFilesForChanges() - gui.PopupHandler = &RealPopupHandler{gui: gui} + gui.PopupHandler = popup.NewPopupHandler( + cmn, + gui.createPopupPanel, + func() error { return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) }, + func() error { return gui.closeConfirmationPrompt(false) }, + gui.createMenu, + gui.withWaitingStatus, + ) authors.SetCustomAuthors(gui.UserConfig.Gui.AuthorColors) presentation.SetCustomBranches(gui.UserConfig.Gui.BranchColors) + guiCommon := &guiCommon{gui: gui, IPopupHandler: gui.PopupHandler} + controllerCommon := &controllers.ControllerCommon{IGuiCommon: guiCommon, Common: cmn} + + gui.Controllers = Controllers{ + Submodules: controllers.NewSubmodulesController( + controllerCommon, + gui.enterSubmodule, + gui.Git, + gui.State.Submodules, + gui.getSelectedSubmodule, + ), + } + return gui, nil } @@ -601,7 +652,7 @@ func (gui *Gui) RunAndHandleError() error { switch err { case gocui.ErrQuit: - if gui.State.RetainOriginalDir { + if gui.RetainOriginalDir { if err := gui.recordDirectory(gui.InitialDir); err != nil { return err } @@ -633,7 +684,7 @@ func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess oscommands.ICmdOb return err } - if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}); err != nil { return err } @@ -654,7 +705,7 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool, } if err := gui.g.Suspend(); err != nil { - return false, gui.surfaceError(err) + return false, gui.PopupHandler.Error(err) } gui.PauseBackgroundThreads = true @@ -668,7 +719,7 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool, gui.PauseBackgroundThreads = false if cmdErr != nil { - return false, gui.surfaceError(cmdErr) + return false, gui.PopupHandler.Error(cmdErr) } return true, nil @@ -703,7 +754,7 @@ func (gui *Gui) loadNewRepo() error { return err } - if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}); err != nil { return err } @@ -723,7 +774,7 @@ func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) { task := task go utils.Safe(func() { if err := task(done); err != nil { - _ = gui.surfaceError(err) + _ = gui.PopupHandler.Error(err) } }) @@ -740,11 +791,11 @@ func (gui *Gui) showIntroPopupMessage(done chan struct{}) error { return gui.Config.SaveAppState() } - return gui.ask(askOpts{ - title: "", - prompt: gui.Tr.IntroPopupMessage, - handleConfirm: onConfirm, - handleClose: onConfirm, + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: "", + Prompt: gui.Tr.IntroPopupMessage, + HandleConfirm: onConfirm, + HandleClose: onConfirm, }) } @@ -775,9 +826,9 @@ func (gui *Gui) startBackgroundFetch() { } err := gui.backgroundFetch() if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew { - _ = gui.ask(askOpts{ - title: gui.Tr.NoAutomaticGitFetchTitle, - prompt: gui.Tr.NoAutomaticGitFetchBody, + _ = gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.NoAutomaticGitFetchTitle, + Prompt: gui.Tr.NoAutomaticGitFetchBody, }) } else { gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error { diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index fa2e0e49b..644e4560b 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -9,28 +9,9 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/constants" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) -// Binding - a keybinding mapping a key and modifier to a handler. The keypress -// is only handled if the given view has focus, or handled globally if the view -// is "" -type Binding struct { - ViewName string - Contexts []string - Handler func() error - Key interface{} // FIXME: find out how to get `gocui.Key | rune` - Modifier gocui.Modifier - Description string - Alternative string - Tag string // e.g. 'navigation'. Used for grouping things in the cheatsheet - OpensMenu bool -} - -// GetDisplayStrings returns the display string of a file -func (b *Binding) GetDisplayStrings(isFocused bool) []string { - return []string{GetKeyDisplay(b.Key), b.Description} -} - var keyMapReversed = map[gocui.Key]string{ gocui.KeyF1: "f1", gocui.KeyF2: "f2", @@ -203,10 +184,10 @@ func (gui *Gui) getKey(key string) interface{} { } // GetInitialKeybindings is a function. -func (gui *Gui) GetInitialKeybindings() []*Binding { +func (gui *Gui) GetInitialKeybindings() []*types.Binding { config := gui.UserConfig.Keybinding - bindings := []*Binding{ + bindings := []*types.Binding{ { ViewName: "", Key: gui.getKey(config.Universal.Quit), @@ -1713,57 +1694,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleCopySelectedSideContextItemToClipboard, Description: gui.Tr.LcCopySubmoduleNameToClipboard, }, - { - ViewName: "files", - Contexts: []string{string(SUBMODULES_CONTEXT_KEY)}, - Key: gui.getKey(config.Universal.GoInto), - Handler: gui.forSubmodule(gui.handleSubmoduleEnter), - Description: gui.Tr.LcEnterSubmodule, - }, - { - ViewName: "files", - Contexts: []string{string(SUBMODULES_CONTEXT_KEY)}, - Key: gui.getKey(config.Universal.Remove), - Handler: gui.forSubmodule(gui.removeSubmodule), - Description: gui.Tr.LcRemoveSubmodule, - OpensMenu: true, - }, - { - ViewName: "files", - Contexts: []string{string(SUBMODULES_CONTEXT_KEY)}, - Key: gui.getKey(config.Submodules.Update), - Handler: gui.forSubmodule(gui.handleUpdateSubmodule), - Description: gui.Tr.LcSubmoduleUpdate, - }, - { - ViewName: "files", - Contexts: []string{string(SUBMODULES_CONTEXT_KEY)}, - Key: gui.getKey(config.Universal.New), - Handler: gui.handleAddSubmodule, - Description: gui.Tr.LcAddSubmodule, - }, - { - ViewName: "files", - Contexts: []string{string(SUBMODULES_CONTEXT_KEY)}, - Key: gui.getKey(config.Universal.Edit), - Handler: gui.forSubmodule(gui.handleEditSubmoduleUrl), - Description: gui.Tr.LcEditSubmoduleUrl, - }, - { - ViewName: "files", - Contexts: []string{string(SUBMODULES_CONTEXT_KEY)}, - Key: gui.getKey(config.Submodules.Init), - Handler: gui.forSubmodule(gui.handleSubmoduleInit), - Description: gui.Tr.LcInitSubmodule, - }, - { - ViewName: "files", - Contexts: []string{string(SUBMODULES_CONTEXT_KEY)}, - Key: gui.getKey(config.Submodules.BulkMenu), - Handler: gui.handleBulkSubmoduleActionsMenu, - Description: gui.Tr.LcViewBulkSubmoduleOptions, - OpensMenu: true, - }, { ViewName: "files", Contexts: []string{string(FILES_CONTEXT_KEY)}, @@ -1841,8 +1771,28 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { }, } + type ContextKeybindings struct { + contextKey ContextKey + viewName string + bindings []*types.Binding + } + + for _, contextKeybindings := range []ContextKeybindings{ + { + contextKey: SUBMODULES_CONTEXT_KEY, + viewName: "files", + bindings: gui.Controllers.Submodules.Keybindings(gui.getKey, config), + }, + } { + for _, binding := range contextKeybindings.bindings { + binding.Contexts = []string{string(contextKeybindings.contextKey)} + binding.ViewName = contextKeybindings.viewName + bindings = append(bindings, binding) + } + } + for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} { - bindings = append(bindings, []*Binding{ + bindings = append(bindings, []*types.Binding{ {ViewName: viewName, Key: gui.getKey(config.Universal.PrevBlock), Modifier: gocui.ModNone, Handler: gui.previousSideWindow}, {ViewName: viewName, Key: gui.getKey(config.Universal.NextBlock), Modifier: gocui.ModNone, Handler: gui.nextSideWindow}, {ViewName: viewName, Key: gui.getKey(config.Universal.PrevBlockAlt), Modifier: gocui.ModNone, Handler: gui.previousSideWindow}, @@ -1859,7 +1809,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { log.Fatal("Jump to block keybindings cannot be set. Exactly 5 keybindings must be supplied.") } else { for i, window := range windows { - bindings = append(bindings, &Binding{ + bindings = append(bindings, &types.Binding{ ViewName: "", Key: gui.getKey(config.Universal.JumpToBlock[i]), Modifier: gocui.ModNone, @@ -1868,7 +1818,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { } for viewName := range gui.State.Contexts.initialViewTabContextMap() { - bindings = append(bindings, []*Binding{ + bindings = append(bindings, []*types.Binding{ { ViewName: viewName, Key: gui.getKey(config.Universal.NextTab), diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index c5872654a..3af9ea9af 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -234,9 +234,9 @@ func (gui *Gui) layout(g *gocui.Gui) error { // if the commit files view is the view to be displayed for its window, we'll display it gui.Views.CommitFiles.Visible = gui.getViewNameForWindow(gui.State.Contexts.CommitFiles.GetWindowName()) == "commitFiles" - if gui.State.OldInformation != informationStr { + if gui.PrevLayout.Information != informationStr { gui.setViewContent(gui.Views.Information, informationStr) - gui.State.OldInformation = informationStr + gui.PrevLayout.Information = informationStr } if !gui.ViewsSetup { @@ -277,9 +277,9 @@ func (gui *Gui) layout(g *gocui.Gui) error { gui.Views.Main.SetOnSelectItem(gui.onSelectItemWrapper(gui.handlelineByLineNavigateTo)) mainViewWidth, mainViewHeight := gui.Views.Main.Size() - if mainViewWidth != gui.State.PrevMainWidth || mainViewHeight != gui.State.PrevMainHeight { - gui.State.PrevMainWidth = mainViewWidth - gui.State.PrevMainHeight = mainViewHeight + if mainViewWidth != gui.PrevLayout.MainWidth || mainViewHeight != gui.PrevLayout.MainHeight { + gui.PrevLayout.MainWidth = mainViewWidth + gui.PrevLayout.MainHeight = mainViewHeight if err := gui.onResize(); err != nil { return err } diff --git a/pkg/gui/line_by_line_panel.go b/pkg/gui/line_by_line_panel.go index a033aa0d7..e851bc821 100644 --- a/pkg/gui/line_by_line_panel.go +++ b/pkg/gui/line_by_line_panel.go @@ -89,7 +89,7 @@ func (gui *Gui) copySelectedToClipboard() error { gui.logAction(gui.Tr.Actions.CopySelectedTextToClipboard) if err := gui.OSCommand.CopyToClipboard(selected); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return nil diff --git a/pkg/gui/list_context_config.go b/pkg/gui/list_context_config.go index 7ddd56e25..c40cade2c 100644 --- a/pkg/gui/list_context_config.go +++ b/pkg/gui/list_context_config.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) func (gui *Gui) menuListContext() IListContext { @@ -391,15 +392,15 @@ func (gui *Gui) getListContexts() []IListContext { } } -func (gui *Gui) getListContextKeyBindings() []*Binding { - bindings := make([]*Binding, 0) +func (gui *Gui) getListContextKeyBindings() []*types.Binding { + bindings := make([]*types.Binding, 0) keybindingConfig := gui.UserConfig.Keybinding for _, listContext := range gui.getListContexts() { listContext := listContext - bindings = append(bindings, []*Binding{ + bindings = append(bindings, []*types.Binding{ {ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine}, {ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine}, {ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine}, @@ -423,7 +424,7 @@ func (gui *Gui) getListContextKeyBindings() []*Binding { gotoBottomHandler = gui.handleGotoBottomForCommitsPanel } - bindings = append(bindings, []*Binding{ + bindings = append(bindings, []*types.Binding{ { ViewName: listContext.GetViewName(), Contexts: []string{string(listContext.GetKey())}, diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index b32b3bf44..f1bc558c4 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -3,31 +3,12 @@ package gui import ( "errors" "fmt" - "strings" + "github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/utils" ) -type menuItem struct { - displayString string - displayStrings []string - onPress func() error - // only applies when displayString is used - opensMenu bool -} - -// every item in a list context needs an ID -func (i *menuItem) ID() string { - if i.displayString != "" { - return i.displayString - } - - return strings.Join(i.displayStrings, "-") -} - -// specific functions - func (gui *Gui) getMenuOptions() map[string]string { keybindingConfig := gui.UserConfig.Keybinding @@ -42,37 +23,34 @@ func (gui *Gui) handleMenuClose() error { return gui.returnFromContext() } -type createMenuOptions struct { - showCancel bool -} - -func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions createMenuOptions) error { - if createMenuOptions.showCancel { +// note: items option is mutated by this function +func (gui *Gui) createMenu(opts popup.CreateMenuOptions) error { + if !opts.HideCancel { // this is mutative but I'm okay with that for now - items = append(items, &menuItem{ - displayStrings: []string{gui.Tr.LcCancel}, - onPress: func() error { + opts.Items = append(opts.Items, &popup.MenuItem{ + DisplayStrings: []string{gui.Tr.LcCancel}, + OnPress: func() error { return nil }, }) } - gui.State.MenuItems = items + gui.State.MenuItems = opts.Items - stringArrays := make([][]string, len(items)) - for i, item := range items { - if item.opensMenu && item.displayStrings != nil { + stringArrays := make([][]string, len(opts.Items)) + for i, item := range opts.Items { + if item.OpensMenu && item.DisplayStrings != nil { return errors.New("Message for the developer of this app: you've set opensMenu with displaystrings on the menu panel. Bad developer!. Apologies, user") } - if item.displayStrings == nil { - styledStr := item.displayString - if item.opensMenu { + if item.DisplayStrings == nil { + styledStr := item.DisplayString + if item.OpensMenu { styledStr = opensMenuStyle(styledStr) } stringArrays[i] = []string{styledStr} } else { - stringArrays[i] = item.displayStrings + stringArrays[i] = item.DisplayStrings } } @@ -80,7 +58,7 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(false, list) menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0) - menuView.Title = title + menuView.Title = opts.Title menuView.FgColor = theme.GocuiDefaultTextColor menuView.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error { return nil @@ -97,7 +75,7 @@ func (gui *Gui) onMenuPress() error { return err } - if err := gui.State.MenuItems[selectedLine].onPress(); err != nil { + if err := gui.State.MenuItems[selectedLine].OnPress(); err != nil { return err } diff --git a/pkg/gui/merge_panel.go b/pkg/gui/merge_panel.go index dbd5b3be7..9f46c177c 100644 --- a/pkg/gui/merge_panel.go +++ b/pkg/gui/merge_panel.go @@ -9,6 +9,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) func (gui *Gui) handleSelectPrevConflictHunk() error { @@ -189,7 +190,7 @@ func (gui *Gui) getMergingOptions() map[string]string { } func (gui *Gui) handleEscapeMerge() error { - if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil { return err } @@ -199,7 +200,7 @@ func (gui *Gui) handleEscapeMerge() error { func (gui *Gui) onLastConflictResolved() error { // as part of refreshing files, we handle the situation where a file has had // its merge conflicts resolved. - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{mode: types.ASYNC, scope: []types.RefreshableView{types.FILES}}) } func (gui *Gui) resetMergeState() { diff --git a/pkg/gui/options_menu_panel.go b/pkg/gui/options_menu_panel.go index 12507597f..f02f96a95 100644 --- a/pkg/gui/options_menu_panel.go +++ b/pkg/gui/options_menu_panel.go @@ -4,13 +4,15 @@ import ( "strings" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) -func (gui *Gui) getBindings(v *gocui.View) []*Binding { +func (gui *Gui) getBindings(v *gocui.View) []*types.Binding { var ( - bindingsGlobal, bindingsPanel []*Binding + bindingsGlobal, bindingsPanel []*types.Binding ) bindings := append(gui.GetCustomCommandKeybindings(), gui.GetInitialKeybindings()...) @@ -30,11 +32,11 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding { // append dummy element to have a separator between // panel and global keybindings - bindingsPanel = append(bindingsPanel, &Binding{}) + bindingsPanel = append(bindingsPanel, &types.Binding{}) return append(bindingsPanel, bindingsGlobal...) } -func (gui *Gui) displayDescription(binding *Binding) string { +func (gui *Gui) displayDescription(binding *types.Binding) string { if binding.OpensMenu { return opensMenuStyle(binding.Description) } @@ -54,13 +56,13 @@ func (gui *Gui) handleCreateOptionsMenu() error { bindings := gui.getBindings(view) - menuItems := make([]*menuItem, len(bindings)) + menuItems := make([]*popup.MenuItem, len(bindings)) for i, binding := range bindings { binding := binding // note to self, never close over loop variables - menuItems[i] = &menuItem{ - displayStrings: []string{GetKeyDisplay(binding.Key), gui.displayDescription(binding)}, - onPress: func() error { + menuItems[i] = &popup.MenuItem{ + DisplayStrings: []string{GetKeyDisplay(binding.Key), gui.displayDescription(binding)}, + OnPress: func() error { if binding.Key == nil { return nil } @@ -72,5 +74,9 @@ func (gui *Gui) handleCreateOptionsMenu() error { } } - return gui.createMenu(strings.Title(gui.Tr.LcMenu), menuItems, createMenuOptions{}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{ + Title: strings.Title(gui.Tr.LcMenu), + Items: menuItems, + HideCancel: true, + }) } diff --git a/pkg/gui/patch_options_panel.go b/pkg/gui/patch_options_panel.go index c9a3defce..915572c16 100644 --- a/pkg/gui/patch_options_panel.go +++ b/pkg/gui/patch_options_panel.go @@ -4,41 +4,43 @@ import ( "fmt" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) func (gui *Gui) handleCreatePatchOptionsMenu() error { if !gui.Git.Patch.PatchManager.Active() { - return gui.createErrorPanel(gui.Tr.NoPatchError) + return gui.PopupHandler.ErrorMsg(gui.Tr.NoPatchError) } - menuItems := []*menuItem{ + menuItems := []*popup.MenuItem{ { - displayString: "reset patch", - onPress: gui.handleResetPatch, + DisplayString: "reset patch", + OnPress: gui.handleResetPatch, }, { - displayString: "apply patch", - onPress: func() error { return gui.handleApplyPatch(false) }, + DisplayString: "apply patch", + OnPress: func() error { return gui.handleApplyPatch(false) }, }, { - displayString: "apply patch in reverse", - onPress: func() error { return gui.handleApplyPatch(true) }, + DisplayString: "apply patch in reverse", + OnPress: func() error { return gui.handleApplyPatch(true) }, }, } if gui.Git.Patch.PatchManager.CanRebase && gui.Git.Status.WorkingTreeState() == enums.REBASE_MODE_NONE { - menuItems = append(menuItems, []*menuItem{ + menuItems = append(menuItems, []*popup.MenuItem{ { - displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.Git.Patch.PatchManager.To), - onPress: gui.handleDeletePatchFromCommit, + DisplayString: fmt.Sprintf("remove patch from original commit (%s)", gui.Git.Patch.PatchManager.To), + OnPress: gui.handleDeletePatchFromCommit, }, { - displayString: "move patch out into index", - onPress: gui.handleMovePatchIntoWorkingTree, + DisplayString: "move patch out into index", + OnPress: gui.handleMovePatchIntoWorkingTree, }, { - displayString: "move patch into new commit", - onPress: gui.handlePullPatchIntoNewCommit, + DisplayString: "move patch into new commit", + OnPress: gui.handlePullPatchIntoNewCommit, }, }...) @@ -49,10 +51,10 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error { menuItems = append( menuItems[:1], append( - []*menuItem{ + []*popup.MenuItem{ { - displayString: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha), - onPress: gui.handleMovePatchToSelectedCommit, + DisplayString: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha), + OnPress: gui.handleMovePatchToSelectedCommit, }, }, menuItems[1:]..., )..., @@ -61,7 +63,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error { } } - return gui.createMenu(gui.Tr.PatchOptionsTitle, menuItems, createMenuOptions{showCancel: true}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: gui.Tr.PatchOptionsTitle, Items: menuItems}) } func (gui *Gui) getPatchCommitIndex() int { @@ -75,7 +77,7 @@ func (gui *Gui) getPatchCommitIndex() int { func (gui *Gui) validateNormalWorkingTreeState() (bool, error) { if gui.Git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { - return false, gui.createErrorPanel(gui.Tr.CantPatchWhileRebasingError) + return false, gui.PopupHandler.ErrorMsg(gui.Tr.CantPatchWhileRebasingError) } return true, nil } @@ -96,7 +98,7 @@ func (gui *Gui) handleDeletePatchFromCommit() error { return err } - return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { commitIndex := gui.getPatchCommitIndex() gui.logAction(gui.Tr.Actions.RemovePatchFromCommit) err := gui.Git.Patch.DeletePatchesFromCommit(gui.State.Commits, commitIndex) @@ -113,7 +115,7 @@ func (gui *Gui) handleMovePatchToSelectedCommit() error { return err } - return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { commitIndex := gui.getPatchCommitIndex() gui.logAction(gui.Tr.Actions.MovePatchToSelectedCommit) err := gui.Git.Patch.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx) @@ -131,7 +133,7 @@ func (gui *Gui) handleMovePatchIntoWorkingTree() error { } pull := func(stash bool) error { - return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { commitIndex := gui.getPatchCommitIndex() gui.logAction(gui.Tr.Actions.MovePatchIntoIndex) err := gui.Git.Patch.MovePatchIntoIndex(gui.State.Commits, commitIndex, stash) @@ -140,10 +142,10 @@ func (gui *Gui) handleMovePatchIntoWorkingTree() error { } if len(gui.trackedFiles()) > 0 { - return gui.ask(askOpts{ - title: gui.Tr.MustStashTitle, - prompt: gui.Tr.MustStashWarning, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.MustStashTitle, + Prompt: gui.Tr.MustStashWarning, + HandleConfirm: func() error { return pull(true) }, }) @@ -161,7 +163,7 @@ func (gui *Gui) handlePullPatchIntoNewCommit() error { return err } - return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { commitIndex := gui.getPatchCommitIndex() gui.logAction(gui.Tr.Actions.MovePatchIntoNewCommit) err := gui.Git.Patch.PullPatchIntoNewCommit(gui.State.Commits, commitIndex) @@ -180,9 +182,9 @@ func (gui *Gui) handleApplyPatch(reverse bool) error { } gui.logAction(action) if err := gui.Git.Patch.PatchManager.ApplyPatches(reverse); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) } func (gui *Gui) handleResetPatch() error { diff --git a/pkg/gui/popup/popup_handler.go b/pkg/gui/popup/popup_handler.go new file mode 100644 index 000000000..adc276d5f --- /dev/null +++ b/pkg/gui/popup/popup_handler.go @@ -0,0 +1,223 @@ +package popup + +import ( + "strings" + "sync" + + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/common" + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type IPopupHandler interface { + ErrorMsg(message string) error + Error(err error) error + Ask(opts AskOpts) error + Prompt(opts PromptOpts) error + WithLoaderPanel(message string, f func() error) error + WithWaitingStatus(message string, f func() error) error + Menu(opts CreateMenuOptions) error +} + +type CreateMenuOptions struct { + Title string + Items []*MenuItem + HideCancel bool +} + +type CreatePopupPanelOpts struct { + HasLoader bool + Editable bool + Title string + Prompt string + HandleConfirm func() error + HandleConfirmPrompt func(string) error + HandleClose func() error + + // when HandlersManageFocus is true, do not return from the confirmation context automatically. It's expected that the handlers will manage focus, whether that means switching to another context, or manually returning the context. + HandlersManageFocus bool + + FindSuggestionsFunc func(string) []*types.Suggestion +} + +type AskOpts struct { + Title string + Prompt string + HandleConfirm func() error + HandleClose func() error + HandlersManageFocus bool +} + +type PromptOpts struct { + Title string + InitialContent string + FindSuggestionsFunc func(string) []*types.Suggestion + HandleConfirm func(string) error +} + +type MenuItem struct { + DisplayString string + DisplayStrings []string + OnPress func() error + // only applies when displayString is used + OpensMenu bool +} + +type RealPopupHandler struct { + *common.Common + index int + sync.Mutex + createPopupPanelFn func(CreatePopupPanelOpts) error + onErrorFn func() error + closePopupFn func() error + createMenuFn func(CreateMenuOptions) error + withWaitingStatusFn func(message string, f func() error) error +} + +var _ IPopupHandler = &RealPopupHandler{} + +func NewPopupHandler( + common *common.Common, + createPopupPanelFn func(CreatePopupPanelOpts) error, + onErrorFn func() error, + closePopupFn func() error, + createMenuFn func(CreateMenuOptions) error, + withWaitingStatusFn func(message string, f func() error) error, +) *RealPopupHandler { + return &RealPopupHandler{ + Common: common, + index: 0, + createPopupPanelFn: createPopupPanelFn, + onErrorFn: onErrorFn, + closePopupFn: closePopupFn, + createMenuFn: createMenuFn, + withWaitingStatusFn: withWaitingStatusFn, + } +} + +func (self *RealPopupHandler) Menu(opts CreateMenuOptions) error { + return self.createMenuFn(opts) +} + +func (self *RealPopupHandler) WithWaitingStatus(message string, f func() error) error { + return self.withWaitingStatusFn(message, f) +} + +func (self *RealPopupHandler) Error(err error) error { + if err == gocui.ErrQuit { + return err + } + + return self.ErrorMsg(err.Error()) +} + +func (self *RealPopupHandler) ErrorMsg(message string) error { + self.Lock() + self.index++ + self.Unlock() + + coloredMessage := style.FgRed.Sprint(strings.TrimSpace(message)) + if err := self.onErrorFn(); err != nil { + return err + } + + return self.Ask(AskOpts{ + Title: self.Tr.Error, + Prompt: coloredMessage, + }) +} + +func (self *RealPopupHandler) Ask(opts AskOpts) error { + self.Lock() + self.index++ + self.Unlock() + + return self.createPopupPanelFn(CreatePopupPanelOpts{ + Title: opts.Title, + Prompt: opts.Prompt, + HandleConfirm: opts.HandleConfirm, + HandleClose: opts.HandleClose, + HandlersManageFocus: opts.HandlersManageFocus, + }) +} + +func (self *RealPopupHandler) Prompt(opts PromptOpts) error { + self.Lock() + self.index++ + self.Unlock() + + return self.createPopupPanelFn(CreatePopupPanelOpts{ + Title: opts.Title, + Prompt: opts.InitialContent, + Editable: true, + HandleConfirmPrompt: opts.HandleConfirm, + FindSuggestionsFunc: opts.FindSuggestionsFunc, + }) +} + +func (self *RealPopupHandler) WithLoaderPanel(message string, f func() error) error { + index := 0 + self.Lock() + self.index++ + index = self.index + self.Unlock() + + err := self.createPopupPanelFn(CreatePopupPanelOpts{ + Prompt: message, + HasLoader: true, + }) + if err != nil { + self.Log.Error(err) + return nil + } + + go utils.Safe(func() { + if err := f(); err != nil { + self.Log.Error(err) + } + + self.Lock() + if index == self.index { + _ = self.closePopupFn() + } + self.Unlock() + }) + + return nil +} + +type TestPopupHandler struct { + OnErrorMsg func(message string) error + OnAsk func(opts AskOpts) error + OnPrompt func(opts PromptOpts) error +} + +func (self *TestPopupHandler) Error(err error) error { + return self.ErrorMsg(err.Error()) +} + +func (self *TestPopupHandler) ErrorMsg(message string) error { + return self.OnErrorMsg(message) +} + +func (self *TestPopupHandler) Ask(opts AskOpts) error { + return self.OnAsk(opts) +} + +func (self *TestPopupHandler) Prompt(opts PromptOpts) error { + return self.OnPrompt(opts) +} + +func (self *TestPopupHandler) WithLoaderPanel(message string, f func() error) error { + return f() +} + +func (self *TestPopupHandler) WithWaitingStatus(message string, f func() error) error { + return f() +} + +func (self *TestPopupHandler) Menu(opts CreateMenuOptions) error { + panic("not yet implemented") +} diff --git a/pkg/gui/popup_handler.go b/pkg/gui/popup_handler.go deleted file mode 100644 index 9cacc3574..000000000 --- a/pkg/gui/popup_handler.go +++ /dev/null @@ -1,87 +0,0 @@ -package gui - -import ( - "strings" - - "github.com/jesseduffield/lazygit/pkg/gui/style" -) - -type PopupHandler interface { - Error(message string) error - Ask(opts askOpts) error - Prompt(opts promptOpts) error - Loader(message string) error -} - -type RealPopupHandler struct { - gui *Gui -} - -func (self *RealPopupHandler) Error(message string) error { - gui := self.gui - - coloredMessage := style.FgRed.Sprint(strings.TrimSpace(message)) - if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil { - return err - } - - return self.Ask(askOpts{ - title: gui.Tr.Error, - prompt: coloredMessage, - }) -} - -func (self *RealPopupHandler) Ask(opts askOpts) error { - gui := self.gui - - return gui.createPopupPanel(createPopupPanelOpts{ - title: opts.title, - prompt: opts.prompt, - handleConfirm: opts.handleConfirm, - handleClose: opts.handleClose, - handlersManageFocus: opts.handlersManageFocus, - }) -} - -func (self *RealPopupHandler) Prompt(opts promptOpts) error { - gui := self.gui - - return gui.createPopupPanel(createPopupPanelOpts{ - title: opts.title, - prompt: opts.initialContent, - editable: true, - handleConfirmPrompt: opts.handleConfirm, - findSuggestionsFunc: opts.findSuggestionsFunc, - }) -} - -func (self *RealPopupHandler) Loader(message string) error { - gui := self.gui - - return gui.createPopupPanel(createPopupPanelOpts{ - prompt: message, - hasLoader: true, - }) -} - -type TestPopupHandler struct { - onError func(message string) error - onAsk func(opts askOpts) error - onPrompt func(opts promptOpts) error -} - -func (self *TestPopupHandler) Error(message string) error { - return self.onError(message) -} - -func (self *TestPopupHandler) Ask(opts askOpts) error { - return self.onAsk(opts) -} - -func (self *TestPopupHandler) Prompt(opts promptOpts) error { - return self.onPrompt(opts) -} - -func (self *TestPopupHandler) Loader(message string) error { - return nil -} diff --git a/pkg/gui/pull_request_menu_panel.go b/pkg/gui/pull_request_menu_panel.go index c2d5c9f57..5c2a3df8d 100644 --- a/pkg/gui/pull_request_menu_panel.go +++ b/pkg/gui/pull_request_menu_panel.go @@ -5,30 +5,31 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/hosting_service" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/popup" ) func (gui *Gui) createPullRequestMenu(selectedBranch *models.Branch, checkedOutBranch *models.Branch) error { - menuItems := make([]*menuItem, 0, 4) + menuItems := make([]*popup.MenuItem, 0, 4) fromToDisplayStrings := func(from string, to string) []string { return []string{fmt.Sprintf("%s → %s", from, to)} } - menuItemsForBranch := func(branch *models.Branch) []*menuItem { - return []*menuItem{ + menuItemsForBranch := func(branch *models.Branch) []*popup.MenuItem { + return []*popup.MenuItem{ { - displayStrings: fromToDisplayStrings(branch.Name, gui.Tr.LcDefaultBranch), - onPress: func() error { + DisplayStrings: fromToDisplayStrings(branch.Name, gui.Tr.LcDefaultBranch), + OnPress: func() error { return gui.createPullRequest(branch.Name, "") }, }, { - displayStrings: fromToDisplayStrings(branch.Name, gui.Tr.LcSelectBranch), - onPress: func() error { - return gui.prompt(promptOpts{ - title: branch.Name + " →", - findSuggestionsFunc: gui.getBranchNameSuggestionsFunc(), - handleConfirm: func(targetBranchName string) error { + DisplayStrings: fromToDisplayStrings(branch.Name, gui.Tr.LcSelectBranch), + OnPress: func() error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: branch.Name + " →", + FindSuggestionsFunc: gui.getBranchNameSuggestionsFunc(), + HandleConfirm: func(targetBranchName string) error { return gui.createPullRequest(branch.Name, targetBranchName) }}, ) @@ -39,9 +40,9 @@ func (gui *Gui) createPullRequestMenu(selectedBranch *models.Branch, checkedOutB if selectedBranch != checkedOutBranch { menuItems = append(menuItems, - &menuItem{ - displayStrings: fromToDisplayStrings(checkedOutBranch.Name, selectedBranch.Name), - onPress: func() error { + &popup.MenuItem{ + DisplayStrings: fromToDisplayStrings(checkedOutBranch.Name, selectedBranch.Name), + OnPress: func() error { return gui.createPullRequest(checkedOutBranch.Name, selectedBranch.Name) }, }, @@ -51,20 +52,20 @@ func (gui *Gui) createPullRequestMenu(selectedBranch *models.Branch, checkedOutB menuItems = append(menuItems, menuItemsForBranch(selectedBranch)...) - return gui.createMenu(fmt.Sprintf(gui.Tr.CreatePullRequestOptions), menuItems, createMenuOptions{showCancel: true}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: fmt.Sprintf(gui.Tr.CreatePullRequestOptions), Items: menuItems}) } func (gui *Gui) createPullRequest(from string, to string) error { hostingServiceMgr := gui.getHostingServiceMgr() url, err := hostingServiceMgr.GetPullRequestURL(from, to) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.logAction(gui.Tr.Actions.OpenPullRequest) if err := gui.OSCommand.OpenLink(url); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return nil diff --git a/pkg/gui/quitting.go b/pkg/gui/quitting.go index d3beaf998..4e9242640 100644 --- a/pkg/gui/quitting.go +++ b/pkg/gui/quitting.go @@ -4,6 +4,7 @@ import ( "os" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/popup" ) // when a user runs lazygit with the LAZYGIT_NEW_DIR_FILE env variable defined @@ -28,12 +29,12 @@ func (gui *Gui) recordDirectory(dirName string) error { } func (gui *Gui) handleQuitWithoutChangingDirectory() error { - gui.State.RetainOriginalDir = true + gui.RetainOriginalDir = true return gui.quit() } func (gui *Gui) handleQuit() error { - gui.State.RetainOriginalDir = false + gui.RetainOriginalDir = false return gui.quit() } @@ -53,12 +54,8 @@ func (gui *Gui) handleTopLevelReturn() error { } repoPathStack := gui.RepoPathStack - if len(repoPathStack) > 0 { - n := len(repoPathStack) - 1 - - path := repoPathStack[n] - - gui.RepoPathStack = repoPathStack[:n] + if !repoPathStack.IsEmpty() { + path := repoPathStack.Pop() return gui.dispatchSwitchToRepo(path, true) } @@ -76,10 +73,10 @@ func (gui *Gui) quit() error { } if gui.UserConfig.ConfirmOnQuit { - return gui.ask(askOpts{ - title: "", - prompt: gui.Tr.ConfirmQuit, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: "", + Prompt: gui.Tr.ConfirmQuit, + HandleConfirm: func() error { return gocui.ErrQuit }, }) diff --git a/pkg/gui/rebase_options_panel.go b/pkg/gui/rebase_options_panel.go index a9e7d9317..b4a37e956 100644 --- a/pkg/gui/rebase_options_panel.go +++ b/pkg/gui/rebase_options_panel.go @@ -5,6 +5,8 @@ import ( "strings" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) type RebaseOption string @@ -22,13 +24,13 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error { options = append(options, REBASE_OPTION_SKIP) } - menuItems := make([]*menuItem, len(options)) + menuItems := make([]*popup.MenuItem, len(options)) for i, option := range options { // note to self. Never, EVER, close over loop variables in a function option := option - menuItems[i] = &menuItem{ - displayString: option, - onPress: func() error { + menuItems[i] = &popup.MenuItem{ + DisplayString: option, + OnPress: func() error { return gui.genericMergeCommand(option) }, } @@ -41,14 +43,14 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error { title = gui.Tr.RebaseOptionsTitle } - return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: title, Items: menuItems}) } func (gui *Gui) genericMergeCommand(command string) error { status := gui.Git.Status.WorkingTreeState() if status != enums.REBASE_MODE_MERGING && status != enums.REBASE_MODE_REBASING { - return gui.createErrorPanel(gui.Tr.NotMergingOrRebasing) + return gui.PopupHandler.ErrorMsg(gui.Tr.NotMergingOrRebasing) } gui.logAction(fmt.Sprintf("Merge/Rebase: %s", command)) @@ -97,7 +99,7 @@ func isMergeConflictErr(errStr string) bool { } func (gui *Gui) handleGenericMergeCommandResult(result error) error { - if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}); err != nil { return err } if result == nil { @@ -110,14 +112,14 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error { // assume in this case that we're already done return nil } else if isMergeConflictErr(result.Error()) { - return gui.ask(askOpts{ - title: gui.Tr.FoundConflictsTitle, - prompt: gui.Tr.FoundConflicts, - handlersManageFocus: true, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.FoundConflictsTitle, + Prompt: gui.Tr.FoundConflicts, + HandlersManageFocus: true, + HandleConfirm: func() error { return gui.pushContext(gui.State.Contexts.Files) }, - handleClose: func() error { + HandleClose: func() error { if err := gui.returnFromContext(); err != nil { return err } @@ -126,17 +128,17 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error { }, }) } else { - return gui.createErrorPanel(result.Error()) + return gui.PopupHandler.ErrorMsg(result.Error()) } } func (gui *Gui) abortMergeOrRebaseWithConfirm() error { // prompt user to confirm that they want to abort, then do it mode := gui.workingTreeStateNoun() - return gui.ask(askOpts{ - title: fmt.Sprintf(gui.Tr.AbortTitle, mode), - prompt: fmt.Sprintf(gui.Tr.AbortPrompt, mode), - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: fmt.Sprintf(gui.Tr.AbortTitle, mode), + Prompt: fmt.Sprintf(gui.Tr.AbortPrompt, mode), + HandleConfirm: func() error { return gui.genericMergeCommand(REBASE_OPTION_ABORT) }, }) diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go index 01b9a00d3..ec6a1ffc7 100644 --- a/pkg/gui/recent_repos_panel.go +++ b/pkg/gui/recent_repos_panel.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/git_config" "github.com/jesseduffield/lazygit/pkg/env" + "github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -16,24 +17,24 @@ func (gui *Gui) handleCreateRecentReposMenu() error { reposCount := utils.Min(len(recentRepoPaths), 20) // we won't show the current repo hence the -1 - menuItems := make([]*menuItem, reposCount-1) + menuItems := make([]*popup.MenuItem, reposCount-1) for i, path := range recentRepoPaths[1:reposCount] { path := path // cos we're closing over the loop variable - menuItems[i] = &menuItem{ - displayStrings: []string{ + menuItems[i] = &popup.MenuItem{ + DisplayStrings: []string{ filepath.Base(path), style.FgMagenta.Sprint(path), }, - onPress: func() error { + OnPress: func() error { // if we were in a submodule, we want to forget about that stack of repos // so that hitting escape in the new repo does nothing - gui.RepoPathStack = []string{} + gui.RepoPathStack.Clear() return gui.dispatchSwitchToRepo(path, false) }, } } - return gui.createMenu(gui.Tr.RecentRepos, menuItems, createMenuOptions{showCancel: true}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: gui.Tr.RecentRepos, Items: menuItems}) } func (gui *Gui) handleShowAllBranchLogs() error { @@ -57,7 +58,7 @@ func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error { if err := os.Chdir(path); err != nil { if os.IsNotExist(err) { - return gui.createErrorPanel(gui.Tr.ErrRepositoryMovedOrDeleted) + return gui.PopupHandler.ErrorMsg(gui.Tr.ErrRepositoryMovedOrDeleted) } return err } diff --git a/pkg/gui/reflog_panel.go b/pkg/gui/reflog_panel.go index af8e8092c..3ccbdb7c8 100644 --- a/pkg/gui/reflog_panel.go +++ b/pkg/gui/reflog_panel.go @@ -2,6 +2,7 @@ package gui import ( "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/popup" ) // list panel functions @@ -55,7 +56,7 @@ func (gui *Gui) refreshReflogCommits() error { commits, onlyObtainedNewReflogCommits, err := gui.Git.Loaders.ReflogCommits. GetReflogCommits(lastReflogCommit, filterPath) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } if onlyObtainedNewReflogCommits { @@ -87,10 +88,10 @@ func (gui *Gui) handleCheckoutReflogCommit() error { return nil } - err := gui.ask(askOpts{ - title: gui.Tr.LcCheckoutCommit, - prompt: gui.Tr.SureCheckoutThisCommit, - handleConfirm: func() error { + err := gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.LcCheckoutCommit, + Prompt: gui.Tr.SureCheckoutThisCommit, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.CheckoutReflogCommit) return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{}) }, diff --git a/pkg/gui/remote_branches_panel.go b/pkg/gui/remote_branches_panel.go index 29ab59187..1ee402097 100644 --- a/pkg/gui/remote_branches_panel.go +++ b/pkg/gui/remote_branches_panel.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -52,16 +54,18 @@ func (gui *Gui) handleDeleteRemoteBranch() error { } message := fmt.Sprintf("%s '%s'?", gui.Tr.DeleteRemoteBranchMessage, remoteBranch.FullName()) - return gui.ask(askOpts{ - title: gui.Tr.DeleteRemoteBranch, - prompt: message, - handleConfirm: func() error { - return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.DeleteRemoteBranch, + Prompt: message, + HandleConfirm: func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.DeletingStatus, func() error { gui.logAction(gui.Tr.Actions.DeleteRemoteBranch) err := gui.Git.Remote.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name) - gui.handleCredentialsPopup(err) + if err != nil { + _ = gui.PopupHandler.Error(err) + } - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}}) + return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}}) }) }, }) @@ -84,16 +88,16 @@ func (gui *Gui) handleSetBranchUpstream() error { }, ) - return gui.ask(askOpts{ - title: gui.Tr.SetUpstreamTitle, - prompt: message, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.SetUpstreamTitle, + Prompt: message, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.SetBranchUpstream) if err := gui.Git.Branch.SetUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}}) + return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}}) }, }) } diff --git a/pkg/gui/remotes_panel.go b/pkg/gui/remotes_panel.go index c74653a27..bc06f77f0 100644 --- a/pkg/gui/remotes_panel.go +++ b/pkg/gui/remotes_panel.go @@ -5,7 +5,9 @@ import ( "strings" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -42,7 +44,7 @@ func (gui *Gui) refreshRemotes() error { remotes, err := gui.Git.Loaders.Remotes.GetRemotes() if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.State.Remotes = remotes @@ -79,17 +81,17 @@ func (gui *Gui) handleRemoteEnter() error { } func (gui *Gui) handleAddRemote() error { - return gui.prompt(promptOpts{ - title: gui.Tr.LcNewRemoteName, - handleConfirm: func(remoteName string) error { - return gui.prompt(promptOpts{ - title: gui.Tr.LcNewRemoteUrl, - handleConfirm: func(remoteUrl string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: gui.Tr.LcNewRemoteName, + HandleConfirm: func(remoteName string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: gui.Tr.LcNewRemoteUrl, + HandleConfirm: func(remoteUrl string) error { gui.logAction(gui.Tr.Actions.AddRemote) if err := gui.Git.Remote.AddRemote(remoteName, remoteUrl); err != nil { return err } - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{REMOTES}}) + return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.REMOTES}}) }, }) }, @@ -103,16 +105,16 @@ func (gui *Gui) handleRemoveRemote() error { return nil } - return gui.ask(askOpts{ - title: gui.Tr.LcRemoveRemote, - prompt: gui.Tr.LcRemoveRemotePrompt + " '" + remote.Name + "'?", - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.LcRemoveRemote, + Prompt: gui.Tr.LcRemoveRemotePrompt + " '" + remote.Name + "'?", + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.RemoveRemote) if err := gui.Git.Remote.RemoveRemote(remote.Name); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}}) + return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}}) }, }) } @@ -130,14 +132,14 @@ func (gui *Gui) handleEditRemote() error { }, ) - return gui.prompt(promptOpts{ - title: editNameMessage, - initialContent: remote.Name, - handleConfirm: func(updatedRemoteName string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: editNameMessage, + InitialContent: remote.Name, + HandleConfirm: func(updatedRemoteName string) error { if updatedRemoteName != remote.Name { gui.logAction(gui.Tr.Actions.UpdateRemote) if err := gui.Git.Remote.RenameRemote(remote.Name, updatedRemoteName); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } } @@ -154,15 +156,15 @@ func (gui *Gui) handleEditRemote() error { url = urls[0] } - return gui.prompt(promptOpts{ - title: editUrlMessage, - initialContent: url, - handleConfirm: func(updatedRemoteUrl string) error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: editUrlMessage, + InitialContent: url, + HandleConfirm: func(updatedRemoteUrl string) error { gui.logAction(gui.Tr.Actions.UpdateRemote) if err := gui.Git.Remote.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}}) + return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}}) }, }) }, @@ -175,13 +177,15 @@ func (gui *Gui) handleFetchRemote() error { return nil } - return gui.WithWaitingStatus(gui.Tr.FetchingRemoteStatus, func() error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.FetchingRemoteStatus, func() error { gui.Mutexes.FetchMutex.Lock() defer gui.Mutexes.FetchMutex.Unlock() err := gui.Git.Sync.FetchRemote(remote.Name) - gui.handleCredentialsPopup(err) + if err != nil { + _ = gui.PopupHandler.Error(err) + } - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}}) + return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}}) }) } diff --git a/pkg/gui/reset_menu_panel.go b/pkg/gui/reset_menu_panel.go index 586987778..d920765f9 100644 --- a/pkg/gui/reset_menu_panel.go +++ b/pkg/gui/reset_menu_panel.go @@ -3,12 +3,14 @@ package gui import ( "fmt" + "github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) func (gui *Gui) resetToRef(ref string, strength string, envVars []string) error { if err := gui.Git.Commit.ResetToCommit(ref, strength, envVars); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.State.Panels.Commits.SelectedLineIdx = 0 @@ -20,7 +22,7 @@ func (gui *Gui) resetToRef(ref string, strength string, envVars []string) error return err } - if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES, BRANCHES, REFLOG, COMMITS}}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.BRANCHES, types.REFLOG, types.COMMITS}}); err != nil { return err } @@ -29,20 +31,23 @@ func (gui *Gui) resetToRef(ref string, strength string, envVars []string) error func (gui *Gui) createResetMenu(ref string) error { strengths := []string{"soft", "mixed", "hard"} - menuItems := make([]*menuItem, len(strengths)) + menuItems := make([]*popup.MenuItem, len(strengths)) for i, strength := range strengths { strength := strength - menuItems[i] = &menuItem{ - displayStrings: []string{ + menuItems[i] = &popup.MenuItem{ + DisplayStrings: []string{ fmt.Sprintf("%s reset", strength), style.FgRed.Sprintf("reset --%s %s", strength, ref), }, - onPress: func() error { + OnPress: func() error { gui.logAction("Reset") return gui.resetToRef(ref, strength, []string{}) }, } } - return gui.createMenu(fmt.Sprintf("%s %s", gui.Tr.LcResetTo, ref), menuItems, createMenuOptions{showCancel: true}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{ + Title: fmt.Sprintf("%s %s", gui.Tr.LcResetTo, ref), + Items: menuItems, + }) } diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go index ecb208dcc..f4cd98ea2 100644 --- a/pkg/gui/staging_panel.go +++ b/pkg/gui/staging_panel.go @@ -4,6 +4,8 @@ import ( "strings" "github.com/jesseduffield/lazygit/pkg/commands/patch" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx int) error { @@ -112,10 +114,10 @@ func (gui *Gui) handleResetSelection() error { } if !gui.UserConfig.Gui.SkipUnstageLineWarning { - return gui.ask(askOpts{ - title: gui.Tr.UnstageLinesTitle, - prompt: gui.Tr.UnstageLinesPrompt, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.UnstageLinesTitle, + Prompt: gui.Tr.UnstageLinesPrompt, + HandleConfirm: func() error { return gui.withLBLActiveCheck(func(state *LblPanelState) error { return gui.applySelection(true, state) }) @@ -149,14 +151,14 @@ func (gui *Gui) applySelection(reverse bool, state *LblPanelState) error { gui.logAction(gui.Tr.Actions.ApplyPatch) err := gui.Git.WorkingTree.ApplyPatch(patch, applyFlags...) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } if state.SelectingRange() { state.SetLineSelectMode() } - if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil { return err } if err := gui.refreshStagingPanel(false, -1); err != nil { diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go index 2b9bae445..f8121bf94 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -2,6 +2,8 @@ package gui import ( "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) // list panel functions @@ -54,7 +56,7 @@ func (gui *Gui) handleStashApply() error { err := gui.Git.Stash.Apply(stashEntry.Index) _ = gui.postStashRefresh() if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return nil } @@ -63,10 +65,10 @@ func (gui *Gui) handleStashApply() error { return apply() } - return gui.ask(askOpts{ - title: gui.Tr.StashApply, - prompt: gui.Tr.SureApplyStashEntry, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.StashApply, + Prompt: gui.Tr.SureApplyStashEntry, + HandleConfirm: func() error { return apply() }, }) @@ -85,7 +87,7 @@ func (gui *Gui) handleStashPop() error { err := gui.Git.Stash.Pop(stashEntry.Index) _ = gui.postStashRefresh() if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return nil } @@ -94,10 +96,10 @@ func (gui *Gui) handleStashPop() error { return pop() } - return gui.ask(askOpts{ - title: gui.Tr.StashPop, - prompt: gui.Tr.SurePopStashEntry, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.StashPop, + Prompt: gui.Tr.SurePopStashEntry, + HandleConfirm: func() error { return pop() }, }) @@ -109,15 +111,15 @@ func (gui *Gui) handleStashDrop() error { return nil } - return gui.ask(askOpts{ - title: gui.Tr.StashDrop, - prompt: gui.Tr.SureDropStashEntry, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.StashDrop, + Prompt: gui.Tr.SureDropStashEntry, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.Stash) err := gui.Git.Stash.Drop(stashEntry.Index) _ = gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH}}) if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return nil }, @@ -125,12 +127,12 @@ func (gui *Gui) handleStashDrop() error { } func (gui *Gui) postStashRefresh() error { - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH, FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH, types.FILES}}) } func (gui *Gui) handleStashSave(stashFunc func(message string) error) error { if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 { - return gui.createErrorPanel(gui.Tr.NoTrackedStagedFilesStash) + return gui.PopupHandler.ErrorMsg(gui.Tr.NoTrackedStagedFilesStash) } return gui.prompt(promptOpts{ @@ -139,7 +141,7 @@ func (gui *Gui) handleStashSave(stashFunc func(message string) error) error { err := stashFunc(stashComment) _ = gui.postStashRefresh() if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return nil }, diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 444c32da1..d9fff2913 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/constants" + "github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/utils" @@ -49,8 +50,10 @@ func cursorInSubstring(cx int, prefix string, substring string) bool { } func (gui *Gui) handleCheckForUpdate() error { - gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true) - return gui.createLoaderPanel(gui.Tr.CheckingForUpdates) + return gui.PopupHandler.WithWaitingStatus(gui.Tr.CheckingForUpdates, func() error { + gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true) + return nil + }) } func (gui *Gui) handleStatusClick() error { @@ -136,17 +139,21 @@ func (gui *Gui) askForConfigFile(action func(file string) error) error { case 1: return action(confPaths[0]) default: - menuItems := make([]*menuItem, len(confPaths)) + menuItems := make([]*popup.MenuItem, len(confPaths)) for i, file := range confPaths { i := i - menuItems[i] = &menuItem{ - displayString: file, - onPress: func() error { + menuItems[i] = &popup.MenuItem{ + DisplayString: file, + OnPress: func() error { return action(confPaths[i]) }, } } - return gui.createMenu(gui.Tr.SelectConfigFile, menuItems, createMenuOptions{}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{ + Title: gui.Tr.SelectConfigFile, + Items: menuItems, + HideCancel: true, + }) } } diff --git a/pkg/gui/sub_commits_panel.go b/pkg/gui/sub_commits_panel.go index d023d261f..97f57d158 100644 --- a/pkg/gui/sub_commits_panel.go +++ b/pkg/gui/sub_commits_panel.go @@ -3,6 +3,7 @@ package gui import ( "github.com/jesseduffield/lazygit/pkg/commands/loaders" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/popup" ) // list panel functions @@ -42,10 +43,10 @@ func (gui *Gui) handleCheckoutSubCommit() error { return nil } - err := gui.ask(askOpts{ - title: gui.Tr.LcCheckoutCommit, - prompt: gui.Tr.SureCheckoutThisCommit, - handleConfirm: func() error { + err := gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.LcCheckoutCommit, + Prompt: gui.Tr.SureCheckoutThisCommit, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.CheckoutCommit) return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{}) }, diff --git a/pkg/gui/submodules_panel.go b/pkg/gui/submodules_panel.go index be0574356..e63634bc6 100644 --- a/pkg/gui/submodules_panel.go +++ b/pkg/gui/submodules_panel.go @@ -3,8 +3,6 @@ package gui import ( "fmt" "os" - "path/filepath" - "strings" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/style" @@ -36,7 +34,7 @@ func (gui *Gui) submodulesRenderToMain() error { if file == nil { task = NewRenderStringTask(prefix) } else { - cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, gui.State.IgnoreWhitespaceInDiffView) + cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, gui.IgnoreWhitespaceInDiffView) task = NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix) } } @@ -60,205 +58,12 @@ func (gui *Gui) refreshStateSubmoduleConfigs() error { return nil } -func (gui *Gui) handleSubmoduleEnter(submodule *models.SubmoduleConfig) error { - return gui.enterSubmodule(submodule) -} - func (gui *Gui) enterSubmodule(submodule *models.SubmoduleConfig) error { wd, err := os.Getwd() if err != nil { return err } - gui.RepoPathStack = append(gui.RepoPathStack, wd) + gui.RepoPathStack.Push(wd) return gui.dispatchSwitchToRepo(submodule.Path, true) } - -func (gui *Gui) removeSubmodule(submodule *models.SubmoduleConfig) error { - return gui.ask(askOpts{ - title: gui.Tr.RemoveSubmodule, - prompt: fmt.Sprintf(gui.Tr.RemoveSubmodulePrompt, submodule.Name), - handleConfirm: func() error { - gui.logAction(gui.Tr.Actions.RemoveSubmodule) - if err := gui.Git.Submodule.Delete(submodule); err != nil { - return gui.surfaceError(err) - } - - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES, FILES}}) - }, - }) -} - -func (gui *Gui) handleResetSubmodule(submodule *models.SubmoduleConfig) error { - return gui.WithWaitingStatus(gui.Tr.LcResettingSubmoduleStatus, func() error { - return gui.resetSubmodule(submodule) - }) -} - -func (gui *Gui) fileForSubmodule(submodule *models.SubmoduleConfig) *models.File { - for _, file := range gui.State.FileTreeViewModel.GetAllFiles() { - if file.IsSubmodule([]*models.SubmoduleConfig{submodule}) { - return file - } - } - - return nil -} - -func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error { - gui.logAction(gui.Tr.Actions.ResetSubmodule) - - file := gui.fileForSubmodule(submodule) - if file != nil { - if err := gui.Git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { - return gui.surfaceError(err) - } - } - - if err := gui.Git.Submodule.Stash(submodule); err != nil { - return gui.surfaceError(err) - } - if err := gui.Git.Submodule.Reset(submodule); err != nil { - return gui.surfaceError(err) - } - - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES, SUBMODULES}}) -} - -func (gui *Gui) handleAddSubmodule() error { - return gui.prompt(promptOpts{ - title: gui.Tr.LcNewSubmoduleUrl, - handleConfirm: func(submoduleUrl string) error { - nameSuggestion := filepath.Base(strings.TrimSuffix(submoduleUrl, filepath.Ext(submoduleUrl))) - - return gui.prompt(promptOpts{ - title: gui.Tr.LcNewSubmoduleName, - initialContent: nameSuggestion, - handleConfirm: func(submoduleName string) error { - - return gui.prompt(promptOpts{ - title: gui.Tr.LcNewSubmodulePath, - initialContent: submoduleName, - handleConfirm: func(submodulePath string) error { - return gui.WithWaitingStatus(gui.Tr.LcAddingSubmoduleStatus, func() error { - gui.logAction(gui.Tr.Actions.AddSubmodule) - err := gui.Git.Submodule.Add(submoduleName, submodulePath, submoduleUrl) - gui.handleCredentialsPopup(err) - - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) - }) - }, - }) - }, - }) - }, - }) - -} - -func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error { - return gui.prompt(promptOpts{ - title: fmt.Sprintf(gui.Tr.LcUpdateSubmoduleUrl, submodule.Name), - initialContent: submodule.Url, - handleConfirm: func(newUrl string) error { - return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleUrlStatus, func() error { - gui.logAction(gui.Tr.Actions.UpdateSubmoduleUrl) - err := gui.Git.Submodule.UpdateUrl(submodule.Name, submodule.Path, newUrl) - gui.handleCredentialsPopup(err) - - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) - }) - }, - }) -} - -func (gui *Gui) handleSubmoduleInit(submodule *models.SubmoduleConfig) error { - return gui.WithWaitingStatus(gui.Tr.LcInitializingSubmoduleStatus, func() error { - gui.logAction(gui.Tr.Actions.InitialiseSubmodule) - err := gui.Git.Submodule.Init(submodule.Path) - gui.handleCredentialsPopup(err) - - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) - }) -} - -func (gui *Gui) forSubmodule(callback func(*models.SubmoduleConfig) error) func() error { - return func() error { - submodule := gui.getSelectedSubmodule() - if submodule == nil { - return nil - } - - return callback(submodule) - } -} - -func (gui *Gui) handleBulkSubmoduleActionsMenu() error { - menuItems := []*menuItem{ - { - displayStrings: []string{gui.Tr.LcBulkInitSubmodules, style.FgGreen.Sprint(gui.Git.Submodule.BulkInitCmdObj().ToString())}, - onPress: func() error { - return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { - gui.logAction(gui.Tr.Actions.BulkInitialiseSubmodules) - err := gui.Git.Submodule.BulkInitCmdObj().Run() - if err != nil { - return gui.surfaceError(err) - } - - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) - }) - }, - }, - { - displayStrings: []string{gui.Tr.LcBulkUpdateSubmodules, style.FgYellow.Sprint(gui.Git.Submodule.BulkUpdateCmdObj().ToString())}, - onPress: func() error { - return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { - gui.logAction(gui.Tr.Actions.BulkUpdateSubmodules) - if err := gui.Git.Submodule.BulkUpdateCmdObj().Run(); err != nil { - return gui.surfaceError(err) - } - - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) - }) - }, - }, - { - displayStrings: []string{gui.Tr.LcSubmoduleStashAndReset, style.FgRed.Sprintf("git stash in each submodule && %s", gui.Git.Submodule.ForceBulkUpdateCmdObj().ToString())}, - onPress: func() error { - return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { - gui.logAction(gui.Tr.Actions.BulkStashAndResetSubmodules) - if err := gui.Git.Submodule.ResetSubmodules(gui.State.Submodules); err != nil { - return gui.surfaceError(err) - } - - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) - }) - }, - }, - { - displayStrings: []string{gui.Tr.LcBulkDeinitSubmodules, style.FgRed.Sprint(gui.Git.Submodule.BulkDeinitCmdObj().ToString())}, - onPress: func() error { - return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error { - gui.logAction(gui.Tr.Actions.BulkDeinitialiseSubmodules) - if err := gui.Git.Submodule.BulkDeinitCmdObj().Run(); err != nil { - return gui.surfaceError(err) - } - - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) - }) - }, - }, - } - - return gui.createMenu(gui.Tr.LcBulkSubmoduleOptions, menuItems, createMenuOptions{showCancel: true}) -} - -func (gui *Gui) handleUpdateSubmodule(submodule *models.SubmoduleConfig) error { - return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleStatus, func() error { - gui.logAction(gui.Tr.Actions.UpdateSubmodule) - err := gui.Git.Submodule.Update(submodule.Path) - gui.handleCredentialsPopup(err) - - return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}}) - }) -} diff --git a/pkg/gui/tags_panel.go b/pkg/gui/tags_panel.go index efad33fbf..9f516a006 100644 --- a/pkg/gui/tags_panel.go +++ b/pkg/gui/tags_panel.go @@ -2,6 +2,8 @@ package gui import ( "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -41,7 +43,7 @@ func (gui *Gui) tagsRenderToMain() error { func (gui *Gui) refreshTags() error { tags, err := gui.Git.Loaders.Tags.GetTags() if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } gui.State.Tags = tags @@ -78,15 +80,15 @@ func (gui *Gui) handleDeleteTag(tag *models.Tag) error { }, ) - return gui.ask(askOpts{ - title: gui.Tr.DeleteTagTitle, - prompt: prompt, - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.DeleteTagTitle, + Prompt: prompt, + HandleConfirm: func() error { gui.logAction(gui.Tr.Actions.DeleteTag) if err := gui.Git.Tag.Delete(tag.Name); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}}) }, }) } @@ -99,15 +101,17 @@ func (gui *Gui) handlePushTag(tag *models.Tag) error { }, ) - return gui.prompt(promptOpts{ - title: title, - initialContent: "origin", - findSuggestionsFunc: gui.getRemoteSuggestionsFunc(), - handleConfirm: func(response string) error { - return gui.WithWaitingStatus(gui.Tr.PushingTagStatus, func() error { + return gui.PopupHandler.Prompt(popup.PromptOpts{ + Title: title, + InitialContent: "origin", + FindSuggestionsFunc: gui.getRemoteSuggestionsFunc(), + HandleConfirm: func(response string) error { + return gui.PopupHandler.WithWaitingStatus(gui.Tr.PushingTagStatus, func() error { gui.logAction(gui.Tr.Actions.PushTag) err := gui.Git.Tag.Push(response, tag.Name) - gui.handleCredentialsPopup(err) + if err != nil { + _ = gui.PopupHandler.Error(err) + } return nil }) diff --git a/pkg/gui/types/keybindings.go b/pkg/gui/types/keybindings.go new file mode 100644 index 000000000..abe3f84d0 --- /dev/null +++ b/pkg/gui/types/keybindings.go @@ -0,0 +1,18 @@ +package types + +import "github.com/jesseduffield/gocui" + +// Binding - a keybinding mapping a key and modifier to a handler. The keypress +// is only handled if the given view has focus, or handled globally if the view +// is "" +type Binding struct { + ViewName string + Contexts []string + Handler func() error + Key interface{} // FIXME: find out how to get `gocui.Key | rune` + Modifier gocui.Modifier + Description string + Alternative string + Tag string // e.g. 'navigation'. Used for grouping things in the cheatsheet + OpensMenu bool +} diff --git a/pkg/gui/types/refresh.go b/pkg/gui/types/refresh.go new file mode 100644 index 000000000..d0cbe02ba --- /dev/null +++ b/pkg/gui/types/refresh.go @@ -0,0 +1,32 @@ +package types + +// models/views that we can refresh +type RefreshableView int + +const ( + COMMITS RefreshableView = iota + BRANCHES + FILES + STASH + REFLOG + TAGS + REMOTES + STATUS + SUBMODULES + // not actually a view. Will refactor this later + BISECT_INFO +) + +type RefreshMode int + +const ( + SYNC RefreshMode = iota // wait until everything is done before returning + ASYNC // return immediately, allowing each independent thing to update itself + BLOCK_UI // wrap code in an update call to ensure UI updates all at once and keybindings aren't executed till complete +) + +type RefreshOptions struct { + Then func() + Scope []RefreshableView // e.g. []int{COMMITS, BRANCHES}. Leave empty to refresh everything + Mode RefreshMode // one of SYNC (default), ASYNC, and BLOCK_UI +} diff --git a/pkg/gui/undoing.go b/pkg/gui/undoing.go index 72a4ef302..e61950700 100644 --- a/pkg/gui/undoing.go +++ b/pkg/gui/undoing.go @@ -2,6 +2,8 @@ package gui import ( "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/gui/popup" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -89,7 +91,7 @@ func (gui *Gui) reflogUndo() error { undoingStatus := gui.Tr.UndoingStatus if gui.Git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { - return gui.createErrorPanel(gui.Tr.LcCantUndoWhileRebasing) + return gui.PopupHandler.ErrorMsg(gui.Tr.LcCantUndoWhileRebasing) } return gui.parseReflogForActions(func(counter int, action reflogAction) (bool, error) { @@ -124,7 +126,7 @@ func (gui *Gui) reflogRedo() error { redoingStatus := gui.Tr.RedoingStatus if gui.Git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { - return gui.createErrorPanel(gui.Tr.LcCantRedoWhileRebasing) + return gui.PopupHandler.ErrorMsg(gui.Tr.LcCantRedoWhileRebasing) } return gui.parseReflogForActions(func(counter int, action reflogAction) (bool, error) { @@ -166,7 +168,7 @@ type handleHardResetWithAutoStashOptions struct { func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHardResetWithAutoStashOptions) error { reset := func() error { if err := gui.resetToRef(commitSha, "hard", options.EnvVars); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return nil } @@ -175,24 +177,24 @@ func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHar dirtyWorkingTree := len(gui.trackedFiles()) > 0 || len(gui.stagedFiles()) > 0 if dirtyWorkingTree { // offer to autostash changes - return gui.ask(askOpts{ - title: gui.Tr.AutoStashTitle, - prompt: gui.Tr.AutoStashPrompt, - handleConfirm: func() error { - return gui.WithWaitingStatus(options.WaitingStatus, func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: gui.Tr.AutoStashTitle, + Prompt: gui.Tr.AutoStashPrompt, + HandleConfirm: func() error { + return gui.PopupHandler.WithWaitingStatus(options.WaitingStatus, func() error { if err := gui.Git.Stash.Save(gui.Tr.StashPrefix + commitSha); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } if err := reset(); err != nil { return err } err := gui.Git.Stash.Pop(0) - if err := gui.refreshSidePanels(refreshOptions{}); err != nil { + if err := gui.refreshSidePanels(types.RefreshOptions{}); err != nil { return err } if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } return nil }) @@ -200,7 +202,7 @@ func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHar }) } - return gui.WithWaitingStatus(options.WaitingStatus, func() error { + return gui.PopupHandler.WithWaitingStatus(options.WaitingStatus, func() error { return reset() }) } diff --git a/pkg/gui/updates.go b/pkg/gui/updates.go index cce723c4f..5eb08dae3 100644 --- a/pkg/gui/updates.go +++ b/pkg/gui/updates.go @@ -4,13 +4,14 @@ import ( "fmt" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/popup" ) func (gui *Gui) showUpdatePrompt(newVersion string) error { - return gui.ask(askOpts{ - title: "New version available!", - prompt: fmt.Sprintf("Download version %s? (enter/esc)", newVersion), - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: "New version available!", + Prompt: fmt.Sprintf("Download version %s? (enter/esc)", newVersion), + HandleConfirm: func() error { gui.startUpdating(newVersion) return nil }, @@ -19,10 +20,10 @@ func (gui *Gui) showUpdatePrompt(newVersion string) error { func (gui *Gui) onUserUpdateCheckFinish(newVersion string, err error) error { if err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } if newVersion == "" { - return gui.createErrorPanel("New version not found") + return gui.PopupHandler.ErrorMsg("New version not found") } return gui.showUpdatePrompt(newVersion) } @@ -55,7 +56,7 @@ func (gui *Gui) onUpdateFinish(statusId int, err error) error { gui.OnUIThread(func() error { _ = gui.renderString(gui.Views.AppStatus, "") if err != nil { - return gui.createErrorPanel("Update failed: " + err.Error()) + return gui.PopupHandler.ErrorMsg("Update failed: " + err.Error()) } return nil }) @@ -64,10 +65,10 @@ func (gui *Gui) onUpdateFinish(statusId int, err error) error { } func (gui *Gui) createUpdateQuitConfirmation() error { - return gui.ask(askOpts{ - title: "Currently Updating", - prompt: "An update is in progress. Are you sure you want to quit?", - handleConfirm: func() error { + return gui.PopupHandler.Ask(popup.AskOpts{ + Title: "Currently Updating", + Prompt: "An update is in progress. Are you sure you want to quit?", + HandleConfirm: func() error { return gocui.ErrQuit }, }) diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 300571ad3..089cee454 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/spkg/bom" ) @@ -15,34 +16,18 @@ func (gui *Gui) getCyclableWindows() []string { return []string{"status", "files", "branches", "commits", "stash"} } -// models/views that we can refresh -type RefreshableView int - -const ( - COMMITS RefreshableView = iota - BRANCHES - FILES - STASH - REFLOG - TAGS - REMOTES - STATUS - SUBMODULES - // not actually a view. Will refactor this later - BISECT_INFO -) - -func getScopeNames(scopes []RefreshableView) []string { - scopeNameMap := map[RefreshableView]string{ - COMMITS: "commits", - BRANCHES: "branches", - FILES: "files", - SUBMODULES: "submodules", - STASH: "stash", - REFLOG: "reflog", - TAGS: "tags", - REMOTES: "remotes", - STATUS: "status", +func getScopeNames(scopes []types.RefreshableView) []string { + scopeNameMap := map[types.RefreshableView]string{ + types.COMMITS: "commits", + types.BRANCHES: "branches", + types.FILES: "files", + types.SUBMODULES: "submodules", + types.STASH: "stash", + types.REFLOG: "reflog", + types.TAGS: "tags", + types.REMOTES: "remotes", + types.STATUS: "status", + types.BISECT_INFO: "bisect", } scopeNames := make([]string, len(scopes)) @@ -53,69 +38,55 @@ func getScopeNames(scopes []RefreshableView) []string { return scopeNames } -func getModeName(mode RefreshMode) string { +func getModeName(mode types.RefreshMode) string { switch mode { - case SYNC: + case types.SYNC: return "sync" - case ASYNC: + case types.ASYNC: return "async" - case BLOCK_UI: + case types.BLOCK_UI: return "block-ui" default: return "unknown mode" } } -type RefreshMode int - -const ( - SYNC RefreshMode = iota // wait until everything is done before returning - ASYNC // return immediately, allowing each independent thing to update itself - BLOCK_UI // wrap code in an update call to ensure UI updates all at once and keybindings aren't executed till complete -) - -type refreshOptions struct { - then func() - scope []RefreshableView // e.g. []int{COMMITS, BRANCHES}. Leave empty to refresh everything - mode RefreshMode // one of SYNC (default), ASYNC, and BLOCK_UI -} - -func arrToMap(arr []RefreshableView) map[RefreshableView]bool { - output := map[RefreshableView]bool{} +func arrToMap(arr []types.RefreshableView) map[types.RefreshableView]bool { + output := map[types.RefreshableView]bool{} for _, el := range arr { output[el] = true } return output } -func (gui *Gui) refreshSidePanels(options refreshOptions) error { - if options.scope == nil { +func (gui *Gui) refreshSidePanels(options types.RefreshOptions) error { + if options.Scope == nil { gui.Log.Infof( "refreshing all scopes in %s mode", - getModeName(options.mode), + getModeName(options.Mode), ) } else { gui.Log.Infof( "refreshing the following scopes in %s mode: %s", - getModeName(options.mode), - strings.Join(getScopeNames(options.scope), ","), + getModeName(options.Mode), + strings.Join(getScopeNames(options.Scope), ","), ) } wg := sync.WaitGroup{} f := func() { - var scopeMap map[RefreshableView]bool - if len(options.scope) == 0 { - scopeMap = arrToMap([]RefreshableView{COMMITS, BRANCHES, FILES, STASH, REFLOG, TAGS, REMOTES, STATUS, BISECT_INFO}) + var scopeMap map[types.RefreshableView]bool + if len(options.Scope) == 0 { + scopeMap = arrToMap([]types.RefreshableView{types.COMMITS, types.BRANCHES, types.FILES, types.STASH, types.REFLOG, types.TAGS, types.REMOTES, types.STATUS, types.BISECT_INFO}) } else { - scopeMap = arrToMap(options.scope) + scopeMap = arrToMap(options.Scope) } - if scopeMap[COMMITS] || scopeMap[BRANCHES] || scopeMap[REFLOG] || scopeMap[BISECT_INFO] { + if scopeMap[types.COMMITS] || scopeMap[types.BRANCHES] || scopeMap[types.REFLOG] || scopeMap[types.BISECT_INFO] { wg.Add(1) func() { - if options.mode == ASYNC { + if options.Mode == types.ASYNC { go utils.Safe(func() { gui.refreshCommits() }) } else { gui.refreshCommits() @@ -124,10 +95,10 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error { }() } - if scopeMap[FILES] || scopeMap[SUBMODULES] { + if scopeMap[types.FILES] || scopeMap[types.SUBMODULES] { wg.Add(1) func() { - if options.mode == ASYNC { + if options.Mode == types.ASYNC { go utils.Safe(func() { _ = gui.refreshFilesAndSubmodules() }) } else { _ = gui.refreshFilesAndSubmodules() @@ -136,10 +107,10 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error { }() } - if scopeMap[STASH] { + if scopeMap[types.STASH] { wg.Add(1) func() { - if options.mode == ASYNC { + if options.Mode == types.ASYNC { go utils.Safe(func() { _ = gui.refreshStashEntries() }) } else { _ = gui.refreshStashEntries() @@ -148,10 +119,10 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error { }() } - if scopeMap[TAGS] { + if scopeMap[types.TAGS] { wg.Add(1) func() { - if options.mode == ASYNC { + if options.Mode == types.ASYNC { go utils.Safe(func() { _ = gui.refreshTags() }) } else { _ = gui.refreshTags() @@ -160,10 +131,10 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error { }() } - if scopeMap[REMOTES] { + if scopeMap[types.REMOTES] { wg.Add(1) func() { - if options.mode == ASYNC { + if options.Mode == types.ASYNC { go utils.Safe(func() { _ = gui.refreshRemotes() }) } else { _ = gui.refreshRemotes() @@ -176,12 +147,12 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error { gui.refreshStatus() - if options.then != nil { - options.then() + if options.Then != nil { + options.Then() } } - if options.mode == BLOCK_UI { + if options.Mode == types.BLOCK_UI { gui.OnUIThread(func() error { f() return nil diff --git a/pkg/gui/whitespace-toggle.go b/pkg/gui/whitespace-toggle.go index e7df9d879..7ded50c18 100644 --- a/pkg/gui/whitespace-toggle.go +++ b/pkg/gui/whitespace-toggle.go @@ -1,10 +1,10 @@ package gui func (gui *Gui) toggleWhitespaceInDiffView() error { - gui.State.IgnoreWhitespaceInDiffView = !gui.State.IgnoreWhitespaceInDiffView + gui.IgnoreWhitespaceInDiffView = !gui.IgnoreWhitespaceInDiffView toastMessage := gui.Tr.ShowingWhitespaceInDiffView - if gui.State.IgnoreWhitespaceInDiffView { + if gui.IgnoreWhitespaceInDiffView { toastMessage = gui.Tr.IgnoringWhitespaceInDiffView } gui.raiseToast(toastMessage) diff --git a/pkg/gui/workspace_reset_options_panel.go b/pkg/gui/workspace_reset_options_panel.go index ba2df22e6..6230e0966 100644 --- a/pkg/gui/workspace_reset_options_panel.go +++ b/pkg/gui/workspace_reset_options_panel.go @@ -3,7 +3,9 @@ package gui import ( "fmt" + "github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) func (gui *Gui) handleCreateResetMenu() error { @@ -14,92 +16,92 @@ func (gui *Gui) handleCreateResetMenu() error { nukeStr = fmt.Sprintf("%s (%s)", nukeStr, gui.Tr.LcAndResetSubmodules) } - menuItems := []*menuItem{ + menuItems := []*popup.MenuItem{ { - displayStrings: []string{ + DisplayStrings: []string{ gui.Tr.LcDiscardAllChangesToAllFiles, red.Sprint(nukeStr), }, - onPress: func() error { + OnPress: func() error { gui.logAction(gui.Tr.Actions.NukeWorkingTree) if err := gui.Git.WorkingTree.ResetAndClean(); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) }, }, { - displayStrings: []string{ + DisplayStrings: []string{ gui.Tr.LcDiscardAnyUnstagedChanges, red.Sprint("git checkout -- ."), }, - onPress: func() error { + OnPress: func() error { gui.logAction(gui.Tr.Actions.DiscardUnstagedFileChanges) if err := gui.Git.WorkingTree.DiscardAnyUnstagedFileChanges(); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) }, }, { - displayStrings: []string{ + DisplayStrings: []string{ gui.Tr.LcDiscardUntrackedFiles, red.Sprint("git clean -fd"), }, - onPress: func() error { + OnPress: func() error { gui.logAction(gui.Tr.Actions.RemoveUntrackedFiles) if err := gui.Git.WorkingTree.RemoveUntrackedFiles(); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) }, }, { - displayStrings: []string{ + DisplayStrings: []string{ gui.Tr.LcSoftReset, red.Sprint("git reset --soft HEAD"), }, - onPress: func() error { + OnPress: func() error { gui.logAction(gui.Tr.Actions.SoftReset) if err := gui.Git.WorkingTree.ResetSoft("HEAD"); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) }, }, { - displayStrings: []string{ + DisplayStrings: []string{ "mixed reset", red.Sprint("git reset --mixed HEAD"), }, - onPress: func() error { + OnPress: func() error { gui.logAction(gui.Tr.Actions.MixedReset) if err := gui.Git.WorkingTree.ResetMixed("HEAD"); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) }, }, { - displayStrings: []string{ + DisplayStrings: []string{ gui.Tr.LcHardReset, red.Sprint("git reset --hard HEAD"), }, - onPress: func() error { + OnPress: func() error { gui.logAction(gui.Tr.Actions.HardReset) if err := gui.Git.WorkingTree.ResetHard("HEAD"); err != nil { - return gui.surfaceError(err) + return gui.PopupHandler.Error(err) } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}}) + return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) }, }, } - return gui.createMenu("", menuItems, createMenuOptions{showCancel: true}) + return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: "", Items: menuItems}) } diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go index 3bc9e34d9..fc2983dee 100644 --- a/pkg/i18n/chinese.go +++ b/pkg/i18n/chinese.go @@ -504,7 +504,6 @@ func chineseTranslationSet() TranslationSet { InitialiseSubmodule: "初始化子模块", BulkInitialiseSubmodules: "批量初始化子模块", BulkUpdateSubmodules: "批量更新子模块", - BulkStashAndResetSubmodules: "批量存储和重置子模块", BulkDeinitialiseSubmodules: "批量取消初始化子模块", UpdateSubmodule: "更新子模块", DeleteTag: "删除标签", diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 381baa57b..f3b90a194 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -540,7 +540,6 @@ type Actions struct { InitialiseSubmodule string BulkInitialiseSubmodules string BulkUpdateSubmodules string - BulkStashAndResetSubmodules string BulkDeinitialiseSubmodules string UpdateSubmodule string CreateLightweightTag string @@ -651,7 +650,7 @@ func EnglishTranslationSet() TranslationSet { NoBranchesThisRepo: "No branches for this repo", CommitMessageConfirm: "{{.keyBindClose}}: close, {{.keyBindNewLine}}: new line, {{.keyBindConfirm}}: confirm", CommitWithoutMessageErr: "You cannot commit without a commit message", - CloseConfirm: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm", + CloseConfirm: "{{.keyBindClose}}: close/cancel, {{.keyBindConfirm}}: confirm", LcClose: "close", LcQuit: "quit", LcSquashDown: "squash down", @@ -1097,7 +1096,6 @@ func EnglishTranslationSet() TranslationSet { InitialiseSubmodule: "Initialise submodule", BulkInitialiseSubmodules: "Bulk initialise submodules", BulkUpdateSubmodules: "Bulk update submodules", - BulkStashAndResetSubmodules: "Bulk stash and reset submodules", BulkDeinitialiseSubmodules: "Bulk deinitialise submodules", UpdateSubmodule: "Update submodule", DeleteTag: "Delete tag", diff --git a/pkg/updates/updates.go b/pkg/updates/updates.go index 1c52d0419..95fcfa0eb 100644 --- a/pkg/updates/updates.go +++ b/pkg/updates/updates.go @@ -144,12 +144,10 @@ func (u *Updater) CheckForNewUpdate(onFinish func(string, error) error, userRequ return } - go utils.Safe(func() { - newVersion, err := u.checkForNewUpdate() - if err = onFinish(newVersion, err); err != nil { - u.Log.Error(err) - } - }) + newVersion, err := u.checkForNewUpdate() + if err = onFinish(newVersion, err); err != nil { + u.Log.Error(err) + } } func (u *Updater) skipUpdateCheck() bool { diff --git a/pkg/utils/string_stack.go b/pkg/utils/string_stack.go new file mode 100644 index 000000000..c2d18c70c --- /dev/null +++ b/pkg/utils/string_stack.go @@ -0,0 +1,27 @@ +package utils + +type StringStack struct { + stack []string +} + +func (self *StringStack) Push(s string) { + self.stack = append(self.stack, s) +} + +func (self *StringStack) Pop() string { + if len(self.stack) == 0 { + return "" + } + n := len(self.stack) - 1 + last := self.stack[n] + self.stack = self.stack[:n] + return last +} + +func (self *StringStack) IsEmpty() bool { + return len(self.stack) == 0 +} + +func (self *StringStack) Clear() { + self.stack = []string{} +}