1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-05-31 23:19:40 +02:00

Enforce single-item selection in various actions

We want to show an error when the user tries to invoke an action that expects only
a single item to be selected.

We're using the GetDisabledReason field to enforce this (as well as DisabledReason
on menu items).

I've created a ListControllerTrait to store some shared convenience functions for this.
This commit is contained in:
Jesse Duffield 2024-01-14 13:51:20 +11:00
parent 280b4d60f8
commit 51fb82d6bf
45 changed files with 854 additions and 757 deletions

View File

@ -114,10 +114,18 @@ func (self *ListCursor) CancelRangeSelect() {
self.rangeSelectMode = RangeSelectModeNone 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 { func (self *ListCursor) IsSelectingRange() bool {
return self.rangeSelectMode != RangeSelectModeNone 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) { func (self *ListCursor) GetSelectionRange() (int, int) {
if self.IsSelectingRange() { if self.IsSelectingRange() {
return utils.MinMax(self.selectedIdx, self.rangeStartIdx) return utils.MinMax(self.selectedIdx, self.rangeStartIdx)

View File

@ -22,50 +22,61 @@ type ContainsCommits interface {
type BasicCommitsController struct { type BasicCommitsController struct {
baseController baseController
*ListControllerTrait[*models.Commit]
c *ControllerCommon c *ControllerCommon
context ContainsCommits context ContainsCommits
} }
func NewBasicCommitsController(controllerCommon *ControllerCommon, context ContainsCommits) *BasicCommitsController { func NewBasicCommitsController(c *ControllerCommon, context ContainsCommits) *BasicCommitsController {
return &BasicCommitsController{ return &BasicCommitsController{
baseController: baseController{}, baseController: baseController{},
c: controllerCommon, c: c,
context: context, context: context,
ListControllerTrait: NewListControllerTrait[*models.Commit](
c,
context,
context.GetSelected,
),
} }
} }
func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Commits.CheckoutCommit), Key: opts.GetKey(opts.Config.Commits.CheckoutCommit),
Handler: self.checkSelected(self.checkout), Handler: self.withItem(self.checkout),
Description: self.c.Tr.CheckoutCommit, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.CheckoutCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.CopyCommitAttributeToClipboard), Key: opts.GetKey(opts.Config.Commits.CopyCommitAttributeToClipboard),
Handler: self.checkSelected(self.copyCommitAttribute), Handler: self.withItem(self.copyCommitAttribute),
Description: self.c.Tr.CopyCommitAttributeToClipboard, GetDisabledReason: self.require(self.singleItemSelected()),
OpensMenu: true, Description: self.c.Tr.CopyCommitAttributeToClipboard,
OpensMenu: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.OpenInBrowser), Key: opts.GetKey(opts.Config.Commits.OpenInBrowser),
Handler: self.checkSelected(self.openInBrowser), Handler: self.withItem(self.openInBrowser),
Description: self.c.Tr.OpenCommitInBrowser, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.OpenCommitInBrowser,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.New), Key: opts.GetKey(opts.Config.Universal.New),
Handler: self.checkSelected(self.newBranch), Handler: self.withItem(self.newBranch),
Description: self.c.Tr.CreateNewBranchFromCommit, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.CreateNewBranchFromCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
Handler: self.checkSelected(self.createResetMenu), Handler: self.withItem(self.createResetMenu),
Description: self.c.Tr.ViewResetOptions, GetDisabledReason: self.require(self.singleItemSelected()),
OpensMenu: true, Description: self.c.Tr.ViewResetOptions,
OpensMenu: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.CherryPickCopy), Key: opts.GetKey(opts.Config.Commits.CherryPickCopy),
Handler: self.checkSelected(self.copyRange), Handler: self.withItem(self.copyRange),
Description: self.c.Tr.CherryPickCopy, Description: self.c.Tr.CherryPickCopy,
}, },
{ {
@ -74,30 +85,16 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
Description: self.c.Tr.ResetCherryPick, Description: self.c.Tr.ResetCherryPick,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.OpenDiffTool), Key: opts.GetKey(opts.Config.Universal.OpenDiffTool),
Handler: self.checkSelected(self.openDiffTool), Handler: self.withItem(self.openDiffTool),
Description: self.c.Tr.OpenDiffTool, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.OpenDiffTool,
}, },
} }
return bindings 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 { func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) error {
return self.c.Menu(types.CreateMenuOptions{ return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Actions.CopyCommitAttributeToClipboard, Title: self.c.Tr.Actions.CopyCommitAttributeToClipboard,

View File

@ -14,17 +14,23 @@ import (
type BisectController struct { type BisectController struct {
baseController baseController
*ListControllerTrait[*models.Commit]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &BisectController{} var _ types.IController = &BisectController{}
func NewBisectController( func NewBisectController(
common *ControllerCommon, c *ControllerCommon,
) *BisectController { ) *BisectController {
return &BisectController{ return &BisectController{
baseController: baseController{}, 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{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Commits.ViewBisectOptions), 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, Description: self.c.Tr.ViewBisectOptions,
OpensMenu: true, 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 // 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 // not, we're still picking the initial commits before we really start, so
// use the selected commit in that case. // 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) 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{ menuItems := []*types.MenuItem{
{ {
Label: fmt.Sprintf(self.c.Tr.Bisect.Mark, shortShaToMark, info.NewTerm()), 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) return self.afterMark(selectCurrentAfter, waitToReselect)
}, },
Key: 'b', DisabledReason: singleItemIfNotBisecting,
Key: 'b',
}, },
{ {
Label: fmt.Sprintf(self.c.Tr.Bisect.Mark, shortShaToMark, info.OldTerm()), 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) return self.afterMark(selectCurrentAfter, waitToReselect)
}, },
Key: 'g', DisabledReason: singleItemIfNotBisecting,
Key: 'g',
}, },
{ {
Label: fmt.Sprintf(self.c.Tr.Bisect.SkipCurrent, shortShaToMark), 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) return self.afterMark(selectCurrentAfter, waitToReselect)
}, },
Key: 's', DisabledReason: singleItemIfNotBisecting,
Key: 's',
}, },
} }
if info.GetCurrentSha() != "" && info.GetCurrentSha() != commit.Sha { 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) return self.afterMark(selectCurrentAfter, waitToReselect)
}, },
Key: 'S', DisabledReason: self.require(self.singleItemSelected())(),
Key: 'S',
})) }))
} }
menuItems = append(menuItems, lo.ToPtr(types.MenuItem{ 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() 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()), 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() return self.c.Helpers().Bisect.PostBisectCommandRefresh()
}, },
Key: 'g', DisabledReason: self.require(self.singleItemSelected())(),
Key: 'g',
}, },
{ {
Label: self.c.Tr.Bisect.ChooseTerms, 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 { func (self *BisectController) context() *context.LocalCommitsContext {
return self.c.Contexts().LocalCommits return self.c.Contexts().LocalCommits
} }

View File

@ -17,48 +17,61 @@ import (
type BranchesController struct { type BranchesController struct {
baseController baseController
*ListControllerTrait[*models.Branch]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &BranchesController{} var _ types.IController = &BranchesController{}
func NewBranchesController( func NewBranchesController(
common *ControllerCommon, c *ControllerCommon,
) *BranchesController { ) *BranchesController {
return &BranchesController{ return &BranchesController{
baseController: baseController{}, 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 { func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{ return []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.Select), Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.checkSelected(self.press), Handler: self.withItem(self.press),
GetDisabledReason: self.getDisabledReasonForPress, GetDisabledReason: self.require(
Description: self.c.Tr.Checkout, self.singleItemSelected(),
self.notPulling,
),
Description: self.c.Tr.Checkout,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.New), Key: opts.GetKey(opts.Config.Universal.New),
Handler: self.checkSelected(self.newBranch), Handler: self.withItem(self.newBranch),
Description: self.c.Tr.NewBranch, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.NewBranch,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.CreatePullRequest), Key: opts.GetKey(opts.Config.Branches.CreatePullRequest),
Handler: self.checkSelected(self.handleCreatePullRequest), Handler: self.withItem(self.handleCreatePullRequest),
Description: self.c.Tr.CreatePullRequest, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.CreatePullRequest,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.ViewPullRequestOptions), Key: opts.GetKey(opts.Config.Branches.ViewPullRequestOptions),
Handler: self.checkSelected(self.handleCreatePullRequestMenu), Handler: self.withItem(self.handleCreatePullRequestMenu),
Description: self.c.Tr.CreatePullRequestOptions, GetDisabledReason: self.require(self.singleItemSelected()),
OpensMenu: true, Description: self.c.Tr.CreatePullRequestOptions,
OpensMenu: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.CopyPullRequestURL), Key: opts.GetKey(opts.Config.Branches.CopyPullRequestURL),
Handler: self.copyPullRequestURL, Handler: self.copyPullRequestURL,
Description: self.c.Tr.CopyPullRequestURL, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.CopyPullRequestURL,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.CheckoutBranchByName), Key: opts.GetKey(opts.Config.Branches.CheckoutBranchByName),
@ -66,60 +79,69 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
Description: self.c.Tr.CheckoutByName, Description: self.c.Tr.CheckoutByName,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.ForceCheckoutBranch), Key: opts.GetKey(opts.Config.Branches.ForceCheckoutBranch),
Handler: self.forceCheckout, Handler: self.forceCheckout,
Description: self.c.Tr.ForceCheckout, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.ForceCheckout,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelectedAndReal(self.delete), Handler: self.withItem(self.delete),
Description: self.c.Tr.ViewDeleteOptions, GetDisabledReason: self.require(self.singleItemSelected(self.branchIsReal)),
OpensMenu: true, Description: self.c.Tr.ViewDeleteOptions,
OpensMenu: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.RebaseBranch), Key: opts.GetKey(opts.Config.Branches.RebaseBranch),
Handler: opts.Guards.OutsideFilterMode(self.rebase), Handler: opts.Guards.OutsideFilterMode(self.rebase),
Description: self.c.Tr.RebaseBranch, GetDisabledReason: self.require(
GetDisabledReason: self.getDisabledReasonForRebase, self.singleItemSelected(self.notRebasingOntoSelf),
),
Description: self.c.Tr.RebaseBranch,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch), Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch),
Handler: opts.Guards.OutsideFilterMode(self.merge), Handler: opts.Guards.OutsideFilterMode(self.merge),
Description: self.c.Tr.MergeIntoCurrentBranch, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.MergeIntoCurrentBranch,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.FastForward), Key: opts.GetKey(opts.Config.Branches.FastForward),
Handler: self.checkSelectedAndReal(self.fastForward), Handler: self.withItem(self.fastForward),
Description: self.c.Tr.FastForward, GetDisabledReason: self.require(self.singleItemSelected(self.branchIsReal)),
Description: self.c.Tr.FastForward,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.CreateTag), Key: opts.GetKey(opts.Config.Branches.CreateTag),
Handler: self.checkSelected(self.createTag), Handler: self.withItem(self.createTag),
Description: self.c.Tr.CreateTag, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.CreateTag,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.SortOrder), Key: opts.GetKey(opts.Config.Branches.SortOrder),
Handler: self.createSortMenu, Handler: self.createSortMenu,
Description: self.c.Tr.SortOrder, Description: self.c.Tr.SortOrder,
OpensMenu: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
Handler: self.checkSelected(self.createResetMenu), Handler: self.withItem(self.createResetMenu),
Description: self.c.Tr.ViewResetOptions, GetDisabledReason: self.require(self.singleItemSelected()),
OpensMenu: true, Description: self.c.Tr.ViewResetOptions,
OpensMenu: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.RenameBranch), Key: opts.GetKey(opts.Config.Branches.RenameBranch),
Handler: self.checkSelectedAndReal(self.rename), Handler: self.withItem(self.rename),
Description: self.c.Tr.RenameBranch, GetDisabledReason: self.require(self.singleItemSelected(self.branchIsReal)),
Description: self.c.Tr.RenameBranch,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.SetUpstream), Key: opts.GetKey(opts.Config.Branches.SetUpstream),
Handler: self.checkSelected(self.viewUpstreamOptions), Handler: self.withItem(self.viewUpstreamOptions),
Description: self.c.Tr.ViewBranchUpstreamOptions, GetDisabledReason: self.require(self.singleItemSelected()),
Tooltip: self.c.Tr.ViewBranchUpstreamOptionsTooltip, Description: self.c.Tr.ViewBranchUpstreamOptions,
OpensMenu: true, 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{}) 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() currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
if currentBranch != nil { if currentBranch != nil {
op := self.c.State().GetItemOperation(currentBranch) op := self.c.State().GetItemOperation(currentBranch)
@ -561,8 +583,8 @@ func (self *BranchesController) rebase() error {
return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName) return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName)
} }
func (self *BranchesController) getDisabledReasonForRebase() *types.DisabledReason { func (self *BranchesController) notRebasingOntoSelf(branch *models.Branch) *types.DisabledReason {
selectedBranchName := self.context().GetSelected().Name selectedBranchName := branch.Name
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name
if selectedBranchName == checkedOutBranch { if selectedBranchName == checkedOutBranch {
return &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf} return &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf}
@ -753,24 +775,10 @@ func (self *BranchesController) createPullRequest(from string, to string) error
return nil return nil
} }
func (self *BranchesController) checkSelected(callback func(*models.Branch) error) func() error { func (self *BranchesController) branchIsReal(branch *models.Branch) *types.DisabledReason {
return func() error { if !branch.IsRealBranch() {
selectedItem := self.context().GetSelected() return &types.DisabledReason{Text: self.c.Tr.SelectedItemIsNotABranch}
if selectedItem == nil {
return nil
}
return callback(selectedItem)
} }
}
func (self *BranchesController) checkSelectedAndReal(callback func(*models.Branch) error) func() error { return nil
return func() error {
selectedItem := self.context().GetSelected()
if selectedItem == nil || !selectedItem.IsRealBranch() {
return nil
}
return callback(selectedItem)
}
} }

