diff --git a/pkg/gui/context/traits/list_cursor.go b/pkg/gui/context/traits/list_cursor.go index 85cf22e30..368485c05 100644 --- a/pkg/gui/context/traits/list_cursor.go +++ b/pkg/gui/context/traits/list_cursor.go @@ -114,10 +114,18 @@ func (self *ListCursor) CancelRangeSelect() { self.rangeSelectMode = RangeSelectModeNone } +// Returns true if we are in range select mode. Note that we may be in range select +// mode and still only selecting a single item. See AreMultipleItemsSelected below. func (self *ListCursor) IsSelectingRange() bool { return self.rangeSelectMode != RangeSelectModeNone } +// Returns true if we are in range select mode and selecting multiple items +func (self *ListCursor) AreMultipleItemsSelected() bool { + startIdx, endIdx := self.GetSelectionRange() + return startIdx != endIdx +} + func (self *ListCursor) GetSelectionRange() (int, int) { if self.IsSelectingRange() { return utils.MinMax(self.selectedIdx, self.rangeStartIdx) diff --git a/pkg/gui/controllers/basic_commits_controller.go b/pkg/gui/controllers/basic_commits_controller.go index 2f120a0f4..386877b4d 100644 --- a/pkg/gui/controllers/basic_commits_controller.go +++ b/pkg/gui/controllers/basic_commits_controller.go @@ -22,50 +22,61 @@ type ContainsCommits interface { type BasicCommitsController struct { baseController + *ListControllerTrait[*models.Commit] c *ControllerCommon context ContainsCommits } -func NewBasicCommitsController(controllerCommon *ControllerCommon, context ContainsCommits) *BasicCommitsController { +func NewBasicCommitsController(c *ControllerCommon, context ContainsCommits) *BasicCommitsController { return &BasicCommitsController{ baseController: baseController{}, - c: controllerCommon, + c: c, context: context, + ListControllerTrait: NewListControllerTrait[*models.Commit]( + c, + context, + context.GetSelected, + ), } } func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { - Key: opts.GetKey(opts.Config.Commits.CheckoutCommit), - Handler: self.checkSelected(self.checkout), - Description: self.c.Tr.CheckoutCommit, + Key: opts.GetKey(opts.Config.Commits.CheckoutCommit), + Handler: self.withItem(self.checkout), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.CheckoutCommit, }, { - Key: opts.GetKey(opts.Config.Commits.CopyCommitAttributeToClipboard), - Handler: self.checkSelected(self.copyCommitAttribute), - Description: self.c.Tr.CopyCommitAttributeToClipboard, - OpensMenu: true, + Key: opts.GetKey(opts.Config.Commits.CopyCommitAttributeToClipboard), + Handler: self.withItem(self.copyCommitAttribute), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.CopyCommitAttributeToClipboard, + OpensMenu: true, }, { - Key: opts.GetKey(opts.Config.Commits.OpenInBrowser), - Handler: self.checkSelected(self.openInBrowser), - Description: self.c.Tr.OpenCommitInBrowser, + Key: opts.GetKey(opts.Config.Commits.OpenInBrowser), + Handler: self.withItem(self.openInBrowser), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.OpenCommitInBrowser, }, { - Key: opts.GetKey(opts.Config.Universal.New), - Handler: self.checkSelected(self.newBranch), - Description: self.c.Tr.CreateNewBranchFromCommit, + Key: opts.GetKey(opts.Config.Universal.New), + Handler: self.withItem(self.newBranch), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.CreateNewBranchFromCommit, }, { - Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), - Handler: self.checkSelected(self.createResetMenu), - Description: self.c.Tr.ViewResetOptions, - OpensMenu: true, + Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), + Handler: self.withItem(self.createResetMenu), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.ViewResetOptions, + OpensMenu: true, }, { Key: opts.GetKey(opts.Config.Commits.CherryPickCopy), - Handler: self.checkSelected(self.copyRange), + Handler: self.withItem(self.copyRange), Description: self.c.Tr.CherryPickCopy, }, { @@ -74,30 +85,16 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ Description: self.c.Tr.ResetCherryPick, }, { - Key: opts.GetKey(opts.Config.Universal.OpenDiffTool), - Handler: self.checkSelected(self.openDiffTool), - Description: self.c.Tr.OpenDiffTool, + Key: opts.GetKey(opts.Config.Universal.OpenDiffTool), + Handler: self.withItem(self.openDiffTool), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.OpenDiffTool, }, } return bindings } -func (self *BasicCommitsController) checkSelected(callback func(*models.Commit) error) func() error { - return func() error { - commit := self.context.GetSelected() - if commit == nil { - return nil - } - - return callback(commit) - } -} - -func (self *BasicCommitsController) Context() types.Context { - return self.context -} - func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) error { return self.c.Menu(types.CreateMenuOptions{ Title: self.c.Tr.Actions.CopyCommitAttributeToClipboard, diff --git a/pkg/gui/controllers/bisect_controller.go b/pkg/gui/controllers/bisect_controller.go index bff603afb..deb4f1b7a 100644 --- a/pkg/gui/controllers/bisect_controller.go +++ b/pkg/gui/controllers/bisect_controller.go @@ -14,17 +14,23 @@ import ( type BisectController struct { baseController + *ListControllerTrait[*models.Commit] c *ControllerCommon } var _ types.IController = &BisectController{} func NewBisectController( - common *ControllerCommon, + c *ControllerCommon, ) *BisectController { return &BisectController{ baseController: baseController{}, - c: common, + c: c, + ListControllerTrait: NewListControllerTrait[*models.Commit]( + c, + c.Contexts().LocalCommits, + c.Contexts().LocalCommits.GetSelected, + ), } } @@ -32,7 +38,7 @@ func (self *BisectController) GetKeybindings(opts types.KeybindingsOpts) []*type bindings := []*types.Binding{ { Key: opts.GetKey(opts.Config.Commits.ViewBisectOptions), - Handler: opts.Guards.OutsideFilterMode(self.checkSelected(self.openMenu)), + Handler: opts.Guards.OutsideFilterMode(self.withItem(self.openMenu)), Description: self.c.Tr.ViewBisectOptions, OpensMenu: true, }, @@ -70,9 +76,19 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c // If we have a current sha already, then we always want to use that one. If // not, we're still picking the initial commits before we really start, so // use the selected commit in that case. - shaToMark := lo.Ternary(info.GetCurrentSha() != "", info.GetCurrentSha(), commit.Sha) + + bisecting := info.GetCurrentSha() != "" + shaToMark := lo.Ternary(bisecting, info.GetCurrentSha(), commit.Sha) shortShaToMark := utils.ShortSha(shaToMark) + // For marking a commit as bad, when we're not already bisecting, we require + // a single item selected, but once we are bisecting, it doesn't matter because + // the action applies to the HEAD commit rather than the selected commit. + var singleItemIfNotBisecting *types.DisabledReason + if !bisecting { + singleItemIfNotBisecting = self.require(self.singleItemSelected())() + } + menuItems := []*types.MenuItem{ { Label: fmt.Sprintf(self.c.Tr.Bisect.Mark, shortShaToMark, info.NewTerm()), @@ -84,7 +100,8 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c return self.afterMark(selectCurrentAfter, waitToReselect) }, - Key: 'b', + DisabledReason: singleItemIfNotBisecting, + Key: 'b', }, { Label: fmt.Sprintf(self.c.Tr.Bisect.Mark, shortShaToMark, info.OldTerm()), @@ -96,7 +113,8 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c return self.afterMark(selectCurrentAfter, waitToReselect) }, - Key: 'g', + DisabledReason: singleItemIfNotBisecting, + Key: 'g', }, { Label: fmt.Sprintf(self.c.Tr.Bisect.SkipCurrent, shortShaToMark), @@ -108,7 +126,8 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c return self.afterMark(selectCurrentAfter, waitToReselect) }, - Key: 's', + DisabledReason: singleItemIfNotBisecting, + Key: 's', }, } if info.GetCurrentSha() != "" && info.GetCurrentSha() != commit.Sha { @@ -122,7 +141,8 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c return self.afterMark(selectCurrentAfter, waitToReselect) }, - Key: 'S', + DisabledReason: self.require(self.singleItemSelected())(), + Key: 'S', })) } menuItems = append(menuItems, lo.ToPtr(types.MenuItem{ @@ -157,7 +177,8 @@ func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo, return self.c.Helpers().Bisect.PostBisectCommandRefresh() }, - Key: 'b', + DisabledReason: self.require(self.singleItemSelected())(), + Key: 'b', }, { Label: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.OldTerm()), @@ -173,7 +194,8 @@ func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo, return self.c.Helpers().Bisect.PostBisectCommandRefresh() }, - Key: 'g', + DisabledReason: self.require(self.singleItemSelected())(), + Key: 'g', }, { Label: self.c.Tr.Bisect.ChooseTerms, @@ -273,21 +295,6 @@ func (self *BisectController) selectCurrentBisectCommit() { } } -func (self *BisectController) checkSelected(callback func(*models.Commit) error) func() error { - return func() error { - commit := self.context().GetSelected() - if commit == nil { - return nil - } - - return callback(commit) - } -} - -func (self *BisectController) Context() types.Context { - return self.context() -} - func (self *BisectController) context() *context.LocalCommitsContext { return self.c.Contexts().LocalCommits } diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index 8e762d78b..37a637202 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -17,48 +17,61 @@ import ( type BranchesController struct { baseController + *ListControllerTrait[*models.Branch] c *ControllerCommon } var _ types.IController = &BranchesController{} func NewBranchesController( - common *ControllerCommon, + c *ControllerCommon, ) *BranchesController { return &BranchesController{ baseController: baseController{}, - c: common, + c: c, + ListControllerTrait: NewListControllerTrait[*models.Branch]( + c, + c.Contexts().Branches, + c.Contexts().Branches.GetSelected, + ), } } func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { return []*types.Binding{ { - Key: opts.GetKey(opts.Config.Universal.Select), - Handler: self.checkSelected(self.press), - GetDisabledReason: self.getDisabledReasonForPress, - Description: self.c.Tr.Checkout, + Key: opts.GetKey(opts.Config.Universal.Select), + Handler: self.withItem(self.press), + GetDisabledReason: self.require( + self.singleItemSelected(), + self.notPulling, + ), + Description: self.c.Tr.Checkout, }, { - Key: opts.GetKey(opts.Config.Universal.New), - Handler: self.checkSelected(self.newBranch), - Description: self.c.Tr.NewBranch, + Key: opts.GetKey(opts.Config.Universal.New), + Handler: self.withItem(self.newBranch), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.NewBranch, }, { - Key: opts.GetKey(opts.Config.Branches.CreatePullRequest), - Handler: self.checkSelected(self.handleCreatePullRequest), - Description: self.c.Tr.CreatePullRequest, + Key: opts.GetKey(opts.Config.Branches.CreatePullRequest), + Handler: self.withItem(self.handleCreatePullRequest), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.CreatePullRequest, }, { - Key: opts.GetKey(opts.Config.Branches.ViewPullRequestOptions), - Handler: self.checkSelected(self.handleCreatePullRequestMenu), - Description: self.c.Tr.CreatePullRequestOptions, - OpensMenu: true, + Key: opts.GetKey(opts.Config.Branches.ViewPullRequestOptions), + Handler: self.withItem(self.handleCreatePullRequestMenu), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.CreatePullRequestOptions, + OpensMenu: true, }, { - Key: opts.GetKey(opts.Config.Branches.CopyPullRequestURL), - Handler: self.copyPullRequestURL, - Description: self.c.Tr.CopyPullRequestURL, + Key: opts.GetKey(opts.Config.Branches.CopyPullRequestURL), + Handler: self.copyPullRequestURL, + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.CopyPullRequestURL, }, { Key: opts.GetKey(opts.Config.Branches.CheckoutBranchByName), @@ -66,60 +79,69 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty Description: self.c.Tr.CheckoutByName, }, { - Key: opts.GetKey(opts.Config.Branches.ForceCheckoutBranch), - Handler: self.forceCheckout, - Description: self.c.Tr.ForceCheckout, + Key: opts.GetKey(opts.Config.Branches.ForceCheckoutBranch), + Handler: self.forceCheckout, + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.ForceCheckout, }, { - Key: opts.GetKey(opts.Config.Universal.Remove), - Handler: self.checkSelectedAndReal(self.delete), - Description: self.c.Tr.ViewDeleteOptions, - OpensMenu: true, + Key: opts.GetKey(opts.Config.Universal.Remove), + Handler: self.withItem(self.delete), + GetDisabledReason: self.require(self.singleItemSelected(self.branchIsReal)), + Description: self.c.Tr.ViewDeleteOptions, + OpensMenu: true, }, { - Key: opts.GetKey(opts.Config.Branches.RebaseBranch), - Handler: opts.Guards.OutsideFilterMode(self.rebase), - Description: self.c.Tr.RebaseBranch, - GetDisabledReason: self.getDisabledReasonForRebase, + Key: opts.GetKey(opts.Config.Branches.RebaseBranch), + Handler: opts.Guards.OutsideFilterMode(self.rebase), + GetDisabledReason: self.require( + self.singleItemSelected(self.notRebasingOntoSelf), + ), + Description: self.c.Tr.RebaseBranch, }, { - Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch), - Handler: opts.Guards.OutsideFilterMode(self.merge), - Description: self.c.Tr.MergeIntoCurrentBranch, + Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch), + Handler: opts.Guards.OutsideFilterMode(self.merge), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.MergeIntoCurrentBranch, }, { - Key: opts.GetKey(opts.Config.Branches.FastForward), - Handler: self.checkSelectedAndReal(self.fastForward), - Description: self.c.Tr.FastForward, + Key: opts.GetKey(opts.Config.Branches.FastForward), + Handler: self.withItem(self.fastForward), + GetDisabledReason: self.require(self.singleItemSelected(self.branchIsReal)), + Description: self.c.Tr.FastForward, }, { - Key: opts.GetKey(opts.Config.Branches.CreateTag), - Handler: self.checkSelected(self.createTag), - Description: self.c.Tr.CreateTag, + Key: opts.GetKey(opts.Config.Branches.CreateTag), + Handler: self.withItem(self.createTag), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.CreateTag, }, { Key: opts.GetKey(opts.Config.Branches.SortOrder), Handler: self.createSortMenu, Description: self.c.Tr.SortOrder, - OpensMenu: true, }, { - Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), - Handler: self.checkSelected(self.createResetMenu), - Description: self.c.Tr.ViewResetOptions, - OpensMenu: true, + Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), + Handler: self.withItem(self.createResetMenu), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.ViewResetOptions, + OpensMenu: true, }, { - Key: opts.GetKey(opts.Config.Branches.RenameBranch), - Handler: self.checkSelectedAndReal(self.rename), - Description: self.c.Tr.RenameBranch, + Key: opts.GetKey(opts.Config.Branches.RenameBranch), + Handler: self.withItem(self.rename), + GetDisabledReason: self.require(self.singleItemSelected(self.branchIsReal)), + Description: self.c.Tr.RenameBranch, }, { - Key: opts.GetKey(opts.Config.Branches.SetUpstream), - Handler: self.checkSelected(self.viewUpstreamOptions), - Description: self.c.Tr.ViewBranchUpstreamOptions, - Tooltip: self.c.Tr.ViewBranchUpstreamOptionsTooltip, - OpensMenu: true, + Key: opts.GetKey(opts.Config.Branches.SetUpstream), + Handler: self.withItem(self.viewUpstreamOptions), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.ViewBranchUpstreamOptions, + Tooltip: self.c.Tr.ViewBranchUpstreamOptionsTooltip, + OpensMenu: true, }, } } @@ -308,7 +330,7 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error { return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{}) } -func (self *BranchesController) getDisabledReasonForPress() *types.DisabledReason { +func (self *BranchesController) notPulling() *types.DisabledReason { currentBranch := self.c.Helpers().Refs.GetCheckedOutRef() if currentBranch != nil { op := self.c.State().GetItemOperation(currentBranch) @@ -561,8 +583,8 @@ func (self *BranchesController) rebase() error { return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName) } -func (self *BranchesController) getDisabledReasonForRebase() *types.DisabledReason { - selectedBranchName := self.context().GetSelected().Name +func (self *BranchesController) notRebasingOntoSelf(branch *models.Branch) *types.DisabledReason { + selectedBranchName := branch.Name checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name if selectedBranchName == checkedOutBranch { return &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf} @@ -753,24 +775,10 @@ func (self *BranchesController) createPullRequest(from string, to string) error return nil } -func (self *BranchesController) checkSelected(callback func(*models.Branch) error) func() error { - return func() error { - selectedItem := self.context().GetSelected() - if selectedItem == nil { - return nil - } - - return callback(selectedItem) +func (self *BranchesController) branchIsReal(branch *models.Branch) *types.DisabledReason { + if !branch.IsRealBranch() { + return &types.DisabledReason{Text: self.c.Tr.SelectedItemIsNotABranch} } -} -func (self *BranchesController) checkSelectedAndReal(callback func(*models.Branch) error) func() error { - return func() error { - selectedItem := self.context().GetSelected() - if selectedItem == nil || !selectedItem.IsRealBranch() { - return nil - } - - return callback(selectedItem) - } + return nil } diff --git a/pkg/gui/controllers/command_log_controller.go b/pkg/gui/controllers/command_log_controller.go index 0c3479914..92b6540be 100644 --- a/pkg/gui/controllers/command_log_controller.go +++ b/pkg/gui/controllers/command_log_controller.go @@ -12,11 +12,11 @@ type CommandLogController struct { var _ types.IController = &CommandLogController{} func NewCommandLogController( - common *ControllerCommon, + c *ControllerCommon, ) *CommandLogController { return &CommandLogController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/commit_description_controller.go b/pkg/gui/controllers/commit_description_controller.go index 13bb5949f..8f07cecfc 100644 --- a/pkg/gui/controllers/commit_description_controller.go +++ b/pkg/gui/controllers/commit_description_controller.go @@ -13,11 +13,11 @@ type CommitDescriptionController struct { var _ types.IController = &CommitMessageController{} func NewCommitDescriptionController( - common *ControllerCommon, + c *ControllerCommon, ) *CommitDescriptionController { return &CommitDescriptionController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/commit_message_controller.go b/pkg/gui/controllers/commit_message_controller.go index fc5aca970..c52a8038f 100644 --- a/pkg/gui/controllers/commit_message_controller.go +++ b/pkg/gui/controllers/commit_message_controller.go @@ -14,11 +14,11 @@ type CommitMessageController struct { var _ types.IController = &CommitMessageController{} func NewCommitMessageController( - common *ControllerCommon, + c *ControllerCommon, ) *CommitMessageController { return &CommitMessageController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 5b3097363..a5333e448 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -12,61 +12,74 @@ import ( type CommitFilesController struct { baseController + *ListControllerTrait[*filetree.CommitFileNode] c *ControllerCommon } var _ types.IController = &CommitFilesController{} func NewCommitFilesController( - common *ControllerCommon, + c *ControllerCommon, ) *CommitFilesController { return &CommitFilesController{ baseController: baseController{}, - c: common, + c: c, + ListControllerTrait: NewListControllerTrait[*filetree.CommitFileNode]( + c, + c.Contexts().CommitFiles, + c.Contexts().CommitFiles.GetSelected, + ), } } func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { - Key: opts.GetKey(opts.Config.CommitFiles.CheckoutCommitFile), - Handler: self.checkSelected(self.checkout), - Description: self.c.Tr.CheckoutCommitFile, + Key: opts.GetKey(opts.Config.CommitFiles.CheckoutCommitFile), + Handler: self.withItem(self.checkout), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.CheckoutCommitFile, }, { - Key: opts.GetKey(opts.Config.Universal.Remove), - Handler: self.checkSelected(self.discard), - Description: self.c.Tr.DiscardOldFileChange, + Key: opts.GetKey(opts.Config.Universal.Remove), + Handler: self.withItem(self.discard), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.DiscardOldFileChange, }, { - Key: opts.GetKey(opts.Config.Universal.OpenFile), - Handler: self.checkSelected(self.open), - Description: self.c.Tr.OpenFile, + Key: opts.GetKey(opts.Config.Universal.OpenFile), + Handler: self.withItem(self.open), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.OpenFile, }, { - Key: opts.GetKey(opts.Config.Universal.Edit), - Handler: self.checkSelected(self.edit), - Description: self.c.Tr.EditFile, + Key: opts.GetKey(opts.Config.Universal.Edit), + Handler: self.withItem(self.edit), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.EditFile, }, { - Key: opts.GetKey(opts.Config.Universal.OpenDiffTool), - Handler: self.checkSelected(self.openDiffTool), - Description: self.c.Tr.OpenDiffTool, + Key: opts.GetKey(opts.Config.Universal.OpenDiffTool), + Handler: self.withItem(self.openDiffTool), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.OpenDiffTool, }, { - Key: opts.GetKey(opts.Config.Universal.Select), - Handler: self.checkSelected(self.toggleForPatch), - Description: self.c.Tr.ToggleAddToPatch, + Key: opts.GetKey(opts.Config.Universal.Select), + Handler: self.withItem(self.toggleForPatch), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.ToggleAddToPatch, }, { Key: opts.GetKey(opts.Config.Files.ToggleStagedAll), - Handler: self.checkSelected(self.toggleAllForPatch), + Handler: self.withItem(self.toggleAllForPatch), Description: self.c.Tr.ToggleAllInPatch, }, { - Key: opts.GetKey(opts.Config.Universal.GoInto), - Handler: self.checkSelected(self.enter), - Description: self.c.Tr.EnterFile, + Key: opts.GetKey(opts.Config.Universal.GoInto), + Handler: self.withItem(self.enter), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.EnterFile, }, { Key: opts.GetKey(opts.Config.Files.ToggleTreeView), @@ -89,21 +102,6 @@ func (self *CommitFilesController) GetMouseKeybindings(opts types.KeybindingsOpt } } -func (self *CommitFilesController) checkSelected(callback func(*filetree.CommitFileNode) error) func() error { - return func() error { - selected := self.context().GetSelected() - if selected == nil { - return nil - } - - return callback(selected) - } -} - -func (self *CommitFilesController) Context() types.Context { - return self.context() -} - func (self *CommitFilesController) context() *context.CommitFilesContext { return self.c.Contexts().CommitFiles } diff --git a/pkg/gui/controllers/confirmation_controller.go b/pkg/gui/controllers/confirmation_controller.go index 59ddf8c43..164af19ec 100644 --- a/pkg/gui/controllers/confirmation_controller.go +++ b/pkg/gui/controllers/confirmation_controller.go @@ -13,11 +13,11 @@ type ConfirmationController struct { var _ types.IController = &ConfirmationController{} func NewConfirmationController( - common *ControllerCommon, + c *ControllerCommon, ) *ConfirmationController { return &ConfirmationController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/context_lines_controller.go b/pkg/gui/controllers/context_lines_controller.go index d3ff7688d..ddb507b31 100644 --- a/pkg/gui/controllers/context_lines_controller.go +++ b/pkg/gui/controllers/context_lines_controller.go @@ -31,11 +31,11 @@ type ContextLinesController struct { var _ types.IController = &ContextLinesController{} func NewContextLinesController( - common *ControllerCommon, + c *ControllerCommon, ) *ContextLinesController { return &ContextLinesController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/custom_patch_options_menu_action.go b/pkg/gui/controllers/custom_patch_options_menu_action.go index a8feed168..5710b44b7 100644 --- a/pkg/gui/controllers/custom_patch_options_menu_action.go +++ b/pkg/gui/controllers/custom_patch_options_menu_action.go @@ -62,15 +62,22 @@ func (self *CustomPatchOptionsMenuAction) Call() error { if self.c.CurrentContext().GetKey() == self.c.Contexts().LocalCommits.GetKey() { selectedCommit := self.c.Contexts().LocalCommits.GetSelected() if selectedCommit != nil && self.c.Git().Patch.PatchBuilder.To != selectedCommit.Sha { + + var disabledReason *types.DisabledReason + if self.c.Contexts().LocalCommits.AreMultipleItemsSelected() { + disabledReason = &types.DisabledReason{Text: self.c.Tr.RangeSelectNotSupported} + } + // adding this option to index 1 menuItems = append( menuItems[:1], append( []*types.MenuItem{ { - Label: fmt.Sprintf(self.c.Tr.MovePatchToSelectedCommit, selectedCommit.Sha), - OnPress: self.handleMovePatchToSelectedCommit, - Key: 'm', + Label: fmt.Sprintf(self.c.Tr.MovePatchToSelectedCommit, selectedCommit.Sha), + OnPress: self.handleMovePatchToSelectedCommit, + Key: 'm', + DisabledReason: disabledReason, }, }, menuItems[1:]..., )..., diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 7fb21dda8..3d418bf8e 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -13,25 +13,32 @@ import ( type FilesController struct { baseController // nolint: unused - c *ControllerCommon + *ListControllerTrait[*filetree.FileNode] + c *ControllerCommon } var _ types.IController = &FilesController{} func NewFilesController( - common *ControllerCommon, + c *ControllerCommon, ) *FilesController { return &FilesController{ - c: common, + c: c, + ListControllerTrait: NewListControllerTrait[*filetree.FileNode]( + c, + c.Contexts().Files, + c.Contexts().Files.GetSelected, + ), } } func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { return []*types.Binding{ { - Key: opts.GetKey(opts.Config.Universal.Select), - Handler: self.checkSelectedFileNode(self.press), - Description: self.c.Tr.ToggleStaged, + Key: opts.GetKey(opts.Config.Universal.Select), + Handler: self.withItem(self.press), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.ToggleStaged, }, { Key: opts.GetKey(opts.Config.Files.OpenStatusFilter), @@ -71,20 +78,23 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types Tooltip: self.c.Tr.FindBaseCommitForFixupTooltip, }, { - Key: opts.GetKey(opts.Config.Universal.Edit), - Handler: self.checkSelectedFileNode(self.edit), - Description: self.c.Tr.EditFile, + Key: opts.GetKey(opts.Config.Universal.Edit), + Handler: self.withItem(self.edit), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.EditFile, }, { - Key: opts.GetKey(opts.Config.Universal.OpenFile), - Handler: self.Open, - Description: self.c.Tr.OpenFile, + Key: opts.GetKey(opts.Config.Universal.OpenFile), + Handler: self.Open, + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.OpenFile, }, { - Key: opts.GetKey(opts.Config.Files.IgnoreFile), - Handler: self.checkSelectedFileNode(self.ignoreOrExcludeMenu), - Description: self.c.Tr.Actions.IgnoreExcludeFile, - OpensMenu: true, + Key: opts.GetKey(opts.Config.Files.IgnoreFile), + Handler: self.withItem(self.ignoreOrExcludeMenu), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.Actions.IgnoreExcludeFile, + OpensMenu: true, }, { Key: opts.GetKey(opts.Config.Files.RefreshFiles), @@ -108,9 +118,10 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types Description: self.c.Tr.ToggleStagedAll, }, { - Key: opts.GetKey(opts.Config.Universal.GoInto), - Handler: self.enter, - Description: self.c.Tr.FileEnter, + Key: opts.GetKey(opts.Config.Universal.GoInto), + Handler: self.enter, + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.FileEnter, }, { Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), @@ -130,9 +141,10 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types Description: self.c.Tr.ToggleTreeView, }, { - Key: opts.GetKey(opts.Config.Universal.OpenDiffTool), - Handler: self.checkSelectedFileNode(self.openDiffTool), - Description: self.c.Tr.OpenDiffTool, + Key: opts.GetKey(opts.Config.Universal.OpenDiffTool), + Handler: self.withItem(self.openDiffTool), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.OpenDiffTool, }, { Key: opts.GetKey(opts.Config.Files.OpenMergeTool), @@ -254,7 +266,7 @@ func (self *FilesController) GetOnRenderToMain() func() error { } func (self *FilesController) GetOnClick() func() error { - return self.checkSelectedFileNode(self.press) + return self.withItemGraceful(self.press) } // if we are dealing with a status for which there is no key in this map, @@ -411,17 +423,6 @@ func (self *FilesController) press(node *filetree.FileNode) error { return self.context().HandleFocus(types.OnFocusOpts{}) } -func (self *FilesController) checkSelectedFileNode(callback func(*filetree.FileNode) error) func() error { - return func() error { - node := self.context().GetSelected() - if node == nil { - return nil - } - - return callback(node) - } -} - func (self *FilesController) Context() types.Context { return self.context() } @@ -798,7 +799,8 @@ func (self *FilesController) openCopyMenu() error { self.c.Toast(self.c.Tr.FileNameCopiedToast) return nil }, - Key: 'n', + DisabledReason: self.require(self.singleItemSelected())(), + Key: 'n', } copyPathItem := &types.MenuItem{ Label: self.c.Tr.CopyFilePath, @@ -809,7 +811,8 @@ func (self *FilesController) openCopyMenu() error { self.c.Toast(self.c.Tr.FilePathCopiedToast) return nil }, - Key: 'p', + DisabledReason: self.require(self.singleItemSelected())(), + Key: 'p', } copyFileDiffItem := &types.MenuItem{ Label: self.c.Tr.CopySelectedDiff, @@ -827,6 +830,14 @@ func (self *FilesController) openCopyMenu() error { self.c.Toast(self.c.Tr.FileDiffCopiedToast) return nil }, + DisabledReason: self.require(self.singleItemSelected( + func(file *filetree.FileNode) *types.DisabledReason { + if !node.GetHasStagedOrTrackedChanges() { + return &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError} + } + return nil + }, + ))(), Key: 's', } copyAllDiff := &types.MenuItem{ @@ -844,21 +855,17 @@ func (self *FilesController) openCopyMenu() error { self.c.Toast(self.c.Tr.AllFilesDiffCopiedToast) return nil }, + DisabledReason: self.require( + func() *types.DisabledReason { + if !self.anyStagedOrTrackedFile() { + return &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError} + } + return nil + }, + )(), Key: 'a', } - if node == nil { - copyNameItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError} - copyPathItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError} - copyFileDiffItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError} - } - if node != nil && !node.GetHasStagedOrTrackedChanges() { - copyFileDiffItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError} - } - if !self.anyStagedOrTrackedFile() { - copyAllDiff.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError} - } - return self.c.Menu(types.CreateMenuOptions{ Title: self.c.Tr.CopyToClipboardMenu, Items: []*types.MenuItem{ diff --git a/pkg/gui/controllers/files_remove_controller.go b/pkg/gui/controllers/files_remove_controller.go index 2afa6e5a8..9b21557de 100644 --- a/pkg/gui/controllers/files_remove_controller.go +++ b/pkg/gui/controllers/files_remove_controller.go @@ -3,7 +3,6 @@ package controllers import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/filetree" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" @@ -13,27 +12,34 @@ import ( type FilesRemoveController struct { baseController + *ListControllerTrait[*filetree.FileNode] c *ControllerCommon } var _ types.IController = &FilesRemoveController{} func NewFilesRemoveController( - common *ControllerCommon, + c *ControllerCommon, ) *FilesRemoveController { return &FilesRemoveController{ baseController: baseController{}, - c: common, + c: c, + ListControllerTrait: NewListControllerTrait[*filetree.FileNode]( + c, + c.Contexts().Files, + c.Contexts().Files.GetSelected, + ), } } func (self *FilesRemoveController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { - Key: opts.GetKey(opts.Config.Universal.Remove), - Handler: self.checkSelectedFileNode(self.remove), - Description: self.c.Tr.ViewDiscardOptions, - OpensMenu: true, + Key: opts.GetKey(opts.Config.Universal.Remove), + Handler: self.withItem(self.remove), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.ViewDiscardOptions, + OpensMenu: true, }, } @@ -166,22 +172,3 @@ func (self *FilesRemoveController) ResetSubmodule(submodule *models.SubmoduleCon return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.SUBMODULES}}) }) } - -func (self *FilesRemoveController) checkSelectedFileNode(callback func(*filetree.FileNode) error) func() error { - return func() error { - node := self.context().GetSelected() - if node == nil { - return nil - } - - return callback(node) - } -} - -func (self *FilesRemoveController) Context() types.Context { - return self.context() -} - -func (self *FilesRemoveController) context() *context.WorkingTreeContext { - return self.c.Contexts().Files -} diff --git a/pkg/gui/controllers/git_flow_controller.go b/pkg/gui/controllers/git_flow_controller.go index 4086a142b..c8da4bd0c 100644 --- a/pkg/gui/controllers/git_flow_controller.go +++ b/pkg/gui/controllers/git_flow_controller.go @@ -4,24 +4,29 @@ import ( "fmt" "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) type GitFlowController struct { baseController + *ListControllerTrait[*models.Branch] c *ControllerCommon } var _ types.IController = &GitFlowController{} func NewGitFlowController( - common *ControllerCommon, + c *ControllerCommon, ) *GitFlowController { return &GitFlowController{ baseController: baseController{}, - c: common, + ListControllerTrait: NewListControllerTrait[*models.Branch]( + c, + c.Contexts().Branches, + c.Contexts().Branches.GetSelected, + ), + c: c, } } @@ -29,7 +34,7 @@ func (self *GitFlowController) GetKeybindings(opts types.KeybindingsOpts) []*typ bindings := []*types.Binding{ { Key: opts.GetKey(opts.Config.Branches.ViewGitFlowOptions), - Handler: self.checkSelected(self.handleCreateGitFlowMenu), + Handler: self.withItem(self.handleCreateGitFlowMenu), Description: self.c.Tr.GitFlowOptions, OpensMenu: true, }, @@ -68,6 +73,7 @@ func (self *GitFlowController) handleCreateGitFlowMenu(branch *models.Branch) er OnPress: func() error { return self.gitFlowFinishBranch(branch.Name) }, + DisabledReason: self.require(self.singleItemSelected())(), }, { Label: "start feature", @@ -102,22 +108,3 @@ func (self *GitFlowController) gitFlowFinishBranch(branchName string) error { self.c.LogAction(self.c.Tr.Actions.GitFlowFinish) return self.c.RunSubprocessAndRefresh(cmdObj) } - -func (self *GitFlowController) checkSelected(callback func(*models.Branch) error) func() error { - return func() error { - node := self.context().GetSelected() - if node == nil { - return nil - } - - return callback(node) - } -} - -func (self *GitFlowController) Context() types.Context { - return self.context() -} - -func (self *GitFlowController) context() *context.BranchesContext { - return self.c.Contexts().Branches -} diff --git a/pkg/gui/controllers/global_controller.go b/pkg/gui/controllers/global_controller.go index 2942567e8..f8e9b3e6b 100644 --- a/pkg/gui/controllers/global_controller.go +++ b/pkg/gui/controllers/global_controller.go @@ -11,11 +11,11 @@ type GlobalController struct { } func NewGlobalController( - common *ControllerCommon, + c *ControllerCommon, ) *GlobalController { return &GlobalController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/jump_to_side_window_controller.go b/pkg/gui/controllers/jump_to_side_window_controller.go index 7ac407ab4..a3985968f 100644 --- a/pkg/gui/controllers/jump_to_side_window_controller.go +++ b/pkg/gui/controllers/jump_to_side_window_controller.go @@ -14,11 +14,11 @@ type JumpToSideWindowController struct { } func NewJumpToSideWindowController( - common *ControllerCommon, + c *ControllerCommon, ) *JumpToSideWindowController { return &JumpToSideWindowController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/list_controller_trait.go b/pkg/gui/controllers/list_controller_trait.go new file mode 100644 index 000000000..fa60223b9 --- /dev/null +++ b/pkg/gui/controllers/list_controller_trait.go @@ -0,0 +1,95 @@ +package controllers + +import "github.com/jesseduffield/lazygit/pkg/gui/types" + +// Embed this into your list controller to get some convenience methods for +// ensuring a single item is selected, etc. + +type ListControllerTrait[T comparable] struct { + c *ControllerCommon + context types.IListContext + getSelected func() T +} + +func NewListControllerTrait[T comparable]( + c *ControllerCommon, + context types.IListContext, + getSelected func() T, +) *ListControllerTrait[T] { + return &ListControllerTrait[T]{ + c: c, + context: context, + getSelected: getSelected, + } +} + +// Convenience function for combining multiple disabledReason callbacks. +// The first callback to return a disabled reason will be the one returned. +func (self *ListControllerTrait[T]) require(callbacks ...func() *types.DisabledReason) func() *types.DisabledReason { + return func() *types.DisabledReason { + for _, callback := range callbacks { + if disabledReason := callback(); disabledReason != nil { + return disabledReason + } + } + + return nil + } +} + +// Convenience function for enforcing that a single item is selected. +// Also takes callbacks for additional disabled reasons, and passes the selected +// item into each one. +func (self *ListControllerTrait[T]) singleItemSelected(callbacks ...func(T) *types.DisabledReason) func() *types.DisabledReason { + return func() *types.DisabledReason { + if self.context.GetList().AreMultipleItemsSelected() { + return &types.DisabledReason{Text: self.c.Tr.RangeSelectNotSupported} + } + + var zeroValue T + item := self.getSelected() + if item == zeroValue { + return &types.DisabledReason{Text: self.c.Tr.NoItemSelected} + } + + for _, callback := range callbacks { + if reason := callback(item); reason != nil { + return reason + } + } + + return nil + } +} + +// Passes the selected item to the callback. Used for handler functions. +func (self *ListControllerTrait[T]) withItem(callback func(T) error) func() error { + return func() error { + var zeroValue T + commit := self.getSelected() + if commit == zeroValue { + return self.c.ErrorMsg(self.c.Tr.NoItemSelected) + } + + return callback(commit) + } +} + +// Like withItem, but doesn't show an error message if no item is selected. +// Use this for click actions (it's a no-op to click empty space) +func (self *ListControllerTrait[T]) withItemGraceful(callback func(T) error) func() error { + return func() error { + var zeroValue T + commit := self.getSelected() + if commit == zeroValue { + return nil + } + + return callback(commit) + } +} + +// All controllers must implement this method so we're defining it here for convenience +func (self *ListControllerTrait[T]) Context() types.Context { + return self.context +} diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index c484d2487..5fe08b85e 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -25,6 +25,7 @@ type ( type LocalCommitsController struct { baseController + *ListControllerTrait[*models.Commit] c *ControllerCommon pullFiles PullFilesFn @@ -33,13 +34,18 @@ type LocalCommitsController struct { var _ types.IController = &LocalCommitsController{} func NewLocalCommitsController( - common *ControllerCommon, + c *ControllerCommon, pullFiles PullFilesFn, ) *LocalCommitsController { return &LocalCommitsController{ baseController: baseController{}, - c: common, + c: c, pullFiles: pullFiles, + ListControllerTrait: NewListControllerTrait[*models.Commit]( + c, + c.Contexts().LocalCommits, + c.Contexts().LocalCommits.GetSelected, + ), } } @@ -48,47 +54,59 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ outsideFilterModeBindings := []*types.Binding{ { - Key: opts.GetKey(opts.Config.Commits.SquashDown), - Handler: self.checkSelected(self.squashDown), - GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForSquashDown), - Description: self.c.Tr.SquashDown, + Key: opts.GetKey(opts.Config.Commits.SquashDown), + Handler: self.withItem(self.squashDown), + GetDisabledReason: self.require( + self.singleItemSelected(self.getDisabledReasonForSquashDown), + ), + Description: self.c.Tr.SquashDown, }, { - Key: opts.GetKey(opts.Config.Commits.MarkCommitAsFixup), - Handler: self.checkSelected(self.fixup), - GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForFixup), - Description: self.c.Tr.FixupCommit, + Key: opts.GetKey(opts.Config.Commits.MarkCommitAsFixup), + Handler: self.withItem(self.fixup), + GetDisabledReason: self.require( + self.singleItemSelected(self.getDisabledReasonForFixup), + ), + Description: self.c.Tr.FixupCommit, }, { - Key: opts.GetKey(opts.Config.Commits.RenameCommit), - Handler: self.checkSelected(self.reword), - GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Reword), - Description: self.c.Tr.RewordCommit, + Key: opts.GetKey(opts.Config.Commits.RenameCommit), + Handler: self.withItem(self.reword), + GetDisabledReason: self.require( + self.singleItemSelected(self.rebaseCommandEnabled(todo.Reword)), + ), + Description: self.c.Tr.RewordCommit, }, { - Key: opts.GetKey(opts.Config.Commits.RenameCommitWithEditor), - Handler: self.checkSelected(self.rewordEditor), - GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Reword), - Description: self.c.Tr.RenameCommitEditor, + Key: opts.GetKey(opts.Config.Commits.RenameCommitWithEditor), + Handler: self.withItem(self.rewordEditor), + GetDisabledReason: self.require( + self.singleItemSelected(self.rebaseCommandEnabled(todo.Reword)), + ), + Description: self.c.Tr.RenameCommitEditor, }, { - Key: opts.GetKey(opts.Config.Universal.Remove), - Handler: self.checkSelected(self.drop), - GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Drop), - Description: self.c.Tr.DeleteCommit, + Key: opts.GetKey(opts.Config.Universal.Remove), + Handler: self.withItem(self.drop), + GetDisabledReason: self.require( + self.singleItemSelected(self.rebaseCommandEnabled(todo.Drop)), + ), + Description: self.c.Tr.DeleteCommit, }, { - Key: opts.GetKey(editCommitKey), - Handler: self.checkSelected(self.edit), - GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Edit), - Description: self.c.Tr.EditCommit, + Key: opts.GetKey(editCommitKey), + Handler: self.withItem(self.edit), + GetDisabledReason: self.require( + self.singleItemSelected(self.rebaseCommandEnabled(todo.Edit)), + ), + Description: self.c.Tr.EditCommit, }, { // The user-facing description here is 'Start interactive rebase' but internally // we're calling it 'quick-start interactive rebase' to differentiate it from // when you manually select the base commit. Key: opts.GetKey(opts.Config.Commits.StartInteractiveRebase), - Handler: self.checkSelected(self.quickStartInteractiveRebase), + Handler: self.withItem(self.quickStartInteractiveRebase), GetDisabledReason: self.require(self.notMidRebase, self.canFindCommitForQuickStart), Description: self.c.Tr.QuickStartInteractiveRebase, Tooltip: utils.ResolvePlaceholderString(self.c.Tr.QuickStartInteractiveRebaseTooltip, map[string]string{ @@ -96,45 +114,50 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }), }, { - Key: opts.GetKey(opts.Config.Commits.PickCommit), - Handler: self.checkSelected(self.pick), - GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Pick), - Description: self.c.Tr.PickCommit, + Key: opts.GetKey(opts.Config.Commits.PickCommit), + Handler: self.withItem(self.pick), + GetDisabledReason: self.require( + self.singleItemSelected(self.rebaseCommandEnabled(todo.Pick)), + ), + Description: self.c.Tr.PickCommit, }, { Key: opts.GetKey(opts.Config.Commits.CreateFixupCommit), - Handler: self.checkSelected(self.createFixupCommit), - GetDisabledReason: self.disabledIfNoSelectedCommit(), + Handler: self.withItem(self.createFixupCommit), + GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.CreateFixupCommitDescription, }, { - Key: opts.GetKey(opts.Config.Commits.SquashAboveCommits), - Handler: self.checkSelected(self.squashAllAboveFixupCommits), - GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForSquashAllAboveFixupCommits), - Description: self.c.Tr.SquashAboveCommits, + Key: opts.GetKey(opts.Config.Commits.SquashAboveCommits), + Handler: self.withItem(self.squashAllAboveFixupCommits), + GetDisabledReason: self.require( + self.notMidRebase, + self.singleItemSelected(), + ), + Description: self.c.Tr.SquashAboveCommits, }, { Key: opts.GetKey(opts.Config.Commits.MoveDownCommit), - Handler: self.checkSelected(self.moveDown), - GetDisabledReason: self.disabledIfNoSelectedCommit(), + Handler: self.withItem(self.moveDown), + GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.MoveDownCommit, }, { Key: opts.GetKey(opts.Config.Commits.MoveUpCommit), - Handler: self.checkSelected(self.moveUp), - GetDisabledReason: self.disabledIfNoSelectedCommit(), + Handler: self.withItem(self.moveUp), + GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.MoveUpCommit, }, { Key: opts.GetKey(opts.Config.Commits.PasteCommits), Handler: self.paste, - GetDisabledReason: self.getDisabledReasonForPaste, + GetDisabledReason: self.require(self.canPaste), Description: self.c.Tr.PasteCommits, }, { Key: opts.GetKey(opts.Config.Commits.MarkCommitAsBaseForRebase), - Handler: self.checkSelected(self.markAsBaseCommit), - GetDisabledReason: self.disabledIfNoSelectedCommit(), + Handler: self.withItem(self.markAsBaseCommit), + GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.MarkAsBaseCommit, Tooltip: self.c.Tr.MarkAsBaseCommitTooltip, }, @@ -161,27 +184,27 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ bindings := append(outsideFilterModeBindings, []*types.Binding{ { Key: opts.GetKey(opts.Config.Commits.AmendToCommit), - Handler: self.checkSelected(self.amendTo), - GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForAmendTo), + Handler: self.withItem(self.amendTo), + GetDisabledReason: self.require(self.singleItemSelected(self.canAmend)), Description: self.c.Tr.AmendToCommit, }, { Key: opts.GetKey(opts.Config.Commits.ResetCommitAuthor), - Handler: self.checkSelected(self.amendAttribute), - GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForAmendTo), + Handler: self.withItem(self.amendAttribute), + GetDisabledReason: self.require(self.singleItemSelected(self.canAmend)), Description: self.c.Tr.SetResetCommitAuthor, OpensMenu: true, }, { Key: opts.GetKey(opts.Config.Commits.RevertCommit), - Handler: self.checkSelected(self.revert), - GetDisabledReason: self.disabledIfNoSelectedCommit(), + Handler: self.withItem(self.revert), + GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.RevertCommit, }, { Key: opts.GetKey(opts.Config.Commits.CreateTag), - Handler: self.checkSelected(self.createTag), - GetDisabledReason: self.disabledIfNoSelectedCommit(), + Handler: self.withItem(self.createTag), + GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.TagCommit, }, { @@ -266,7 +289,7 @@ func (self *LocalCommitsController) getDisabledReasonForSquashDown(commit *model return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit} } - return self.rebaseCommandEnabled(todo.Squash, commit) + return self.rebaseCommandEnabled(todo.Squash)(commit) } func (self *LocalCommitsController) fixup(commit *models.Commit) error { @@ -295,7 +318,7 @@ func (self *LocalCommitsController) getDisabledReasonForFixup(commit *models.Com return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit} } - return self.rebaseCommandEnabled(todo.Squash, commit) + return self.rebaseCommandEnabled(todo.Squash)(commit) } func (self *LocalCommitsController) reword(commit *models.Commit) error { @@ -528,36 +551,38 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoComma }) } -func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand, commit *models.Commit) *types.DisabledReason { - if commit.Action == models.ActionConflict { - return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed} - } +func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand) func(*models.Commit) *types.DisabledReason { + return func(commit *models.Commit) *types.DisabledReason { + if commit.Action == models.ActionConflict { + return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed} + } - if !commit.IsTODO() { - if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { - // If we are in a rebase, the only action that is allowed for - // non-todo commits is rewording the current head commit - if !(action == todo.Reword && self.isHeadCommit()) { - return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} + if !commit.IsTODO() { + if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { + // If we are in a rebase, the only action that is allowed for + // non-todo commits is rewording the current head commit + if !(action == todo.Reword && self.isHeadCommit()) { + return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} + } } + + return nil + } + + // for now we do not support setting 'reword' because it requires an editor + // and that means we either unconditionally wait around for the subprocess to ask for + // our input or we set a lazygit client as the EDITOR env variable and have it + // request us to edit the commit message when prompted. + if action == todo.Reword { + return &types.DisabledReason{Text: self.c.Tr.RewordNotSupported} + } + + if allowed := isChangeOfRebaseTodoAllowed(action); !allowed { + return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed} } return nil } - - // for now we do not support setting 'reword' because it requires an editor - // and that means we either unconditionally wait around for the subprocess to ask for - // our input or we set a lazygit client as the EDITOR env variable and have it - // request us to edit the commit message when prompted. - if action == todo.Reword { - return &types.DisabledReason{Text: self.c.Tr.RewordNotSupported} - } - - if allowed := isChangeOfRebaseTodoAllowed(action); !allowed { - return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed} - } - - return nil } func (self *LocalCommitsController) moveDown(commit *models.Commit) error { @@ -687,7 +712,7 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error { }) } -func (self *LocalCommitsController) getDisabledReasonForAmendTo(commit *models.Commit) *types.DisabledReason { +func (self *LocalCommitsController) canAmend(commit *models.Commit) *types.DisabledReason { if !self.isHeadCommit() && self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} } @@ -870,14 +895,6 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co }) } -func (self *LocalCommitsController) getDisabledReasonForSquashAllAboveFixupCommits(commit *models.Commit) *types.DisabledReason { - if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { - return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} - } - - return nil -} - // For getting disabled reason func (self *LocalCommitsController) notMidRebase() *types.DisabledReason { if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { @@ -1016,39 +1033,6 @@ func (self *LocalCommitsController) handleOpenLogMenu() error { }) } -func (self *LocalCommitsController) checkSelected(callback func(*models.Commit) error) func() error { - return func() error { - commit := self.context().GetSelected() - if commit == nil { - // The enabled callback should have checked for this - panic("no commit selected") - } - - return callback(commit) - } -} - -func (self *LocalCommitsController) callGetDisabledReasonFuncWithSelectedCommit(callback func(*models.Commit) *types.DisabledReason) func() *types.DisabledReason { - return func() *types.DisabledReason { - commit := self.context().GetSelected() - if commit == nil { - return &types.DisabledReason{Text: self.c.Tr.NoCommitSelected} - } - - return callback(commit) - } -} - -func (self *LocalCommitsController) disabledIfNoSelectedCommit() func() *types.DisabledReason { - return self.callGetDisabledReasonFuncWithSelectedCommit(func(*models.Commit) *types.DisabledReason { return nil }) -} - -func (self *LocalCommitsController) getDisabledReasonForRebaseCommandWithSelectedCommit(action todo.TodoCommand) func() *types.DisabledReason { - return self.callGetDisabledReasonFuncWithSelectedCommit(func(commit *models.Commit) *types.DisabledReason { - return self.rebaseCommandEnabled(action, commit) - }) -} - func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) error { return func(types.OnFocusOpts) error { context := self.context() @@ -1065,10 +1049,6 @@ func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) error { } } -func (self *LocalCommitsController) Context() types.Context { - return self.context() -} - func (self *LocalCommitsController) context() *context.LocalCommitsContext { return self.c.Contexts().LocalCommits } @@ -1077,7 +1057,7 @@ func (self *LocalCommitsController) paste() error { return self.c.Helpers().CherryPick.Paste() } -func (self *LocalCommitsController) getDisabledReasonForPaste() *types.DisabledReason { +func (self *LocalCommitsController) canPaste() *types.DisabledReason { if !self.c.Helpers().CherryPick.CanPaste() { return &types.DisabledReason{Text: self.c.Tr.NoCopiedCommits} } @@ -1099,19 +1079,6 @@ func (self *LocalCommitsController) isHeadCommit() bool { return models.IsHeadCommit(self.c.Model().Commits, self.context().GetSelectedLineIdx()) } -// Convenience function for composing multiple disabled reason functions -func (self *LocalCommitsController) require(callbacks ...func() *types.DisabledReason) func() *types.DisabledReason { - return func() *types.DisabledReason { - for _, callback := range callbacks { - if disabledReason := callback(); disabledReason != nil { - return disabledReason - } - } - - return nil - } -} - func isChangeOfRebaseTodoAllowed(action todo.TodoCommand) bool { allowedActions := []todo.TodoCommand{ todo.Pick, diff --git a/pkg/gui/controllers/menu_controller.go b/pkg/gui/controllers/menu_controller.go index 0af32ef71..133840cef 100644 --- a/pkg/gui/controllers/menu_controller.go +++ b/pkg/gui/controllers/menu_controller.go @@ -7,17 +7,23 @@ import ( type MenuController struct { baseController + *ListControllerTrait[*types.MenuItem] c *ControllerCommon } var _ types.IController = &MenuController{} func NewMenuController( - common *ControllerCommon, + c *ControllerCommon, ) *MenuController { return &MenuController{ baseController: baseController{}, - c: common, + ListControllerTrait: NewListControllerTrait[*types.MenuItem]( + c, + c.Contexts().Menu, + c.Contexts().Menu.GetSelected, + ), + c: c, } } @@ -26,14 +32,16 @@ func NewMenuController( func (self *MenuController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { - Key: opts.GetKey(opts.Config.Universal.Select), - Handler: self.press, + Key: opts.GetKey(opts.Config.Universal.Select), + Handler: self.withItem(self.press), + GetDisabledReason: self.require(self.singleItemSelected()), }, { - Key: opts.GetKey(opts.Config.Universal.Confirm), - Handler: self.press, - Description: self.c.Tr.Execute, - Display: true, + Key: opts.GetKey(opts.Config.Universal.Confirm), + Handler: self.withItem(self.press), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.Execute, + Display: true, }, { Key: opts.GetKey(opts.Config.Universal.Return), @@ -47,7 +55,7 @@ func (self *MenuController) GetKeybindings(opts types.KeybindingsOpts) []*types. } func (self *MenuController) GetOnClick() func() error { - return self.press + return self.withItemGraceful(self.press) } func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error { @@ -60,8 +68,8 @@ func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error { } } -func (self *MenuController) press() error { - return self.context().OnMenuPress(self.context().GetSelected()) +func (self *MenuController) press(selectedItem *types.MenuItem) error { + return self.context().OnMenuPress(selectedItem) } func (self *MenuController) close() error { @@ -73,10 +81,6 @@ func (self *MenuController) close() error { return self.c.PopContext() } -func (self *MenuController) Context() types.Context { - return self.context() -} - func (self *MenuController) context() *context.MenuContext { return self.c.Contexts().Menu } diff --git a/pkg/gui/controllers/merge_conflicts_controller.go b/pkg/gui/controllers/merge_conflicts_controller.go index 450cd1816..730826ba8 100644 --- a/pkg/gui/controllers/merge_conflicts_controller.go +++ b/pkg/gui/controllers/merge_conflicts_controller.go @@ -17,11 +17,11 @@ type MergeConflictsController struct { var _ types.IController = &MergeConflictsController{} func NewMergeConflictsController( - common *ControllerCommon, + c *ControllerCommon, ) *MergeConflictsController { return &MergeConflictsController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/patch_building_controller.go b/pkg/gui/controllers/patch_building_controller.go index ff5bd777b..dcef64677 100644 --- a/pkg/gui/controllers/patch_building_controller.go +++ b/pkg/gui/controllers/patch_building_controller.go @@ -14,11 +14,11 @@ type PatchBuildingController struct { var _ types.IController = &PatchBuildingController{} func NewPatchBuildingController( - common *ControllerCommon, + c *ControllerCommon, ) *PatchBuildingController { return &PatchBuildingController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/reflog_commits_controller.go b/pkg/gui/controllers/reflog_commits_controller.go index 9cd5dd050..6e0228784 100644 --- a/pkg/gui/controllers/reflog_commits_controller.go +++ b/pkg/gui/controllers/reflog_commits_controller.go @@ -1,23 +1,30 @@ package controllers import ( + "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) type ReflogCommitsController struct { baseController + *ListControllerTrait[*models.Commit] c *ControllerCommon } var _ types.IController = &ReflogCommitsController{} func NewReflogCommitsController( - common *ControllerCommon, + c *ControllerCommon, ) *ReflogCommitsController { return &ReflogCommitsController{ baseController: baseController{}, - c: common, + ListControllerTrait: NewListControllerTrait[*models.Commit]( + c, + c.Contexts().ReflogCommits, + c.Contexts().ReflogCommits.GetSelected, + ), + c: c, } } diff --git a/pkg/gui/controllers/remote_branches_controller.go b/pkg/gui/controllers/remote_branches_controller.go index d6f9e9036..25797003b 100644 --- a/pkg/gui/controllers/remote_branches_controller.go +++ b/pkg/gui/controllers/remote_branches_controller.go @@ -11,17 +11,23 @@ import ( type RemoteBranchesController struct { baseController + *ListControllerTrait[*models.RemoteBranch] c *ControllerCommon } var _ types.IController = &RemoteBranchesController{} func NewRemoteBranchesController( - common *ControllerCommon, + c *ControllerCommon, ) *RemoteBranchesController { return &RemoteBranchesController{ baseController: baseController{}, - c: common, + ListControllerTrait: NewListControllerTrait[*models.RemoteBranch]( + c, + c.Contexts().RemoteBranches, + c.Contexts().RemoteBranches.GetSelected, + ), + c: c, } } @@ -30,33 +36,39 @@ func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts) { Key: opts.GetKey(opts.Config.Universal.Select), // gonna use the exact same handler as the 'n' keybinding because everybody wants this to happen when they checkout a remote branch - Handler: self.checkSelected(self.newLocalBranch), - Description: self.c.Tr.Checkout, + Handler: self.withItem(self.newLocalBranch), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.Checkout, }, { - Key: opts.GetKey(opts.Config.Universal.New), - Handler: self.checkSelected(self.newLocalBranch), - Description: self.c.Tr.NewBranch, + Key: opts.GetKey(opts.Config.Universal.New), + Handler: self.withItem(self.newLocalBranch), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.NewBranch, }, { - Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch), - Handler: opts.Guards.OutsideFilterMode(self.checkSelected(self.merge)), - Description: self.c.Tr.MergeIntoCurrentBranch, + Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch), + Handler: opts.Guards.OutsideFilterMode(self.withItem(self.merge)), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.MergeIntoCurrentBranch, }, { - Key: opts.GetKey(opts.Config.Branches.RebaseBranch), - Handler: opts.Guards.OutsideFilterMode(self.checkSelected(self.rebase)), - Description: self.c.Tr.RebaseBranch, + Key: opts.GetKey(opts.Config.Branches.RebaseBranch), + Handler: opts.Guards.OutsideFilterMode(self.withItem(self.rebase)), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.RebaseBranch, }, { - Key: opts.GetKey(opts.Config.Universal.Remove), - Handler: self.checkSelected(self.delete), - Description: self.c.Tr.DeleteRemoteTag, + Key: opts.GetKey(opts.Config.Universal.Remove), + Handler: self.withItem(self.delete), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.DeleteRemoteTag, }, { - Key: opts.GetKey(opts.Config.Branches.SetUpstream), - Handler: self.checkSelected(self.setAsUpstream), - Description: self.c.Tr.SetAsUpstream, + Key: opts.GetKey(opts.Config.Branches.SetUpstream), + Handler: self.withItem(self.setAsUpstream), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.SetAsUpstream, }, { Key: opts.GetKey(opts.Config.Branches.SortOrder), @@ -65,10 +77,11 @@ func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts) OpensMenu: true, }, { - Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), - Handler: self.checkSelected(self.createResetMenu), - Description: self.c.Tr.ViewResetOptions, - OpensMenu: true, + Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), + Handler: self.withItem(self.createResetMenu), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.ViewResetOptions, + OpensMenu: true, }, } } @@ -96,25 +109,10 @@ func (self *RemoteBranchesController) GetOnRenderToMain() func() error { } } -func (self *RemoteBranchesController) Context() types.Context { - return self.context() -} - func (self *RemoteBranchesController) context() *context.RemoteBranchesContext { return self.c.Contexts().RemoteBranches } -func (self *RemoteBranchesController) checkSelected(callback func(*models.RemoteBranch) error) func() error { - return func() error { - selectedItem := self.context().GetSelected() - if selectedItem == nil { - return nil - } - - return callback(selectedItem) - } -} - func (self *RemoteBranchesController) delete(selectedBranch *models.RemoteBranch) error { return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(selectedBranch.RemoteName, selectedBranch.Name) } diff --git a/pkg/gui/controllers/remotes_controller.go b/pkg/gui/controllers/remotes_controller.go index 47f14417e..ebd232935 100644 --- a/pkg/gui/controllers/remotes_controller.go +++ b/pkg/gui/controllers/remotes_controller.go @@ -14,6 +14,7 @@ import ( type RemotesController struct { baseController + *ListControllerTrait[*models.Remote] c *ControllerCommon setRemoteBranches func([]*models.RemoteBranch) @@ -22,12 +23,17 @@ type RemotesController struct { var _ types.IController = &RemotesController{} func NewRemotesController( - common *ControllerCommon, + c *ControllerCommon, setRemoteBranches func([]*models.RemoteBranch), ) *RemotesController { return &RemotesController{ - baseController: baseController{}, - c: common, + baseController: baseController{}, + ListControllerTrait: NewListControllerTrait[*models.Remote]( + c, + c.Contexts().Remotes, + c.Contexts().Remotes.GetSelected, + ), + c: c, setRemoteBranches: setRemoteBranches, } } @@ -35,13 +41,15 @@ func NewRemotesController( func (self *RemotesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { - Key: opts.GetKey(opts.Config.Universal.GoInto), - Handler: self.checkSelected(self.enter), + Key: opts.GetKey(opts.Config.Universal.GoInto), + Handler: self.withItem(self.enter), + GetDisabledReason: self.require(self.singleItemSelected()), }, { - Key: opts.GetKey(opts.Config.Branches.FetchRemote), - Handler: self.checkSelected(self.fetch), - Description: self.c.Tr.FetchRemote, + Key: opts.GetKey(opts.Config.Branches.FetchRemote), + Handler: self.withItem(self.fetch), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.FetchRemote, }, { Key: opts.GetKey(opts.Config.Universal.New), @@ -49,24 +57,22 @@ func (self *RemotesController) GetKeybindings(opts types.KeybindingsOpts) []*typ Description: self.c.Tr.AddNewRemote, }, { - Key: opts.GetKey(opts.Config.Universal.Remove), - Handler: self.checkSelected(self.remove), - Description: self.c.Tr.RemoveRemote, + Key: opts.GetKey(opts.Config.Universal.Remove), + Handler: self.withItem(self.remove), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.RemoveRemote, }, { - Key: opts.GetKey(opts.Config.Universal.Edit), - Handler: self.checkSelected(self.edit), - Description: self.c.Tr.EditRemote, + Key: opts.GetKey(opts.Config.Universal.Edit), + Handler: self.withItem(self.edit), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.EditRemote, }, } return bindings } -func (self *RemotesController) Context() types.Context { - return self.context() -} - func (self *RemotesController) context() *context.RemotesContext { return self.c.Contexts().Remotes } @@ -94,7 +100,7 @@ func (self *RemotesController) GetOnRenderToMain() func() error { } func (self *RemotesController) GetOnClick() func() error { - return self.checkSelected(self.enter) + return self.withItemGraceful(self.enter) } func (self *RemotesController) enter(remote *models.Remote) error { @@ -208,14 +214,3 @@ func (self *RemotesController) fetch(remote *models.Remote) error { return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}}) }) } - -func (self *RemotesController) checkSelected(callback func(*models.Remote) error) func() error { - return func() error { - file := self.context().GetSelected() - if file == nil { - return nil - } - - return callback(file) - } -} diff --git a/pkg/gui/controllers/search_prompt_controller.go b/pkg/gui/controllers/search_prompt_controller.go index 014edd094..65dd23383 100644 --- a/pkg/gui/controllers/search_prompt_controller.go +++ b/pkg/gui/controllers/search_prompt_controller.go @@ -13,11 +13,11 @@ type SearchPromptController struct { var _ types.IController = &SearchPromptController{} func NewSearchPromptController( - common *ControllerCommon, + c *ControllerCommon, ) *SearchPromptController { return &SearchPromptController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/side_window_controller.go b/pkg/gui/controllers/side_window_controller.go index a2325c54d..0b9877494 100644 --- a/pkg/gui/controllers/side_window_controller.go +++ b/pkg/gui/controllers/side_window_controller.go @@ -9,8 +9,8 @@ type SideWindowControllerFactory struct { c *ControllerCommon } -func NewSideWindowControllerFactory(common *ControllerCommon) *SideWindowControllerFactory { - return &SideWindowControllerFactory{c: common} +func NewSideWindowControllerFactory(c *ControllerCommon) *SideWindowControllerFactory { + return &SideWindowControllerFactory{c: c} } func (self *SideWindowControllerFactory) Create(context types.Context) types.IController { @@ -24,12 +24,12 @@ type SideWindowController struct { } func NewSideWindowController( - common *ControllerCommon, + c *ControllerCommon, context types.Context, ) *SideWindowController { return &SideWindowController{ baseController: baseController{}, - c: common, + c: c, context: context, } } diff --git a/pkg/gui/controllers/snake_controller.go b/pkg/gui/controllers/snake_controller.go index 074a4a6fb..b8e3327f7 100644 --- a/pkg/gui/controllers/snake_controller.go +++ b/pkg/gui/controllers/snake_controller.go @@ -13,11 +13,11 @@ type SnakeController struct { var _ types.IController = &SnakeController{} func NewSnakeController( - common *ControllerCommon, + c *ControllerCommon, ) *SnakeController { return &SnakeController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/staging_controller.go b/pkg/gui/controllers/staging_controller.go index 31432fa68..42dac9aa3 100644 --- a/pkg/gui/controllers/staging_controller.go +++ b/pkg/gui/controllers/staging_controller.go @@ -23,14 +23,14 @@ type StagingController struct { var _ types.IController = &StagingController{} func NewStagingController( - common *ControllerCommon, + c *ControllerCommon, context types.IPatchExplorerContext, otherContext types.IPatchExplorerContext, staged bool, ) *StagingController { return &StagingController{ baseController: baseController{}, - c: common, + c: c, context: context, otherContext: otherContext, staged: staged, diff --git a/pkg/gui/controllers/stash_controller.go b/pkg/gui/controllers/stash_controller.go index acd66cd31..ddef24283 100644 --- a/pkg/gui/controllers/stash_controller.go +++ b/pkg/gui/controllers/stash_controller.go @@ -9,46 +9,57 @@ import ( type StashController struct { baseController + *ListControllerTrait[*models.StashEntry] c *ControllerCommon } var _ types.IController = &StashController{} func NewStashController( - common *ControllerCommon, + c *ControllerCommon, ) *StashController { return &StashController{ baseController: baseController{}, - c: common, + ListControllerTrait: NewListControllerTrait[*models.StashEntry]( + c, + c.Contexts().Stash, + c.Contexts().Stash.GetSelected, + ), + c: c, } } func (self *StashController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { - Key: opts.GetKey(opts.Config.Universal.Select), - Handler: self.checkSelected(self.handleStashApply), - Description: self.c.Tr.Apply, + Key: opts.GetKey(opts.Config.Universal.Select), + Handler: self.withItem(self.handleStashApply), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.Apply, }, { - Key: opts.GetKey(opts.Config.Stash.PopStash), - Handler: self.checkSelected(self.handleStashPop), - Description: self.c.Tr.Pop, + Key: opts.GetKey(opts.Config.Stash.PopStash), + Handler: self.withItem(self.handleStashPop), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.Pop, }, { - Key: opts.GetKey(opts.Config.Universal.Remove), - Handler: self.checkSelected(self.handleStashDrop), - Description: self.c.Tr.Drop, + Key: opts.GetKey(opts.Config.Universal.Remove), + Handler: self.withItem(self.handleStashDrop), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.Drop, }, { - Key: opts.GetKey(opts.Config.Universal.New), - Handler: self.checkSelected(self.handleNewBranchOffStashEntry), - Description: self.c.Tr.NewBranch, + Key: opts.GetKey(opts.Config.Universal.New), + Handler: self.withItem(self.handleNewBranchOffStashEntry), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.NewBranch, }, { - Key: opts.GetKey(opts.Config.Stash.RenameStash), - Handler: self.checkSelected(self.handleRenameStashEntry), - Description: self.c.Tr.RenameStash, + Key: opts.GetKey(opts.Config.Stash.RenameStash), + Handler: self.withItem(self.handleRenameStashEntry), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.RenameStash, }, } @@ -80,21 +91,6 @@ func (self *StashController) GetOnRenderToMain() func() error { } } -func (self *StashController) checkSelected(callback func(*models.StashEntry) error) func() error { - return func() error { - item := self.context().GetSelected() - if item == nil { - return nil - } - - return callback(item) - } -} - -func (self *StashController) Context() types.Context { - return self.context() -} - func (self *StashController) context() *context.StashContext { return self.c.Contexts().Stash } diff --git a/pkg/gui/controllers/status_controller.go b/pkg/gui/controllers/status_controller.go index 5d35d9f47..59df8e352 100644 --- a/pkg/gui/controllers/status_controller.go +++ b/pkg/gui/controllers/status_controller.go @@ -22,11 +22,11 @@ type StatusController struct { var _ types.IController = &StatusController{} func NewStatusController( - common *ControllerCommon, + c *ControllerCommon, ) *StatusController { return &StatusController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/sub_commits_controller.go b/pkg/gui/controllers/sub_commits_controller.go index 46dc0df98..a4ebfb5cd 100644 --- a/pkg/gui/controllers/sub_commits_controller.go +++ b/pkg/gui/controllers/sub_commits_controller.go @@ -2,23 +2,30 @@ package controllers import ( "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) type SubCommitsController struct { baseController + *ListControllerTrait[*models.Commit] c *ControllerCommon } var _ types.IController = &SubCommitsController{} func NewSubCommitsController( - common *ControllerCommon, + c *ControllerCommon, ) *SubCommitsController { return &SubCommitsController{ baseController: baseController{}, - c: common, + ListControllerTrait: NewListControllerTrait[*models.Commit]( + c, + c.Contexts().SubCommits, + c.Contexts().SubCommits.GetSelected, + ), + c: c, } } diff --git a/pkg/gui/controllers/submodules_controller.go b/pkg/gui/controllers/submodules_controller.go index d7ed12132..dc43ff35e 100644 --- a/pkg/gui/controllers/submodules_controller.go +++ b/pkg/gui/controllers/submodules_controller.go @@ -14,41 +14,51 @@ import ( type SubmodulesController struct { baseController + *ListControllerTrait[*models.SubmoduleConfig] c *ControllerCommon } var _ types.IController = &SubmodulesController{} func NewSubmodulesController( - controllerCommon *ControllerCommon, + c *ControllerCommon, ) *SubmodulesController { return &SubmodulesController{ baseController: baseController{}, - c: controllerCommon, + ListControllerTrait: NewListControllerTrait[*models.SubmoduleConfig]( + c, + c.Contexts().Submodules, + c.Contexts().Submodules.GetSelected, + ), + c: c, } } func (self *SubmodulesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { return []*types.Binding{ { - Key: opts.GetKey(opts.Config.Universal.GoInto), - Handler: self.checkSelected(self.enter), - Description: self.c.Tr.EnterSubmodule, + Key: opts.GetKey(opts.Config.Universal.GoInto), + Handler: self.withItem(self.enter), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.EnterSubmodule, }, { - Key: opts.GetKey(opts.Config.Universal.Select), - Handler: self.checkSelected(self.enter), - Description: self.c.Tr.EnterSubmodule, + Key: opts.GetKey(opts.Config.Universal.Select), + Handler: self.withItem(self.enter), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.EnterSubmodule, }, { - Key: opts.GetKey(opts.Config.Universal.Remove), - Handler: self.checkSelected(self.remove), - Description: self.c.Tr.RemoveSubmodule, + Key: opts.GetKey(opts.Config.Universal.Remove), + Handler: self.withItem(self.remove), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.RemoveSubmodule, }, { - Key: opts.GetKey(opts.Config.Submodules.Update), - Handler: self.checkSelected(self.update), - Description: self.c.Tr.SubmoduleUpdate, + Key: opts.GetKey(opts.Config.Submodules.Update), + Handler: self.withItem(self.update), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.SubmoduleUpdate, }, { Key: opts.GetKey(opts.Config.Universal.New), @@ -56,14 +66,16 @@ func (self *SubmodulesController) GetKeybindings(opts types.KeybindingsOpts) []* Description: self.c.Tr.AddSubmodule, }, { - Key: opts.GetKey(opts.Config.Universal.Edit), - Handler: self.checkSelected(self.editURL), - Description: self.c.Tr.EditSubmoduleUrl, + Key: opts.GetKey(opts.Config.Universal.Edit), + Handler: self.withItem(self.editURL), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.EditSubmoduleUrl, }, { - Key: opts.GetKey(opts.Config.Submodules.Init), - Handler: self.checkSelected(self.init), - Description: self.c.Tr.InitSubmodule, + Key: opts.GetKey(opts.Config.Submodules.Init), + Handler: self.withItem(self.init), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.InitSubmodule, }, { Key: opts.GetKey(opts.Config.Submodules.BulkMenu), @@ -80,7 +92,7 @@ func (self *SubmodulesController) GetKeybindings(opts types.KeybindingsOpts) []* } func (self *SubmodulesController) GetOnClick() func() error { - return self.checkSelected(self.enter) + return self.withItemGraceful(self.enter) } func (self *SubmodulesController) GetOnRenderToMain() func() error { @@ -265,21 +277,6 @@ func (self *SubmodulesController) easterEgg() error { return self.c.PushContext(self.c.Contexts().Snake) } -func (self *SubmodulesController) checkSelected(callback func(*models.SubmoduleConfig) error) func() error { - return func() error { - submodule := self.context().GetSelected() - if submodule == nil { - return nil - } - - return callback(submodule) - } -} - -func (self *SubmodulesController) Context() types.Context { - return self.context() -} - func (self *SubmodulesController) context() *context.SubmodulesContext { return self.c.Contexts().Submodules } diff --git a/pkg/gui/controllers/suggestions_controller.go b/pkg/gui/controllers/suggestions_controller.go index 17b8915a1..dbb2b9812 100644 --- a/pkg/gui/controllers/suggestions_controller.go +++ b/pkg/gui/controllers/suggestions_controller.go @@ -7,25 +7,32 @@ import ( type SuggestionsController struct { baseController + *ListControllerTrait[*types.Suggestion] c *ControllerCommon } var _ types.IController = &SuggestionsController{} func NewSuggestionsController( - common *ControllerCommon, + c *ControllerCommon, ) *SuggestionsController { return &SuggestionsController{ baseController: baseController{}, - c: common, + ListControllerTrait: NewListControllerTrait[*types.Suggestion]( + c, + c.Contexts().Suggestions, + c.Contexts().Suggestions.GetSelected, + ), + c: c, } } func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { - Key: opts.GetKey(opts.Config.Universal.Confirm), - Handler: func() error { return self.context().State.OnConfirm() }, + Key: opts.GetKey(opts.Config.Universal.Confirm), + Handler: func() error { return self.context().State.OnConfirm() }, + GetDisabledReason: self.require(self.singleItemSelected()), }, { Key: opts.GetKey(opts.Config.Universal.Return), @@ -47,10 +54,6 @@ func (self *SuggestionsController) GetOnFocusLost() func(types.OnFocusLostOpts) } } -func (self *SuggestionsController) Context() types.Context { - return self.context() -} - func (self *SuggestionsController) context() *context.SuggestionsContext { return self.c.Contexts().Suggestions } diff --git a/pkg/gui/controllers/switch_to_diff_files_controller.go b/pkg/gui/controllers/switch_to_diff_files_controller.go index af2b38984..069726147 100644 --- a/pkg/gui/controllers/switch_to_diff_files_controller.go +++ b/pkg/gui/controllers/switch_to_diff_files_controller.go @@ -10,13 +10,16 @@ import ( var _ types.IController = &SwitchToDiffFilesController{} type CanSwitchToDiffFiles interface { - types.Context + types.IListContext CanRebase() bool GetSelectedRef() types.Ref } +// Not using our ListControllerTrait because our 'selected' item is not a list item +// but an attribute on it i.e. the ref of an item. type SwitchToDiffFilesController struct { baseController + *ListControllerTrait[types.Ref] c *ControllerCommon context CanSwitchToDiffFiles diffFilesContext *context.CommitFilesContext @@ -28,7 +31,12 @@ func NewSwitchToDiffFilesController( diffFilesContext *context.CommitFilesContext, ) *SwitchToDiffFilesController { return &SwitchToDiffFilesController{ - baseController: baseController{}, + baseController: baseController{}, + ListControllerTrait: NewListControllerTrait[types.Ref]( + c, + context, + context.GetSelectedRef, + ), c: c, context: context, diffFilesContext: diffFilesContext, @@ -38,9 +46,10 @@ func NewSwitchToDiffFilesController( func (self *SwitchToDiffFilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { - Key: opts.GetKey(opts.Config.Universal.GoInto), - Handler: self.checkSelected(self.enter), - Description: self.c.Tr.ViewItemFiles, + Key: opts.GetKey(opts.Config.Universal.GoInto), + Handler: self.withItem(self.enter), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.ViewItemFiles, }, } @@ -48,18 +57,7 @@ func (self *SwitchToDiffFilesController) GetKeybindings(opts types.KeybindingsOp } func (self *SwitchToDiffFilesController) GetOnClick() func() error { - return self.checkSelected(self.enter) -} - -func (self *SwitchToDiffFilesController) checkSelected(callback func(types.Ref) error) func() error { - return func() error { - ref := self.context.GetSelectedRef() - if ref == nil { - return nil - } - - return callback(ref) - } + return self.withItemGraceful(self.enter) } func (self *SwitchToDiffFilesController) enter(ref types.Ref) error { @@ -70,10 +68,6 @@ func (self *SwitchToDiffFilesController) enter(ref types.Ref) error { }) } -func (self *SwitchToDiffFilesController) Context() types.Context { - return self.context -} - func (self *SwitchToDiffFilesController) viewFiles(opts SwitchToCommitFilesContextOpts) error { diffFilesContext := self.diffFilesContext diff --git a/pkg/gui/controllers/switch_to_sub_commits_controller.go b/pkg/gui/controllers/switch_to_sub_commits_controller.go index 3109f559e..d7bb0a97d 100644 --- a/pkg/gui/controllers/switch_to_sub_commits_controller.go +++ b/pkg/gui/controllers/switch_to_sub_commits_controller.go @@ -8,34 +8,43 @@ import ( var _ types.IController = &SwitchToSubCommitsController{} type CanSwitchToSubCommits interface { - types.Context + types.IListContext GetSelectedRef() types.Ref ShowBranchHeadsInSubCommits() bool } +// Not using our ListControllerTrait because our 'selected' item is not a list item +// but an attribute on it i.e. the ref of an item. type SwitchToSubCommitsController struct { baseController + *ListControllerTrait[types.Ref] c *ControllerCommon context CanSwitchToSubCommits } func NewSwitchToSubCommitsController( - controllerCommon *ControllerCommon, + c *ControllerCommon, context CanSwitchToSubCommits, ) *SwitchToSubCommitsController { return &SwitchToSubCommitsController{ baseController: baseController{}, - c: controllerCommon, - context: context, + ListControllerTrait: NewListControllerTrait[types.Ref]( + c, + context, + context.GetSelectedRef, + ), + c: c, + context: context, } } func (self *SwitchToSubCommitsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { - Handler: self.viewCommits, - Key: opts.GetKey(opts.Config.Universal.GoInto), - Description: self.c.Tr.ViewCommits, + Handler: self.viewCommits, + GetDisabledReason: self.require(self.singleItemSelected()), + Key: opts.GetKey(opts.Config.Universal.GoInto), + Description: self.c.Tr.ViewCommits, }, } @@ -59,7 +68,3 @@ func (self *SwitchToSubCommitsController) viewCommits() error { ShowBranchHeads: self.context.ShowBranchHeadsInSubCommits(), }) } - -func (self *SwitchToSubCommitsController) Context() types.Context { - return self.context -} diff --git a/pkg/gui/controllers/tags_controller.go b/pkg/gui/controllers/tags_controller.go index e96dde2e4..7baebc54c 100644 --- a/pkg/gui/controllers/tags_controller.go +++ b/pkg/gui/controllers/tags_controller.go @@ -10,37 +10,46 @@ import ( type TagsController struct { baseController + *ListControllerTrait[*models.Tag] c *ControllerCommon } var _ types.IController = &TagsController{} func NewTagsController( - common *ControllerCommon, + c *ControllerCommon, ) *TagsController { return &TagsController{ baseController: baseController{}, - c: common, + ListControllerTrait: NewListControllerTrait[*models.Tag]( + c, + c.Contexts().Tags, + c.Contexts().Tags.GetSelected, + ), + c: c, } } func (self *TagsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { - Key: opts.GetKey(opts.Config.Universal.Select), - Handler: self.withSelectedTag(self.checkout), - Description: self.c.Tr.Checkout, + Key: opts.GetKey(opts.Config.Universal.Select), + Handler: self.withItem(self.checkout), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.Checkout, }, { - Key: opts.GetKey(opts.Config.Universal.Remove), - Handler: self.withSelectedTag(self.delete), - Description: self.c.Tr.ViewDeleteOptions, - OpensMenu: true, + Key: opts.GetKey(opts.Config.Universal.Remove), + Handler: self.withItem(self.delete), + Description: self.c.Tr.ViewDeleteOptions, + GetDisabledReason: self.require(self.singleItemSelected()), + OpensMenu: true, }, { - Key: opts.GetKey(opts.Config.Branches.PushTag), - Handler: self.withSelectedTag(self.push), - Description: self.c.Tr.PushTag, + Key: opts.GetKey(opts.Config.Branches.PushTag), + Handler: self.withItem(self.push), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.PushTag, }, { Key: opts.GetKey(opts.Config.Universal.New), @@ -48,10 +57,11 @@ func (self *TagsController) GetKeybindings(opts types.KeybindingsOpts) []*types. Description: self.c.Tr.CreateTag, }, { - Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), - Handler: self.withSelectedTag(self.createResetMenu), - Description: self.c.Tr.ViewResetOptions, - OpensMenu: true, + Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), + Handler: self.withItem(self.createResetMenu), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.ViewResetOptions, + OpensMenu: true, }, } @@ -215,21 +225,6 @@ func (self *TagsController) create() error { }) } -func (self *TagsController) withSelectedTag(f func(tag *models.Tag) error) func() error { - return func() error { - tag := self.context().GetSelected() - if tag == nil { - return nil - } - - return f(tag) - } -} - -func (self *TagsController) Context() types.Context { - return self.context() -} - func (self *TagsController) context() *context.TagsContext { return self.c.Contexts().Tags } diff --git a/pkg/gui/controllers/undo_controller.go b/pkg/gui/controllers/undo_controller.go index 1546d0c46..c0a754794 100644 --- a/pkg/gui/controllers/undo_controller.go +++ b/pkg/gui/controllers/undo_controller.go @@ -27,11 +27,11 @@ type UndoController struct { var _ types.IController = &UndoController{} func NewUndoController( - common *ControllerCommon, + c *ControllerCommon, ) *UndoController { return &UndoController{ baseController: baseController{}, - c: common, + c: c, } } diff --git a/pkg/gui/controllers/worktree_options_controller.go b/pkg/gui/controllers/worktree_options_controller.go index 8c2c0bbb0..01cc9b362 100644 --- a/pkg/gui/controllers/worktree_options_controller.go +++ b/pkg/gui/controllers/worktree_options_controller.go @@ -14,15 +14,21 @@ type CanViewWorktreeOptions interface { type WorktreeOptionsController struct { baseController + *ListControllerTrait[string] c *ControllerCommon context CanViewWorktreeOptions } -func NewWorktreeOptionsController(controllerCommon *ControllerCommon, context CanViewWorktreeOptions) *WorktreeOptionsController { +func NewWorktreeOptionsController(c *ControllerCommon, context CanViewWorktreeOptions) *WorktreeOptionsController { return &WorktreeOptionsController{ baseController: baseController{}, - c: controllerCommon, - context: context, + ListControllerTrait: NewListControllerTrait[string]( + c, + context, + context.GetSelectedItemId, + ), + c: c, + context: context, } } @@ -30,7 +36,7 @@ func (self *WorktreeOptionsController) GetKeybindings(opts types.KeybindingsOpts bindings := []*types.Binding{ { Key: opts.GetKey(opts.Config.Worktrees.ViewWorktreeOptions), - Handler: self.checkSelected(self.viewWorktreeOptions), + Handler: self.withItem(self.viewWorktreeOptions), Description: self.c.Tr.ViewWorktreeOptions, OpensMenu: true, }, @@ -39,21 +45,6 @@ func (self *WorktreeOptionsController) GetKeybindings(opts types.KeybindingsOpts return bindings } -func (self *WorktreeOptionsController) checkSelected(callback func(string) error) func() error { - return func() error { - ref := self.context.GetSelectedItemId() - if ref == "" { - return nil - } - - return callback(ref) - } -} - -func (self *WorktreeOptionsController) Context() types.Context { - return self.context -} - func (self *WorktreeOptionsController) viewWorktreeOptions(ref string) error { return self.c.Helpers().Worktree.ViewWorktreeOptions(self.context, ref) } diff --git a/pkg/gui/controllers/worktrees_controller.go b/pkg/gui/controllers/worktrees_controller.go index c76c3b1de..b634d0607 100644 --- a/pkg/gui/controllers/worktrees_controller.go +++ b/pkg/gui/controllers/worktrees_controller.go @@ -13,17 +13,23 @@ import ( type WorktreesController struct { baseController + *ListControllerTrait[*models.Worktree] c *ControllerCommon } var _ types.IController = &WorktreesController{} func NewWorktreesController( - common *ControllerCommon, + c *ControllerCommon, ) *WorktreesController { return &WorktreesController{ baseController: baseController{}, - c: common, + ListControllerTrait: NewListControllerTrait[*models.Worktree]( + c, + c.Contexts().Worktrees, + c.Contexts().Worktrees.GetSelected, + ), + c: c, } } @@ -35,24 +41,28 @@ func (self *WorktreesController) GetKeybindings(opts types.KeybindingsOpts) []*t Description: self.c.Tr.CreateWorktree, }, { - Key: opts.GetKey(opts.Config.Universal.Select), - Handler: self.checkSelected(self.enter), - Description: self.c.Tr.SwitchToWorktree, + Key: opts.GetKey(opts.Config.Universal.Select), + Handler: self.withItem(self.enter), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.SwitchToWorktree, }, { - Key: opts.GetKey(opts.Config.Universal.Confirm), - Handler: self.checkSelected(self.enter), - Description: self.c.Tr.SwitchToWorktree, + Key: opts.GetKey(opts.Config.Universal.Confirm), + Handler: self.withItem(self.enter), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.SwitchToWorktree, }, { - Key: opts.GetKey(opts.Config.Universal.OpenFile), - Handler: self.checkSelected(self.open), - Description: self.c.Tr.OpenInEditor, + Key: opts.GetKey(opts.Config.Universal.OpenFile), + Handler: self.withItem(self.open), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.OpenInEditor, }, { - Key: opts.GetKey(opts.Config.Universal.Remove), - Handler: self.checkSelected(self.remove), - Description: self.c.Tr.RemoveWorktree, + Key: opts.GetKey(opts.Config.Universal.Remove), + Handler: self.withItem(self.remove), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.RemoveWorktree, }, } @@ -113,7 +123,7 @@ func (self *WorktreesController) remove(worktree *models.Worktree) error { } func (self *WorktreesController) GetOnClick() func() error { - return self.checkSelected(self.enter) + return self.withItemGraceful(self.enter) } func (self *WorktreesController) enter(worktree *models.Worktree) error { @@ -124,21 +134,6 @@ func (self *WorktreesController) open(worktree *models.Worktree) error { return self.c.Helpers().Files.OpenDirInEditor(worktree.Path) } -func (self *WorktreesController) checkSelected(callback func(worktree *models.Worktree) error) func() error { - return func() error { - worktree := self.context().GetSelected() - if worktree == nil { - return nil - } - - return callback(worktree) - } -} - -func (self *WorktreesController) Context() types.Context { - return self.context() -} - func (self *WorktreesController) context() *context.WorktreesContext { return self.c.Contexts().Worktrees } diff --git a/pkg/gui/global_handlers.go b/pkg/gui/global_handlers.go index c537e8524..9513fff61 100644 --- a/pkg/gui/global_handlers.go +++ b/pkg/gui/global_handlers.go @@ -139,6 +139,28 @@ func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error { return nil } +func (gui *Gui) getCopySelectedSideContextItemToClipboardDisabledReason() *types.DisabledReason { + // important to note that this assumes we've selected an item in a side context + currentSideContext := gui.c.CurrentSideContext() + if currentSideContext == nil { + // This should never happen but if it does we'll just ignore the keypress + return nil + } + + listContext, ok := currentSideContext.(types.IListContext) + if !ok { + // This should never happen but if it does we'll just ignore the keypress + return nil + } + + startIdx, endIdx := listContext.GetList().GetSelectionRange() + if startIdx != endIdx { + return &types.DisabledReason{Text: gui.Tr.RangeSelectNotSupported} + } + + return nil +} + func (gui *Gui) setCaption(caption string) { gui.Views.Options.FgColor = gocui.ColorWhite gui.Views.Options.FgColor |= gocui.AttrBold diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 26ce8ec91..90f27ac43 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -123,28 +123,32 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi Handler: self.scrollDownMain, }, { - ViewName: "files", - Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), - Handler: self.handleCopySelectedSideContextItemToClipboard, - Description: self.c.Tr.CopyFileNameToClipboard, + ViewName: "files", + Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), + Handler: self.handleCopySelectedSideContextItemToClipboard, + GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason, + Description: self.c.Tr.CopyFileNameToClipboard, }, { - ViewName: "localBranches", - Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), - Handler: self.handleCopySelectedSideContextItemToClipboard, - Description: self.c.Tr.CopyBranchNameToClipboard, + ViewName: "localBranches", + Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), + Handler: self.handleCopySelectedSideContextItemToClipboard, + GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason, + Description: self.c.Tr.CopyBranchNameToClipboard, }, { - ViewName: "remoteBranches", - Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), - Handler: self.handleCopySelectedSideContextItemToClipboard, - Description: self.c.Tr.CopyBranchNameToClipboard, + ViewName: "remoteBranches", + Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), + Handler: self.handleCopySelectedSideContextItemToClipboard, + GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason, + Description: self.c.Tr.CopyBranchNameToClipboard, }, { - ViewName: "commits", - Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), - Handler: self.handleCopySelectedSideContextItemToClipboard, - Description: self.c.Tr.CopyCommitShaToClipboard, + ViewName: "commits", + Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), + Handler: self.handleCopySelectedSideContextItemToClipboard, + GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason, + Description: self.c.Tr.CopyCommitShaToClipboard, }, { ViewName: "commits", @@ -153,16 +157,18 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi Description: self.c.Tr.ResetCherryPick, }, { - ViewName: "reflogCommits", - Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), - Handler: self.handleCopySelectedSideContextItemToClipboard, - Description: self.c.Tr.CopyCommitShaToClipboard, + ViewName: "reflogCommits", + Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), + Handler: self.handleCopySelectedSideContextItemToClipboard, + GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason, + Description: self.c.Tr.CopyCommitShaToClipboard, }, { - ViewName: "subCommits", - Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), - Handler: self.handleCopySelectedSideContextItemToClipboard, - Description: self.c.Tr.CopyCommitShaToClipboard, + ViewName: "subCommits", + Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), + Handler: self.handleCopySelectedSideContextItemToClipboard, + GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason, + Description: self.c.Tr.CopyCommitShaToClipboard, }, { ViewName: "information", @@ -171,10 +177,11 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi Handler: self.handleInfoClick, }, { - ViewName: "commitFiles", - Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), - Handler: self.handleCopySelectedSideContextItemToClipboard, - Description: self.c.Tr.CopyCommitFileNameToClipboard, + ViewName: "commitFiles", + Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), + Handler: self.handleCopySelectedSideContextItemToClipboard, + GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason, + Description: self.c.Tr.CopyCommitFileNameToClipboard, }, { ViewName: "", @@ -240,10 +247,11 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi Handler: self.scrollDownConfirmationPanel, }, { - ViewName: "submodules", - Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), - Handler: self.handleCopySelectedSideContextItemToClipboard, - Description: self.c.Tr.CopySubmoduleNameToClipboard, + ViewName: "submodules", + Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), + Handler: self.handleCopySelectedSideContextItemToClipboard, + GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason, + Description: self.c.Tr.CopySubmoduleNameToClipboard, }, { ViewName: "extras", diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index 3557cf5aa..a3f0ad24a 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -231,6 +231,7 @@ type IListCursor interface { GetRangeStartIdx() (int, bool) GetSelectionRange() (int, int) IsSelectingRange() bool + AreMultipleItemsSelected() bool ToggleStickyRange() ExpandNonStickyRange(int) } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index e9d0c9a65..35e7dd687 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -644,7 +644,6 @@ type TranslationSet struct { MarkedCommitMarker string PleaseGoToURL string DisabledMenuItemPrefix string - NoCommitSelected string NoCopiedCommits string QuickStartInteractiveRebase string QuickStartInteractiveRebaseTooltip string @@ -652,6 +651,9 @@ type TranslationSet struct { ToggleRangeSelect string RangeSelectUp string RangeSelectDown string + RangeSelectNotSupported string + NoItemSelected string + SelectedItemIsNotABranch string Actions Actions Bisect Bisect Log Log @@ -1478,13 +1480,15 @@ func EnglishTranslationSet() TranslationSet { MarkedCommitMarker: "↑↑↑ Will rebase from here ↑↑↑", PleaseGoToURL: "Please go to {{.url}}", DisabledMenuItemPrefix: "Disabled: ", - NoCommitSelected: "No commit selected", NoCopiedCommits: "No copied commits", QuickStartInteractiveRebase: "Start interactive rebase", QuickStartInteractiveRebaseTooltip: "Start an interactive rebase for the commits on your branch. This will include all commits from the HEAD commit down to the first merge commit or main branch commit.\nIf you would instead like to start an interactive rebase from the selected commit, press `{{.editKey}}`.", CannotQuickStartInteractiveRebase: "Cannot start interactive rebase: the HEAD commit is a merge commit or is present on the main branch, so there is no appropriate base commit to start the rebase from. You can start an interactive rebase from a specific commit by selecting the commit and pressing `{{.editKey}}`.", RangeSelectUp: "Range select up", RangeSelectDown: "Range select down", + RangeSelectNotSupported: "Action does not support range selection, please select a single item", + NoItemSelected: "No item selected", + SelectedItemIsNotABranch: "Selected item is not a branch", Actions: Actions{ // TODO: combine this with the original keybinding descriptions (those are all in lowercase atm) CheckoutCommit: "Checkout commit", diff --git a/pkg/integration/tests/file/copy_menu.go b/pkg/integration/tests/file/copy_menu.go index a1af13d7b..6e4f537af 100644 --- a/pkg/integration/tests/file/copy_menu.go +++ b/pkg/integration/tests/file/copy_menu.go @@ -29,10 +29,10 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{ t.ExpectPopup().Menu(). Title(Equals("Copy to clipboard")). Select(Contains("File name")). - Tooltip(Equals("Disabled: Nothing to copy")). + Tooltip(Equals("Disabled: No item selected")). Confirm(). Tap(func() { - t.ExpectToast(Equals("Disabled: Nothing to copy")) + t.ExpectToast(Equals("Disabled: No item selected")) }). Cancel() }) diff --git a/pkg/integration/tests/ui/empty_menu.go b/pkg/integration/tests/ui/empty_menu.go index d6b3b42f0..35c3d4560 100644 --- a/pkg/integration/tests/ui/empty_menu.go +++ b/pkg/integration/tests/ui/empty_menu.go @@ -22,7 +22,14 @@ var EmptyMenu = NewIntegrationTest(NewIntegrationTestArgs{ // a string that filters everything out FilterOrSearch("ljasldkjaslkdjalskdjalsdjaslkd"). IsEmpty(). - Press(keys.Universal.Select) + Press(keys.Universal.Select). + Tap(func() { + t.ExpectToast(Equals("Disabled: No item selected")) + }). + // escape the search + PressEscape(). + // escape the view + PressEscape() // back in the files view, selecting the non-existing menu item was a no-op t.Views().Files().