View File

@ -12,11 +12,11 @@ type CommandLogController struct {
var _ types.IController = &CommandLogController{} var _ types.IController = &CommandLogController{}
func NewCommandLogController( func NewCommandLogController(
common *ControllerCommon, c *ControllerCommon,
) *CommandLogController { ) *CommandLogController {
return &CommandLogController{ return &CommandLogController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -13,11 +13,11 @@ type CommitDescriptionController struct {
var _ types.IController = &CommitMessageController{} var _ types.IController = &CommitMessageController{}
func NewCommitDescriptionController( func NewCommitDescriptionController(
common *ControllerCommon, c *ControllerCommon,
) *CommitDescriptionController { ) *CommitDescriptionController {
return &CommitDescriptionController{ return &CommitDescriptionController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -14,11 +14,11 @@ type CommitMessageController struct {
var _ types.IController = &CommitMessageController{} var _ types.IController = &CommitMessageController{}
func NewCommitMessageController( func NewCommitMessageController(
common *ControllerCommon, c *ControllerCommon,
) *CommitMessageController { ) *CommitMessageController {
return &CommitMessageController{ return &CommitMessageController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -12,61 +12,74 @@ import (
type CommitFilesController struct { type CommitFilesController struct {
baseController baseController
*ListControllerTrait[*filetree.CommitFileNode]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &CommitFilesController{} var _ types.IController = &CommitFilesController{}
func NewCommitFilesController( func NewCommitFilesController(
common *ControllerCommon, c *ControllerCommon,
) *CommitFilesController { ) *CommitFilesController {
return &CommitFilesController{ return &CommitFilesController{
baseController: baseController{}, 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 { func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.CommitFiles.CheckoutCommitFile), Key: opts.GetKey(opts.Config.CommitFiles.CheckoutCommitFile),
Handler: self.checkSelected(self.checkout), Handler: self.withItem(self.checkout),
Description: self.c.Tr.CheckoutCommitFile, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.CheckoutCommitFile,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelected(self.discard), Handler: self.withItem(self.discard),
Description: self.c.Tr.DiscardOldFileChange, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.DiscardOldFileChange,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.OpenFile), Key: opts.GetKey(opts.Config.Universal.OpenFile),
Handler: self.checkSelected(self.open), Handler: self.withItem(self.open),
Description: self.c.Tr.OpenFile, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.OpenFile,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Edit), Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: self.checkSelected(self.edit), Handler: self.withItem(self.edit),
Description: self.c.Tr.EditFile, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.EditFile,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.OpenDiffTool), Key: opts.GetKey(opts.Config.Universal.OpenDiffTool),
Handler: self.checkSelected(self.openDiffTool), Handler: self.withItem(self.openDiffTool),
Description: self.c.Tr.OpenDiffTool, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.OpenDiffTool,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Select), Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.checkSelected(self.toggleForPatch), Handler: self.withItem(self.toggleForPatch),
Description: self.c.Tr.ToggleAddToPatch, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.ToggleAddToPatch,
}, },
{ {
Key: opts.GetKey(opts.Config.Files.ToggleStagedAll), Key: opts.GetKey(opts.Config.Files.ToggleStagedAll),
Handler: self.checkSelected(self.toggleAllForPatch), Handler: self.withItem(self.toggleAllForPatch),
Description: self.c.Tr.ToggleAllInPatch, Description: self.c.Tr.ToggleAllInPatch,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.GoInto), Key: opts.GetKey(opts.Config.Universal.GoInto),
Handler: self.checkSelected(self.enter), Handler: self.withItem(self.enter),
Description: self.c.Tr.EnterFile, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.EnterFile,
}, },
{ {
Key: opts.GetKey(opts.Config.Files.ToggleTreeView), 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 { func (self *CommitFilesController) context() *context.CommitFilesContext {
return self.c.Contexts().CommitFiles return self.c.Contexts().CommitFiles
} }

View File

@ -13,11 +13,11 @@ type ConfirmationController struct {
var _ types.IController = &ConfirmationController{} var _ types.IController = &ConfirmationController{}
func NewConfirmationController( func NewConfirmationController(
common *ControllerCommon, c *ControllerCommon,
) *ConfirmationController { ) *ConfirmationController {
return &ConfirmationController{ return &ConfirmationController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -31,11 +31,11 @@ type ContextLinesController struct {
var _ types.IController = &ContextLinesController{} var _ types.IController = &ContextLinesController{}
func NewContextLinesController( func NewContextLinesController(
common *ControllerCommon, c *ControllerCommon,
) *ContextLinesController { ) *ContextLinesController {
return &ContextLinesController{ return &ContextLinesController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -62,15 +62,22 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
if self.c.CurrentContext().GetKey() == self.c.Contexts().LocalCommits.GetKey() { if self.c.CurrentContext().GetKey() == self.c.Contexts().LocalCommits.GetKey() {
selectedCommit := self.c.Contexts().LocalCommits.GetSelected() selectedCommit := self.c.Contexts().LocalCommits.GetSelected()
if selectedCommit != nil && self.c.Git().Patch.PatchBuilder.To != selectedCommit.Sha { 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 // adding this option to index 1
menuItems = append( menuItems = append(
menuItems[:1], menuItems[:1],
append( append(
[]*types.MenuItem{ []*types.MenuItem{
{ {
Label: fmt.Sprintf(self.c.Tr.MovePatchToSelectedCommit, selectedCommit.Sha), Label: fmt.Sprintf(self.c.Tr.MovePatchToSelectedCommit, selectedCommit.Sha),
OnPress: self.handleMovePatchToSelectedCommit, OnPress: self.handleMovePatchToSelectedCommit,
Key: 'm', Key: 'm',
DisabledReason: disabledReason,
}, },
}, menuItems[1:]..., }, menuItems[1:]...,
)..., )...,

View File

@ -13,25 +13,32 @@ import (
type FilesController struct { type FilesController struct {
baseController // nolint: unused baseController // nolint: unused
c *ControllerCommon *ListControllerTrait[*filetree.FileNode]
c *ControllerCommon
} }
var _ types.IController = &FilesController{} var _ types.IController = &FilesController{}
func NewFilesController( func NewFilesController(
common *ControllerCommon, c *ControllerCommon,
) *FilesController { ) *FilesController {
return &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 { func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{ return []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.Select), Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.checkSelectedFileNode(self.press), Handler: self.withItem(self.press),
Description: self.c.Tr.ToggleStaged, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.ToggleStaged,
}, },
{ {
Key: opts.GetKey(opts.Config.Files.OpenStatusFilter), Key: opts.GetKey(opts.Config.Files.OpenStatusFilter),
@ -71,20 +78,23 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
Tooltip: self.c.Tr.FindBaseCommitForFixupTooltip, Tooltip: self.c.Tr.FindBaseCommitForFixupTooltip,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Edit), Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: self.checkSelectedFileNode(self.edit), Handler: self.withItem(self.edit),
Description: self.c.Tr.EditFile, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.EditFile,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.OpenFile), Key: opts.GetKey(opts.Config.Universal.OpenFile),
Handler: self.Open, Handler: self.Open,
Description: self.c.Tr.OpenFile, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.OpenFile,
}, },
{ {
Key: opts.GetKey(opts.Config.Files.IgnoreFile), Key: opts.GetKey(opts.Config.Files.IgnoreFile),
Handler: self.checkSelectedFileNode(self.ignoreOrExcludeMenu), Handler: self.withItem(self.ignoreOrExcludeMenu),
Description: self.c.Tr.Actions.IgnoreExcludeFile, GetDisabledReason: self.require(self.singleItemSelected()),
OpensMenu: true, Description: self.c.Tr.Actions.IgnoreExcludeFile,
OpensMenu: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Files.RefreshFiles), Key: opts.GetKey(opts.Config.Files.RefreshFiles),
@ -108,9 +118,10 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
Description: self.c.Tr.ToggleStagedAll, Description: self.c.Tr.ToggleStagedAll,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.GoInto), Key: opts.GetKey(opts.Config.Universal.GoInto),
Handler: self.enter, Handler: self.enter,
Description: self.c.Tr.FileEnter, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.FileEnter,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
@ -130,9 +141,10 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
Description: self.c.Tr.ToggleTreeView, Description: self.c.Tr.ToggleTreeView,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.OpenDiffTool), Key: opts.GetKey(opts.Config.Universal.OpenDiffTool),
Handler: self.checkSelectedFileNode(self.openDiffTool), Handler: self.withItem(self.openDiffTool),
Description: self.c.Tr.OpenDiffTool, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.OpenDiffTool,
}, },
{ {
Key: opts.GetKey(opts.Config.Files.OpenMergeTool), Key: opts.GetKey(opts.Config.Files.OpenMergeTool),
@ -254,7 +266,7 @@ func (self *FilesController) GetOnRenderToMain() func() error {
} }
func (self *FilesController) GetOnClick() 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, // 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{}) 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 { func (self *FilesController) Context() types.Context {
return self.context() return self.context()
} }
@ -798,7 +799,8 @@ func (self *FilesController) openCopyMenu() error {
self.c.Toast(self.c.Tr.FileNameCopiedToast) self.c.Toast(self.c.Tr.FileNameCopiedToast)
return nil return nil
}, },
Key: 'n', DisabledReason: self.require(self.singleItemSelected())(),
Key: 'n',
} }
copyPathItem := &types.MenuItem{ copyPathItem := &types.MenuItem{
Label: self.c.Tr.CopyFilePath, Label: self.c.Tr.CopyFilePath,
@ -809,7 +811,8 @@ func (self *FilesController) openCopyMenu() error {
self.c.Toast(self.c.Tr.FilePathCopiedToast) self.c.Toast(self.c.Tr.FilePathCopiedToast)
return nil return nil
}, },
Key: 'p', DisabledReason: self.require(self.singleItemSelected())(),
Key: 'p',
} }
copyFileDiffItem := &types.MenuItem{ copyFileDiffItem := &types.MenuItem{
Label: self.c.Tr.CopySelectedDiff, Label: self.c.Tr.CopySelectedDiff,
@ -827,6 +830,14 @@ func (self *FilesController) openCopyMenu() error {
self.c.Toast(self.c.Tr.FileDiffCopiedToast) self.c.Toast(self.c.Tr.FileDiffCopiedToast)
return nil 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', Key: 's',
} }
copyAllDiff := &types.MenuItem{ copyAllDiff := &types.MenuItem{
@ -844,21 +855,17 @@ func (self *FilesController) openCopyMenu() error {
self.c.Toast(self.c.Tr.AllFilesDiffCopiedToast) self.c.Toast(self.c.Tr.AllFilesDiffCopiedToast)
return nil return nil
}, },
DisabledReason: self.require(
func() *types.DisabledReason {
if !self.anyStagedOrTrackedFile() {
return &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError}
}
return nil
},
)(),
Key: 'a', 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{ return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.CopyToClipboardMenu, Title: self.c.Tr.CopyToClipboardMenu,
Items: []*types.MenuItem{ Items: []*types.MenuItem{

View File

@ -3,7 +3,6 @@ package controllers
import ( import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models" "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/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
@ -13,27 +12,34 @@ import (
type FilesRemoveController struct { type FilesRemoveController struct {
baseController baseController
*ListControllerTrait[*filetree.FileNode]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &FilesRemoveController{} var _ types.IController = &FilesRemoveController{}
func NewFilesRemoveController( func NewFilesRemoveController(
common *ControllerCommon, c *ControllerCommon,
) *FilesRemoveController { ) *FilesRemoveController {
return &FilesRemoveController{ return &FilesRemoveController{
baseController: baseController{}, 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 { func (self *FilesRemoveController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelectedFileNode(self.remove), Handler: self.withItem(self.remove),
Description: self.c.Tr.ViewDiscardOptions, GetDisabledReason: self.require(self.singleItemSelected()),
OpensMenu: true, 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}}) 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
}

View File

@ -4,24 +4,29 @@ import (
"fmt" "fmt"
"github.com/jesseduffield/lazygit/pkg/commands/models" "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/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
type GitFlowController struct { type GitFlowController struct {
baseController baseController
*ListControllerTrait[*models.Branch]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &GitFlowController{} var _ types.IController = &GitFlowController{}
func NewGitFlowController( func NewGitFlowController(
common *ControllerCommon, c *ControllerCommon,
) *GitFlowController { ) *GitFlowController {
return &GitFlowController{ return &GitFlowController{
baseController: baseController{}, 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{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Branches.ViewGitFlowOptions), Key: opts.GetKey(opts.Config.Branches.ViewGitFlowOptions),
Handler: self.checkSelected(self.handleCreateGitFlowMenu), Handler: self.withItem(self.handleCreateGitFlowMenu),
Description: self.c.Tr.GitFlowOptions, Description: self.c.Tr.GitFlowOptions,
OpensMenu: true, OpensMenu: true,
}, },
@ -68,6 +73,7 @@ func (self *GitFlowController) handleCreateGitFlowMenu(branch *models.Branch) er
OnPress: func() error { OnPress: func() error {
return self.gitFlowFinishBranch(branch.Name) return self.gitFlowFinishBranch(branch.Name)
}, },
DisabledReason: self.require(self.singleItemSelected())(),
}, },
{ {
Label: "start feature", Label: "start feature",
@ -102,22 +108,3 @@ func (self *GitFlowController) gitFlowFinishBranch(branchName string) error {
self.c.LogAction(self.c.Tr.Actions.GitFlowFinish) self.c.LogAction(self.c.Tr.Actions.GitFlowFinish)
return self.c.RunSubprocessAndRefresh(cmdObj) 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
}

View File

@ -11,11 +11,11 @@ type GlobalController struct {
} }
func NewGlobalController( func NewGlobalController(
common *ControllerCommon, c *ControllerCommon,
) *GlobalController { ) *GlobalController {
return &GlobalController{ return &GlobalController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -14,11 +14,11 @@ type JumpToSideWindowController struct {
} }
func NewJumpToSideWindowController( func NewJumpToSideWindowController(
common *ControllerCommon, c *ControllerCommon,
) *JumpToSideWindowController { ) *JumpToSideWindowController {
return &JumpToSideWindowController{ return &JumpToSideWindowController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -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
}

View File

@ -25,6 +25,7 @@ type (
type LocalCommitsController struct { type LocalCommitsController struct {
baseController baseController
*ListControllerTrait[*models.Commit]
c *ControllerCommon c *ControllerCommon
pullFiles PullFilesFn pullFiles PullFilesFn
@ -33,13 +34,18 @@ type LocalCommitsController struct {
var _ types.IController = &LocalCommitsController{} var _ types.IController = &LocalCommitsController{}
func NewLocalCommitsController( func NewLocalCommitsController(
common *ControllerCommon, c *ControllerCommon,
pullFiles PullFilesFn, pullFiles PullFilesFn,
) *LocalCommitsController { ) *LocalCommitsController {
return &LocalCommitsController{ return &LocalCommitsController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
pullFiles: pullFiles, 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{ outsideFilterModeBindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Commits.SquashDown), Key: opts.GetKey(opts.Config.Commits.SquashDown),
Handler: self.checkSelected(self.squashDown), Handler: self.withItem(self.squashDown),
GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForSquashDown), GetDisabledReason: self.require(
Description: self.c.Tr.SquashDown, self.singleItemSelected(self.getDisabledReasonForSquashDown),
),
Description: self.c.Tr.SquashDown,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.MarkCommitAsFixup), Key: opts.GetKey(opts.Config.Commits.MarkCommitAsFixup),
Handler: self.checkSelected(self.fixup), Handler: self.withItem(self.fixup),
GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForFixup), GetDisabledReason: self.require(
Description: self.c.Tr.FixupCommit, self.singleItemSelected(self.getDisabledReasonForFixup),
),
Description: self.c.Tr.FixupCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.RenameCommit), Key: opts.GetKey(opts.Config.Commits.RenameCommit),
Handler: self.checkSelected(self.reword), Handler: self.withItem(self.reword),
GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Reword), GetDisabledReason: self.require(
Description: self.c.Tr.RewordCommit, self.singleItemSelected(self.rebaseCommandEnabled(todo.Reword)),
),
Description: self.c.Tr.RewordCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.RenameCommitWithEditor), Key: opts.GetKey(opts.Config.Commits.RenameCommitWithEditor),
Handler: self.checkSelected(self.rewordEditor), Handler: self.withItem(self.rewordEditor),
GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Reword), GetDisabledReason: self.require(
Description: self.c.Tr.RenameCommitEditor, self.singleItemSelected(self.rebaseCommandEnabled(todo.Reword)),
),
Description: self.c.Tr.RenameCommitEditor,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelected(self.drop), Handler: self.withItem(self.drop),
GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Drop), GetDisabledReason: self.require(
Description: self.c.Tr.DeleteCommit, self.singleItemSelected(self.rebaseCommandEnabled(todo.Drop)),
),
Description: self.c.Tr.DeleteCommit,
}, },
{ {
Key: opts.GetKey(editCommitKey), Key: opts.GetKey(editCommitKey),
Handler: self.checkSelected(self.edit), Handler: self.withItem(self.edit),
GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Edit), GetDisabledReason: self.require(
Description: self.c.Tr.EditCommit, self.singleItemSelected(self.rebaseCommandEnabled(todo.Edit)),
),
Description: self.c.Tr.EditCommit,
}, },
{ {
// The user-facing description here is 'Start interactive rebase' but internally // The user-facing description here is 'Start interactive rebase' but internally
// we're calling it 'quick-start interactive rebase' to differentiate it from // we're calling it 'quick-start interactive rebase' to differentiate it from
// when you manually select the base commit. // when you manually select the base commit.
Key: opts.GetKey(opts.Config.Commits.StartInteractiveRebase), Key: opts.GetKey(opts.Config.Commits.StartInteractiveRebase),
Handler: self.checkSelected(self.quickStartInteractiveRebase), Handler: self.withItem(self.quickStartInteractiveRebase),
GetDisabledReason: self.require(self.notMidRebase, self.canFindCommitForQuickStart), GetDisabledReason: self.require(self.notMidRebase, self.canFindCommitForQuickStart),
Description: self.c.Tr.QuickStartInteractiveRebase, Description: self.c.Tr.QuickStartInteractiveRebase,
Tooltip: utils.ResolvePlaceholderString(self.c.Tr.QuickStartInteractiveRebaseTooltip, map[string]string{ 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), Key: opts.GetKey(opts.Config.Commits.PickCommit),
Handler: self.checkSelected(self.pick), Handler: self.withItem(self.pick),
GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Pick), GetDisabledReason: self.require(
Description: self.c.Tr.PickCommit, self.singleItemSelected(self.rebaseCommandEnabled(todo.Pick)),
),
Description: self.c.Tr.PickCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.CreateFixupCommit), Key: opts.GetKey(opts.Config.Commits.CreateFixupCommit),
Handler: self.checkSelected(self.createFixupCommit), Handler: self.withItem(self.createFixupCommit),
GetDisabledReason: self.disabledIfNoSelectedCommit(), GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.CreateFixupCommitDescription, Description: self.c.Tr.CreateFixupCommitDescription,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.SquashAboveCommits), Key: opts.GetKey(opts.Config.Commits.SquashAboveCommits),
Handler: self.checkSelected(self.squashAllAboveFixupCommits), Handler: self.withItem(self.squashAllAboveFixupCommits),
GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForSquashAllAboveFixupCommits), GetDisabledReason: self.require(
Description: self.c.Tr.SquashAboveCommits, self.notMidRebase,
self.singleItemSelected(),
),
Description: self.c.Tr.SquashAboveCommits,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.MoveDownCommit), Key: opts.GetKey(opts.Config.Commits.MoveDownCommit),
Handler: self.checkSelected(self.moveDown), Handler: self.withItem(self.moveDown),
GetDisabledReason: self.disabledIfNoSelectedCommit(), GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.MoveDownCommit, Description: self.c.Tr.MoveDownCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.MoveUpCommit), Key: opts.GetKey(opts.Config.Commits.MoveUpCommit),
Handler: self.checkSelected(self.moveUp), Handler: self.withItem(self.moveUp),
GetDisabledReason: self.disabledIfNoSelectedCommit(), GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.MoveUpCommit, Description: self.c.Tr.MoveUpCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.PasteCommits), Key: opts.GetKey(opts.Config.Commits.PasteCommits),
Handler: self.paste, Handler: self.paste,
GetDisabledReason: self.getDisabledReasonForPaste, GetDisabledReason: self.require(self.canPaste),
Description: self.c.Tr.PasteCommits, Description: self.c.Tr.PasteCommits,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.MarkCommitAsBaseForRebase), Key: opts.GetKey(opts.Config.Commits.MarkCommitAsBaseForRebase),
Handler: self.checkSelected(self.markAsBaseCommit), Handler: self.withItem(self.markAsBaseCommit),
GetDisabledReason: self.disabledIfNoSelectedCommit(), GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.MarkAsBaseCommit, Description: self.c.Tr.MarkAsBaseCommit,
Tooltip: self.c.Tr.MarkAsBaseCommitTooltip, Tooltip: self.c.Tr.MarkAsBaseCommitTooltip,
}, },
@ -161,27 +184,27 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
bindings := append(outsideFilterModeBindings, []*types.Binding{ bindings := append(outsideFilterModeBindings, []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Commits.AmendToCommit), Key: opts.GetKey(opts.Config.Commits.AmendToCommit),
Handler: self.checkSelected(self.amendTo), Handler: self.withItem(self.amendTo),
GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForAmendTo), GetDisabledReason: self.require(self.singleItemSelected(self.canAmend)),
Description: self.c.Tr.AmendToCommit, Description: self.c.Tr.AmendToCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.ResetCommitAuthor), Key: opts.GetKey(opts.Config.Commits.ResetCommitAuthor),
Handler: self.checkSelected(self.amendAttribute), Handler: self.withItem(self.amendAttribute),
GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForAmendTo), GetDisabledReason: self.require(self.singleItemSelected(self.canAmend)),
Description: self.c.Tr.SetResetCommitAuthor, Description: self.c.Tr.SetResetCommitAuthor,
OpensMenu: true, OpensMenu: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.RevertCommit), Key: opts.GetKey(opts.Config.Commits.RevertCommit),
Handler: self.checkSelected(self.revert), Handler: self.withItem(self.revert),
GetDisabledReason: self.disabledIfNoSelectedCommit(), GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.RevertCommit, Description: self.c.Tr.RevertCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.CreateTag), Key: opts.GetKey(opts.Config.Commits.CreateTag),
Handler: self.checkSelected(self.createTag), Handler: self.withItem(self.createTag),
GetDisabledReason: self.disabledIfNoSelectedCommit(), GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.TagCommit, Description: self.c.Tr.TagCommit,
}, },
{ {
@ -266,7 +289,7 @@ func (self *LocalCommitsController) getDisabledReasonForSquashDown(commit *model
return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit} 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 { 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 &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 { 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 { func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand) func(*models.Commit) *types.DisabledReason {
if commit.Action == models.ActionConflict { return func(commit *models.Commit) *types.DisabledReason {
return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed} if commit.Action == models.ActionConflict {
} return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed}
}
if !commit.IsTODO() { if !commit.IsTODO() {
if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE {
// If we are in a rebase, the only action that is allowed for // If we are in a rebase, the only action that is allowed for
// non-todo commits is rewording the current head commit // non-todo commits is rewording the current head commit
if !(action == todo.Reword && self.isHeadCommit()) { if !(action == todo.Reword && self.isHeadCommit()) {
return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} 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 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 { 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 { if !self.isHeadCommit() && self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE {
return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} 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 // For getting disabled reason
func (self *LocalCommitsController) notMidRebase() *types.DisabledReason { func (self *LocalCommitsController) notMidRebase() *types.DisabledReason {
if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { 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 { func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error { return func(types.OnFocusOpts) error {
context := self.context() 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 { func (self *LocalCommitsController) context() *context.LocalCommitsContext {
return self.c.Contexts().LocalCommits return self.c.Contexts().LocalCommits
} }
@ -1077,7 +1057,7 @@ func (self *LocalCommitsController) paste() error {
return self.c.Helpers().CherryPick.Paste() return self.c.Helpers().CherryPick.Paste()
} }
func (self *LocalCommitsController) getDisabledReasonForPaste() *types.DisabledReason { func (self *LocalCommitsController) canPaste() *types.DisabledReason {
if !self.c.Helpers().CherryPick.CanPaste() { if !self.c.Helpers().CherryPick.CanPaste() {
return &types.DisabledReason{Text: self.c.Tr.NoCopiedCommits} 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()) 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 { func isChangeOfRebaseTodoAllowed(action todo.TodoCommand) bool {
allowedActions := []todo.TodoCommand{ allowedActions := []todo.TodoCommand{
todo.Pick, todo.Pick,

View File

@ -7,17 +7,23 @@ import (
type MenuController struct { type MenuController struct {
baseController baseController
*ListControllerTrait[*types.MenuItem]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &MenuController{} var _ types.IController = &MenuController{}
func NewMenuController( func NewMenuController(
common *ControllerCommon, c *ControllerCommon,
) *MenuController { ) *MenuController {
return &MenuController{ return &MenuController{
baseController: baseController{}, 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 { func (self *MenuController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.Select), Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.press, Handler: self.withItem(self.press),
GetDisabledReason: self.require(self.singleItemSelected()),
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Confirm), Key: opts.GetKey(opts.Config.Universal.Confirm),
Handler: self.press, Handler: self.withItem(self.press),
Description: self.c.Tr.Execute, GetDisabledReason: self.require(self.singleItemSelected()),
Display: true, Description: self.c.Tr.Execute,
Display: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Return), 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 { func (self *MenuController) GetOnClick() func() error {
return self.press return self.withItemGraceful(self.press)
} }
func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error { 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 { func (self *MenuController) press(selectedItem *types.MenuItem) error {
return self.context().OnMenuPress(self.context().GetSelected()) return self.context().OnMenuPress(selectedItem)
} }
func (self *MenuController) close() error { func (self *MenuController) close() error {
@ -73,10 +81,6 @@ func (self *MenuController) close() error {
return self.c.PopContext() return self.c.PopContext()
} }
func (self *MenuController) Context() types.Context {
return self.context()
}
func (self *MenuController) context() *context.MenuContext { func (self *MenuController) context() *context.MenuContext {
return self.c.Contexts().Menu return self.c.Contexts().Menu
} }

View File

@ -17,11 +17,11 @@ type MergeConflictsController struct {
var _ types.IController = &MergeConflictsController{} var _ types.IController = &MergeConflictsController{}
func NewMergeConflictsController( func NewMergeConflictsController(
common *ControllerCommon, c *ControllerCommon,
) *MergeConflictsController { ) *MergeConflictsController {
return &MergeConflictsController{ return &MergeConflictsController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -14,11 +14,11 @@ type PatchBuildingController struct {
var _ types.IController = &PatchBuildingController{} var _ types.IController = &PatchBuildingController{}
func NewPatchBuildingController( func NewPatchBuildingController(
common *ControllerCommon, c *ControllerCommon,
) *PatchBuildingController { ) *PatchBuildingController {
return &PatchBuildingController{ return &PatchBuildingController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -1,23 +1,30 @@
package controllers package controllers
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
type ReflogCommitsController struct { type ReflogCommitsController struct {
baseController baseController
*ListControllerTrait[*models.Commit]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &ReflogCommitsController{} var _ types.IController = &ReflogCommitsController{}
func NewReflogCommitsController( func NewReflogCommitsController(
common *ControllerCommon, c *ControllerCommon,
) *ReflogCommitsController { ) *ReflogCommitsController {
return &ReflogCommitsController{ return &ReflogCommitsController{
baseController: baseController{}, baseController: baseController{},
c: common, ListControllerTrait: NewListControllerTrait[*models.Commit](
c,
c.Contexts().ReflogCommits,
c.Contexts().ReflogCommits.GetSelected,
),
c: c,
} }
} }

View File

@ -11,17 +11,23 @@ import (
type RemoteBranchesController struct { type RemoteBranchesController struct {
baseController baseController
*ListControllerTrait[*models.RemoteBranch]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &RemoteBranchesController{} var _ types.IController = &RemoteBranchesController{}
func NewRemoteBranchesController( func NewRemoteBranchesController(
common *ControllerCommon, c *ControllerCommon,
) *RemoteBranchesController { ) *RemoteBranchesController {
return &RemoteBranchesController{ return &RemoteBranchesController{
baseController: baseController{}, 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), 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 // 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), Handler: self.withItem(self.newLocalBranch),
Description: self.c.Tr.Checkout, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.Checkout,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.New), Key: opts.GetKey(opts.Config.Universal.New),
Handler: self.checkSelected(self.newLocalBranch), Handler: self.withItem(self.newLocalBranch),
Description: self.c.Tr.NewBranch, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.NewBranch,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch), Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch),
Handler: opts.Guards.OutsideFilterMode(self.checkSelected(self.merge)), Handler: opts.Guards.OutsideFilterMode(self.withItem(self.merge)),
Description: self.c.Tr.MergeIntoCurrentBranch, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.MergeIntoCurrentBranch,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.RebaseBranch), Key: opts.GetKey(opts.Config.Branches.RebaseBranch),
Handler: opts.Guards.OutsideFilterMode(self.checkSelected(self.rebase)), Handler: opts.Guards.OutsideFilterMode(self.withItem(self.rebase)),
Description: self.c.Tr.RebaseBranch, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.RebaseBranch,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelected(self.delete), Handler: self.withItem(self.delete),
Description: self.c.Tr.DeleteRemoteTag, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.DeleteRemoteTag,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.SetUpstream), Key: opts.GetKey(opts.Config.Branches.SetUpstream),
Handler: self.checkSelected(self.setAsUpstream), Handler: self.withItem(self.setAsUpstream),
Description: self.c.Tr.SetAsUpstream, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.SetAsUpstream,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.SortOrder), Key: opts.GetKey(opts.Config.Branches.SortOrder),
@ -65,10 +77,11 @@ func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts)
OpensMenu: true, OpensMenu: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
Handler: self.checkSelected(self.createResetMenu), Handler: self.withItem(self.createResetMenu),
Description: self.c.Tr.ViewResetOptions, GetDisabledReason: self.require(self.singleItemSelected()),
OpensMenu: true, 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 { func (self *RemoteBranchesController) context() *context.RemoteBranchesContext {
return self.c.Contexts().RemoteBranches 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 { func (self *RemoteBranchesController) delete(selectedBranch *models.RemoteBranch) error {
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(selectedBranch.RemoteName, selectedBranch.Name) return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(selectedBranch.RemoteName, selectedBranch.Name)
} }

View File

@ -14,6 +14,7 @@ import (
type RemotesController struct { type RemotesController struct {
baseController baseController
*ListControllerTrait[*models.Remote]
c *ControllerCommon c *ControllerCommon
setRemoteBranches func([]*models.RemoteBranch) setRemoteBranches func([]*models.RemoteBranch)
@ -22,12 +23,17 @@ type RemotesController struct {
var _ types.IController = &RemotesController{} var _ types.IController = &RemotesController{}
func NewRemotesController( func NewRemotesController(
common *ControllerCommon, c *ControllerCommon,
setRemoteBranches func([]*models.RemoteBranch), setRemoteBranches func([]*models.RemoteBranch),
) *RemotesController { ) *RemotesController {
return &RemotesController{ return &RemotesController{
baseController: baseController{}, baseController: baseController{},
c: common, ListControllerTrait: NewListControllerTrait[*models.Remote](
c,
c.Contexts().Remotes,
c.Contexts().Remotes.GetSelected,
),
c: c,
setRemoteBranches: setRemoteBranches, setRemoteBranches: setRemoteBranches,
} }
} }
@ -35,13 +41,15 @@ func NewRemotesController(
func (self *RemotesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { func (self *RemotesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.GoInto), Key: opts.GetKey(opts.Config.Universal.GoInto),
Handler: self.checkSelected(self.enter), Handler: self.withItem(self.enter),
GetDisabledReason: self.require(self.singleItemSelected()),
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.FetchRemote), Key: opts.GetKey(opts.Config.Branches.FetchRemote),
Handler: self.checkSelected(self.fetch), Handler: self.withItem(self.fetch),
Description: self.c.Tr.FetchRemote, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.FetchRemote,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.New), Key: opts.GetKey(opts.Config.Universal.New),
@ -49,24 +57,22 @@ func (self *RemotesController) GetKeybindings(opts types.KeybindingsOpts) []*typ
Description: self.c.Tr.AddNewRemote, Description: self.c.Tr.AddNewRemote,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelected(self.remove), Handler: self.withItem(self.remove),
Description: self.c.Tr.RemoveRemote, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.RemoveRemote,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Edit), Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: self.checkSelected(self.edit), Handler: self.withItem(self.edit),
Description: self.c.Tr.EditRemote, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.EditRemote,
}, },
} }
return bindings return bindings
} }
func (self *RemotesController) Context() types.Context {
return self.context()
}
func (self *RemotesController) context() *context.RemotesContext { func (self *RemotesController) context() *context.RemotesContext {
return self.c.Contexts().Remotes return self.c.Contexts().Remotes
} }
@ -94,7 +100,7 @@ func (self *RemotesController) GetOnRenderToMain() func() error {
} }
func (self *RemotesController) GetOnClick() 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 { 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}}) 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)
}
}

View File

@ -13,11 +13,11 @@ type SearchPromptController struct {
var _ types.IController = &SearchPromptController{} var _ types.IController = &SearchPromptController{}
func NewSearchPromptController( func NewSearchPromptController(
common *ControllerCommon, c *ControllerCommon,
) *SearchPromptController { ) *SearchPromptController {
return &SearchPromptController{ return &SearchPromptController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -9,8 +9,8 @@ type SideWindowControllerFactory struct {
c *ControllerCommon c *ControllerCommon
} }
func NewSideWindowControllerFactory(common *ControllerCommon) *SideWindowControllerFactory { func NewSideWindowControllerFactory(c *ControllerCommon) *SideWindowControllerFactory {
return &SideWindowControllerFactory{c: common} return &SideWindowControllerFactory{c: c}
} }
func (self *SideWindowControllerFactory) Create(context types.Context) types.IController { func (self *SideWindowControllerFactory) Create(context types.Context) types.IController {
@ -24,12 +24,12 @@ type SideWindowController struct {
} }
func NewSideWindowController( func NewSideWindowController(
common *ControllerCommon, c *ControllerCommon,
context types.Context, context types.Context,
) *SideWindowController { ) *SideWindowController {
return &SideWindowController{ return &SideWindowController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
context: context, context: context,
} }
} }

View File

@ -13,11 +13,11 @@ type SnakeController struct {
var _ types.IController = &SnakeController{} var _ types.IController = &SnakeController{}
func NewSnakeController( func NewSnakeController(
common *ControllerCommon, c *ControllerCommon,
) *SnakeController { ) *SnakeController {
return &SnakeController{ return &SnakeController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -23,14 +23,14 @@ type StagingController struct {
var _ types.IController = &StagingController{} var _ types.IController = &StagingController{}
func NewStagingController( func NewStagingController(
common *ControllerCommon, c *ControllerCommon,
context types.IPatchExplorerContext, context types.IPatchExplorerContext,
otherContext types.IPatchExplorerContext, otherContext types.IPatchExplorerContext,
staged bool, staged bool,
) *StagingController { ) *StagingController {
return &StagingController{ return &StagingController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
context: context, context: context,
otherContext: otherContext, otherContext: otherContext,
staged: staged, staged: staged,

View File

@ -9,46 +9,57 @@ import (
type StashController struct { type StashController struct {
baseController baseController
*ListControllerTrait[*models.StashEntry]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &StashController{} var _ types.IController = &StashController{}
func NewStashController( func NewStashController(
common *ControllerCommon, c *ControllerCommon,
) *StashController { ) *StashController {
return &StashController{ return &StashController{
baseController: baseController{}, 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 { func (self *StashController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.Select), Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.checkSelected(self.handleStashApply), Handler: self.withItem(self.handleStashApply),
Description: self.c.Tr.Apply, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.Apply,
}, },
{ {
Key: opts.GetKey(opts.Config.Stash.PopStash), Key: opts.GetKey(opts.Config.Stash.PopStash),
Handler: self.checkSelected(self.handleStashPop), Handler: self.withItem(self.handleStashPop),
Description: self.c.Tr.Pop, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.Pop,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelected(self.handleStashDrop), Handler: self.withItem(self.handleStashDrop),
Description: self.c.Tr.Drop, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.Drop,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.New), Key: opts.GetKey(opts.Config.Universal.New),
Handler: self.checkSelected(self.handleNewBranchOffStashEntry), Handler: self.withItem(self.handleNewBranchOffStashEntry),
Description: self.c.Tr.NewBranch, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.NewBranch,
}, },
{ {
Key: opts.GetKey(opts.Config.Stash.RenameStash), Key: opts.GetKey(opts.Config.Stash.RenameStash),
Handler: self.checkSelected(self.handleRenameStashEntry), Handler: self.withItem(self.handleRenameStashEntry),
Description: self.c.Tr.RenameStash, 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 { func (self *StashController) context() *context.StashContext {
return self.c.Contexts().Stash return self.c.Contexts().Stash
} }

View File

@ -22,11 +22,11 @@ type StatusController struct {
var _ types.IController = &StatusController{} var _ types.IController = &StatusController{}
func NewStatusController( func NewStatusController(
common *ControllerCommon, c *ControllerCommon,
) *StatusController { ) *StatusController {
return &StatusController{ return &StatusController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -2,23 +2,30 @@ package controllers
import ( import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
type SubCommitsController struct { type SubCommitsController struct {
baseController baseController
*ListControllerTrait[*models.Commit]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &SubCommitsController{} var _ types.IController = &SubCommitsController{}
func NewSubCommitsController( func NewSubCommitsController(
common *ControllerCommon, c *ControllerCommon,
) *SubCommitsController { ) *SubCommitsController {
return &SubCommitsController{ return &SubCommitsController{
baseController: baseController{}, baseController: baseController{},
c: common, ListControllerTrait: NewListControllerTrait[*models.Commit](
c,
c.Contexts().SubCommits,
c.Contexts().SubCommits.GetSelected,
),
c: c,
} }
} }

View File

@ -14,41 +14,51 @@ import (
type SubmodulesController struct { type SubmodulesController struct {
baseController baseController
*ListControllerTrait[*models.SubmoduleConfig]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &SubmodulesController{} var _ types.IController = &SubmodulesController{}
func NewSubmodulesController( func NewSubmodulesController(
controllerCommon *ControllerCommon, c *ControllerCommon,
) *SubmodulesController { ) *SubmodulesController {
return &SubmodulesController{ return &SubmodulesController{
baseController: baseController{}, 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 { func (self *SubmodulesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{ return []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.GoInto), Key: opts.GetKey(opts.Config.Universal.GoInto),
Handler: self.checkSelected(self.enter), Handler: self.withItem(self.enter),
Description: self.c.Tr.EnterSubmodule, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.EnterSubmodule,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Select), Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.checkSelected(self.enter), Handler: self.withItem(self.enter),
Description: self.c.Tr.EnterSubmodule, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.EnterSubmodule,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelected(self.remove), Handler: self.withItem(self.remove),
Description: self.c.Tr.RemoveSubmodule, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.RemoveSubmodule,
}, },
{ {
Key: opts.GetKey(opts.Config.Submodules.Update), Key: opts.GetKey(opts.Config.Submodules.Update),
Handler: self.checkSelected(self.update), Handler: self.withItem(self.update),
Description: self.c.Tr.SubmoduleUpdate, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.SubmoduleUpdate,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.New), Key: opts.GetKey(opts.Config.Universal.New),
@ -56,14 +66,16 @@ func (self *SubmodulesController) GetKeybindings(opts types.KeybindingsOpts) []*
Description: self.c.Tr.AddSubmodule, Description: self.c.Tr.AddSubmodule,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Edit), Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: self.checkSelected(self.editURL), Handler: self.withItem(self.editURL),
Description: self.c.Tr.EditSubmoduleUrl, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.EditSubmoduleUrl,
}, },
{ {
Key: opts.GetKey(opts.Config.Submodules.Init), Key: opts.GetKey(opts.Config.Submodules.Init),
Handler: self.checkSelected(self.init), Handler: self.withItem(self.init),
Description: self.c.Tr.InitSubmodule, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.InitSubmodule,
}, },
{ {
Key: opts.GetKey(opts.Config.Submodules.BulkMenu), Key: opts.GetKey(opts.Config.Submodules.BulkMenu),
@ -80,7 +92,7 @@ func (self *SubmodulesController) GetKeybindings(opts types.KeybindingsOpts) []*
} }
func (self *SubmodulesController) GetOnClick() func() error { func (self *SubmodulesController) GetOnClick() func() error {
return self.checkSelected(self.enter) return self.withItemGraceful(self.enter)
} }
func (self *SubmodulesController) GetOnRenderToMain() func() error { func (self *SubmodulesController) GetOnRenderToMain() func() error {
@ -265,21 +277,6 @@ func (self *SubmodulesController) easterEgg() error {
return self.c.PushContext(self.c.Contexts().Snake) 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 { func (self *SubmodulesController) context() *context.SubmodulesContext {
return self.c.Contexts().Submodules return self.c.Contexts().Submodules
} }

View File

@ -7,25 +7,32 @@ import (
type SuggestionsController struct { type SuggestionsController struct {
baseController baseController
*ListControllerTrait[*types.Suggestion]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &SuggestionsController{} var _ types.IController = &SuggestionsController{}
func NewSuggestionsController( func NewSuggestionsController(
common *ControllerCommon, c *ControllerCommon,
) *SuggestionsController { ) *SuggestionsController {
return &SuggestionsController{ return &SuggestionsController{
baseController: baseController{}, 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 { func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.Confirm), Key: opts.GetKey(opts.Config.Universal.Confirm),
Handler: func() error { return self.context().State.OnConfirm() }, Handler: func() error { return self.context().State.OnConfirm() },
GetDisabledReason: self.require(self.singleItemSelected()),
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Return), 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 { func (self *SuggestionsController) context() *context.SuggestionsContext {
return self.c.Contexts().Suggestions return self.c.Contexts().Suggestions
} }

View File

@ -10,13 +10,16 @@ import (
var _ types.IController = &SwitchToDiffFilesController{} var _ types.IController = &SwitchToDiffFilesController{}
type CanSwitchToDiffFiles interface { type CanSwitchToDiffFiles interface {
types.Context types.IListContext
CanRebase() bool CanRebase() bool
GetSelectedRef() types.Ref 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 { type SwitchToDiffFilesController struct {
baseController baseController
*ListControllerTrait[types.Ref]
c *ControllerCommon c *ControllerCommon
context CanSwitchToDiffFiles context CanSwitchToDiffFiles
diffFilesContext *context.CommitFilesContext diffFilesContext *context.CommitFilesContext
@ -28,7 +31,12 @@ func NewSwitchToDiffFilesController(
diffFilesContext *context.CommitFilesContext, diffFilesContext *context.CommitFilesContext,
) *SwitchToDiffFilesController { ) *SwitchToDiffFilesController {
return &SwitchToDiffFilesController{ return &SwitchToDiffFilesController{
baseController: baseController{}, baseController: baseController{},
ListControllerTrait: NewListControllerTrait[types.Ref](
c,
context,
context.GetSelectedRef,
),
c: c, c: c,
context: context, context: context,
diffFilesContext: diffFilesContext, diffFilesContext: diffFilesContext,
@ -38,9 +46,10 @@ func NewSwitchToDiffFilesController(
func (self *SwitchToDiffFilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { func (self *SwitchToDiffFilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.GoInto), Key: opts.GetKey(opts.Config.Universal.GoInto),
Handler: self.checkSelected(self.enter), Handler: self.withItem(self.enter),
Description: self.c.Tr.ViewItemFiles, 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 { func (self *SwitchToDiffFilesController) GetOnClick() func() error {
return self.checkSelected(self.enter) return self.withItemGraceful(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)
}
} }
func (self *SwitchToDiffFilesController) enter(ref types.Ref) error { 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 { func (self *SwitchToDiffFilesController) viewFiles(opts SwitchToCommitFilesContextOpts) error {
diffFilesContext := self.diffFilesContext diffFilesContext := self.diffFilesContext

View File

@ -8,34 +8,43 @@ import (
var _ types.IController = &SwitchToSubCommitsController{} var _ types.IController = &SwitchToSubCommitsController{}
type CanSwitchToSubCommits interface { type CanSwitchToSubCommits interface {
types.Context types.IListContext
GetSelectedRef() types.Ref GetSelectedRef() types.Ref
ShowBranchHeadsInSubCommits() bool 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 { type SwitchToSubCommitsController struct {
baseController baseController
*ListControllerTrait[types.Ref]
c *ControllerCommon c *ControllerCommon
context CanSwitchToSubCommits context CanSwitchToSubCommits
} }
func NewSwitchToSubCommitsController( func NewSwitchToSubCommitsController(
controllerCommon *ControllerCommon, c *ControllerCommon,
context CanSwitchToSubCommits, context CanSwitchToSubCommits,
) *SwitchToSubCommitsController { ) *SwitchToSubCommitsController {
return &SwitchToSubCommitsController{ return &SwitchToSubCommitsController{
baseController: baseController{}, baseController: baseController{},
c: controllerCommon, ListControllerTrait: NewListControllerTrait[types.Ref](
context: context, c,
context,
context.GetSelectedRef,
),
c: c,
context: context,
} }
} }
func (self *SwitchToSubCommitsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { func (self *SwitchToSubCommitsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Handler: self.viewCommits, Handler: self.viewCommits,
Key: opts.GetKey(opts.Config.Universal.GoInto), GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.ViewCommits, 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(), ShowBranchHeads: self.context.ShowBranchHeadsInSubCommits(),
}) })
} }
func (self *SwitchToSubCommitsController) Context() types.Context {
return self.context
}

View File

@ -10,37 +10,46 @@ import (
type TagsController struct { type TagsController struct {
baseController baseController
*ListControllerTrait[*models.Tag]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &TagsController{} var _ types.IController = &TagsController{}
func NewTagsController( func NewTagsController(
common *ControllerCommon, c *ControllerCommon,
) *TagsController { ) *TagsController {
return &TagsController{ return &TagsController{
baseController: baseController{}, 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 { func (self *TagsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.Select), Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.withSelectedTag(self.checkout), Handler: self.withItem(self.checkout),
Description: self.c.Tr.Checkout, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.Checkout,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.withSelectedTag(self.delete), Handler: self.withItem(self.delete),
Description: self.c.Tr.ViewDeleteOptions, Description: self.c.Tr.ViewDeleteOptions,
OpensMenu: true, GetDisabledReason: self.require(self.singleItemSelected()),
OpensMenu: true,
}, },
{ {
Key: opts.GetKey(opts.Config.Branches.PushTag), Key: opts.GetKey(opts.Config.Branches.PushTag),
Handler: self.withSelectedTag(self.push), Handler: self.withItem(self.push),
Description: self.c.Tr.PushTag, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.PushTag,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.New), Key: opts.GetKey(opts.Config.Universal.New),
@ -48,10 +57,11 @@ func (self *TagsController) GetKeybindings(opts types.KeybindingsOpts) []*types.
Description: self.c.Tr.CreateTag, Description: self.c.Tr.CreateTag,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.ViewResetOptions), Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
Handler: self.withSelectedTag(self.createResetMenu), Handler: self.withItem(self.createResetMenu),
Description: self.c.Tr.ViewResetOptions, GetDisabledReason: self.require(self.singleItemSelected()),
OpensMenu: true, 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 { func (self *TagsController) context() *context.TagsContext {
return self.c.Contexts().Tags return self.c.Contexts().Tags
} }

View File

@ -27,11 +27,11 @@ type UndoController struct {
var _ types.IController = &UndoController{} var _ types.IController = &UndoController{}
func NewUndoController( func NewUndoController(
common *ControllerCommon, c *ControllerCommon,
) *UndoController { ) *UndoController {
return &UndoController{ return &UndoController{
baseController: baseController{}, baseController: baseController{},
c: common, c: c,
} }
} }

View File

@ -14,15 +14,21 @@ type CanViewWorktreeOptions interface {
type WorktreeOptionsController struct { type WorktreeOptionsController struct {
baseController baseController
*ListControllerTrait[string]
c *ControllerCommon c *ControllerCommon
context CanViewWorktreeOptions context CanViewWorktreeOptions
} }
func NewWorktreeOptionsController(controllerCommon *ControllerCommon, context CanViewWorktreeOptions) *WorktreeOptionsController { func NewWorktreeOptionsController(c *ControllerCommon, context CanViewWorktreeOptions) *WorktreeOptionsController {
return &WorktreeOptionsController{ return &WorktreeOptionsController{
baseController: baseController{}, baseController: baseController{},
c: controllerCommon, ListControllerTrait: NewListControllerTrait[string](
context: context, c,
context,
context.GetSelectedItemId,
),
c: c,
context: context,
} }
} }
@ -30,7 +36,7 @@ func (self *WorktreeOptionsController) GetKeybindings(opts types.KeybindingsOpts
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Worktrees.ViewWorktreeOptions), Key: opts.GetKey(opts.Config.Worktrees.ViewWorktreeOptions),
Handler: self.checkSelected(self.viewWorktreeOptions), Handler: self.withItem(self.viewWorktreeOptions),
Description: self.c.Tr.ViewWorktreeOptions, Description: self.c.Tr.ViewWorktreeOptions,
OpensMenu: true, OpensMenu: true,
}, },
@ -39,21 +45,6 @@ func (self *WorktreeOptionsController) GetKeybindings(opts types.KeybindingsOpts
return bindings 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 { func (self *WorktreeOptionsController) viewWorktreeOptions(ref string) error {
return self.c.Helpers().Worktree.ViewWorktreeOptions(self.context, ref) return self.c.Helpers().Worktree.ViewWorktreeOptions(self.context, ref)
} }

View File

@ -13,17 +13,23 @@ import (
type WorktreesController struct { type WorktreesController struct {
baseController baseController
*ListControllerTrait[*models.Worktree]
c *ControllerCommon c *ControllerCommon
} }
var _ types.IController = &WorktreesController{} var _ types.IController = &WorktreesController{}
func NewWorktreesController( func NewWorktreesController(
common *ControllerCommon, c *ControllerCommon,
) *WorktreesController { ) *WorktreesController {
return &WorktreesController{ return &WorktreesController{
baseController: baseController{}, 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, Description: self.c.Tr.CreateWorktree,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Select), Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.checkSelected(self.enter), Handler: self.withItem(self.enter),
Description: self.c.Tr.SwitchToWorktree, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.SwitchToWorktree,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Confirm), Key: opts.GetKey(opts.Config.Universal.Confirm),
Handler: self.checkSelected(self.enter), Handler: self.withItem(self.enter),
Description: self.c.Tr.SwitchToWorktree, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.SwitchToWorktree,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.OpenFile), Key: opts.GetKey(opts.Config.Universal.OpenFile),
Handler: self.checkSelected(self.open), Handler: self.withItem(self.open),
Description: self.c.Tr.OpenInEditor, GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.OpenInEditor,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelected(self.remove), Handler: self.withItem(self.remove),
Description: self.c.Tr.RemoveWorktree, 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 { func (self *WorktreesController) GetOnClick() func() error {
return self.checkSelected(self.enter) return self.withItemGraceful(self.enter)
} }
func (self *WorktreesController) enter(worktree *models.Worktree) error { 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) 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 { func (self *WorktreesController) context() *context.WorktreesContext {
return self.c.Contexts().Worktrees return self.c.Contexts().Worktrees
} }

View File

@ -139,6 +139,28 @@ func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
return nil 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) { func (gui *Gui) setCaption(caption string) {
gui.Views.Options.FgColor = gocui.ColorWhite gui.Views.Options.FgColor = gocui.ColorWhite
gui.Views.Options.FgColor |= gocui.AttrBold gui.Views.Options.FgColor |= gocui.AttrBold

View File

@ -123,28 +123,32 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
Handler: self.scrollDownMain, Handler: self.scrollDownMain,
}, },
{ {
ViewName: "files", ViewName: "files",
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
Handler: self.handleCopySelectedSideContextItemToClipboard, Handler: self.handleCopySelectedSideContextItemToClipboard,
Description: self.c.Tr.CopyFileNameToClipboard, GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason,
Description: self.c.Tr.CopyFileNameToClipboard,
}, },
{ {
ViewName: "localBranches", ViewName: "localBranches",
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
Handler: self.handleCopySelectedSideContextItemToClipboard, Handler: self.handleCopySelectedSideContextItemToClipboard,
Description: self.c.Tr.CopyBranchNameToClipboard, GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason,
Description: self.c.Tr.CopyBranchNameToClipboard,
}, },
{ {
ViewName: "remoteBranches", ViewName: "remoteBranches",
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
Handler: self.handleCopySelectedSideContextItemToClipboard, Handler: self.handleCopySelectedSideContextItemToClipboard,
Description: self.c.Tr.CopyBranchNameToClipboard, GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason,
Description: self.c.Tr.CopyBranchNameToClipboard,
}, },
{ {
ViewName: "commits", ViewName: "commits",
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
Handler: self.handleCopySelectedSideContextItemToClipboard, Handler: self.handleCopySelectedSideContextItemToClipboard,
Description: self.c.Tr.CopyCommitShaToClipboard, GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason,
Description: self.c.Tr.CopyCommitShaToClipboard,
}, },
{ {
ViewName: "commits", ViewName: "commits",
@ -153,16 +157,18 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
Description: self.c.Tr.ResetCherryPick, Description: self.c.Tr.ResetCherryPick,
}, },
{ {
ViewName: "reflogCommits", ViewName: "reflogCommits",
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
Handler: self.handleCopySelectedSideContextItemToClipboard, Handler: self.handleCopySelectedSideContextItemToClipboard,
Description: self.c.Tr.CopyCommitShaToClipboard, GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason,
Description: self.c.Tr.CopyCommitShaToClipboard,
}, },
{ {
ViewName: "subCommits", ViewName: "subCommits",
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
Handler: self.handleCopySelectedSideContextItemToClipboard, Handler: self.handleCopySelectedSideContextItemToClipboard,
Description: self.c.Tr.CopyCommitShaToClipboard, GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason,
Description: self.c.Tr.CopyCommitShaToClipboard,
}, },
{ {
ViewName: "information", ViewName: "information",
@ -171,10 +177,11 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
Handler: self.handleInfoClick, Handler: self.handleInfoClick,
}, },
{ {
ViewName: "commitFiles", ViewName: "commitFiles",
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
Handler: self.handleCopySelectedSideContextItemToClipboard, Handler: self.handleCopySelectedSideContextItemToClipboard,
Description: self.c.Tr.CopyCommitFileNameToClipboard, GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason,
Description: self.c.Tr.CopyCommitFileNameToClipboard,
}, },
{ {
ViewName: "", ViewName: "",
@ -240,10 +247,11 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
Handler: self.scrollDownConfirmationPanel, Handler: self.scrollDownConfirmationPanel,
}, },
{ {
ViewName: "submodules", ViewName: "submodules",
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
Handler: self.handleCopySelectedSideContextItemToClipboard, Handler: self.handleCopySelectedSideContextItemToClipboard,
Description: self.c.Tr.CopySubmoduleNameToClipboard, GetDisabledReason: self.getCopySelectedSideContextItemToClipboardDisabledReason,
Description: self.c.Tr.CopySubmoduleNameToClipboard,
}, },
{ {
ViewName: "extras", ViewName: "extras",

View File

@ -231,6 +231,7 @@ type IListCursor interface {
GetRangeStartIdx() (int, bool) GetRangeStartIdx() (int, bool)
GetSelectionRange() (int, int) GetSelectionRange() (int, int)
IsSelectingRange() bool IsSelectingRange() bool
AreMultipleItemsSelected() bool
ToggleStickyRange() ToggleStickyRange()
ExpandNonStickyRange(int) ExpandNonStickyRange(int)
} }

View File

@ -644,7 +644,6 @@ type TranslationSet struct {
MarkedCommitMarker string MarkedCommitMarker string
PleaseGoToURL string PleaseGoToURL string
DisabledMenuItemPrefix string DisabledMenuItemPrefix string
NoCommitSelected string
NoCopiedCommits string NoCopiedCommits string
QuickStartInteractiveRebase string QuickStartInteractiveRebase string
QuickStartInteractiveRebaseTooltip string QuickStartInteractiveRebaseTooltip string
@ -652,6 +651,9 @@ type TranslationSet struct {
ToggleRangeSelect string ToggleRangeSelect string
RangeSelectUp string RangeSelectUp string
RangeSelectDown string RangeSelectDown string
RangeSelectNotSupported string
NoItemSelected string
SelectedItemIsNotABranch string
Actions Actions Actions Actions
Bisect Bisect Bisect Bisect
Log Log Log Log
@ -1478,13 +1480,15 @@ func EnglishTranslationSet() TranslationSet {
MarkedCommitMarker: "↑↑↑ Will rebase from here ↑↑↑", MarkedCommitMarker: "↑↑↑ Will rebase from here ↑↑↑",
PleaseGoToURL: "Please go to {{.url}}", PleaseGoToURL: "Please go to {{.url}}",
DisabledMenuItemPrefix: "Disabled: ", DisabledMenuItemPrefix: "Disabled: ",
NoCommitSelected: "No commit selected",
NoCopiedCommits: "No copied commits", NoCopiedCommits: "No copied commits",
QuickStartInteractiveRebase: "Start interactive rebase", 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}}`.", 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}}`.", 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", RangeSelectUp: "Range select up",
RangeSelectDown: "Range select down", 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{ Actions: Actions{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm) // TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "Checkout commit", CheckoutCommit: "Checkout commit",

View File

@ -29,10 +29,10 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
t.ExpectPopup().Menu(). t.ExpectPopup().Menu().
Title(Equals("Copy to clipboard")). Title(Equals("Copy to clipboard")).
Select(Contains("File name")). Select(Contains("File name")).
Tooltip(Equals("Disabled: Nothing to copy")). Tooltip(Equals("Disabled: No item selected")).
Confirm(). Confirm().
Tap(func() { Tap(func() {
t.ExpectToast(Equals("Disabled: Nothing to copy")) t.ExpectToast(Equals("Disabled: No item selected"))
}). }).
Cancel() Cancel()
}) })

View File

@ -22,7 +22,14 @@ var EmptyMenu = NewIntegrationTest(NewIntegrationTestArgs{
// a string that filters everything out // a string that filters everything out
FilterOrSearch("ljasldkjaslkdjalskdjalsdjaslkd"). FilterOrSearch("ljasldkjaslkdjalskdjalsdjaslkd").
IsEmpty(). 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 // back in the files view, selecting the non-existing menu item was a no-op
t.Views().Files(). t.Views().Files().