mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 10:13:41 -05:00
Some terminals provide a modifier key that bypasses mouse capture so that text can be selected with the mouse and copied to the clipboard (option in iTerm, shift in Ghostty), but others don't (e.g. the terminals built into VS Code and Zed). In those, selecting text requires setting gui.mouseEvents to false in the config file and back again afterwards, which is inconvenient. Add a global keybinding (% by default) that toggles mouse capture on the fly. Like the whitespace toggle, it flips the in-memory config value, so the toggled state lasts until the config file is changed and reloaded. The key needs to be free in every panel, and % is; as a plain rune it also arrives reliably in every terminal on all platforms, and it can't fire while typing in a text prompt, since editors consume plain runes. An alt-based binding would have been more mnemonic, but doesn't reach lazygit in the built-in terminals of VS Code and Zed on macOS (the very terminals that need this feature) unless the option key is configured to act as meta; and a ctrl-based binding would burn one of the few remaining free ctrl keys, which are better saved for future features that get used more frequently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
308 lines
9.3 KiB
Go
308 lines
9.3 KiB
Go
package controllers
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
)
|
|
|
|
type GlobalController struct {
|
|
baseController
|
|
c *ControllerCommon
|
|
}
|
|
|
|
func NewGlobalController(
|
|
c *ControllerCommon,
|
|
) *GlobalController {
|
|
return &GlobalController{
|
|
baseController: baseController{},
|
|
c: c,
|
|
}
|
|
}
|
|
|
|
func (self *GlobalController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
|
return []*types.Binding{
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.ExecuteShellCommand),
|
|
Handler: self.shellCommand,
|
|
Description: self.c.Tr.ExecuteShellCommand,
|
|
Tooltip: self.c.Tr.ExecuteShellCommandTooltip,
|
|
OpensMenu: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.CreatePatchOptionsMenu),
|
|
Handler: self.createCustomPatchOptionsMenu,
|
|
Description: self.c.Tr.ViewPatchOptions,
|
|
OpensMenu: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.CreateRebaseOptionsMenu),
|
|
Handler: opts.Guards.NoPopupPanel(self.c.Helpers().MergeAndRebase.CreateRebaseOptionsMenu),
|
|
Description: self.c.Tr.ViewMergeRebaseOptions,
|
|
Tooltip: self.c.Tr.ViewMergeRebaseOptionsTooltip,
|
|
OpensMenu: true,
|
|
GetDisabledReason: self.canShowRebaseOptions,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Refresh),
|
|
Handler: opts.Guards.NoPopupPanel(self.refresh),
|
|
Description: self.c.Tr.Refresh,
|
|
Tooltip: self.c.Tr.RefreshTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.NextScreenMode),
|
|
Handler: opts.Guards.NoPopupPanel(self.nextScreenMode),
|
|
Description: self.c.Tr.NextScreenMode,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.PrevScreenMode),
|
|
Handler: opts.Guards.NoPopupPanel(self.prevScreenMode),
|
|
Description: self.c.Tr.PrevScreenMode,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.CyclePagers),
|
|
Handler: opts.Guards.NoPopupPanel(self.cyclePagers),
|
|
GetDisabledReason: self.canCyclePagers,
|
|
Description: self.c.Tr.CyclePagers,
|
|
Tooltip: self.c.Tr.CyclePagersTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.CyclePagersReverse),
|
|
Handler: opts.Guards.NoPopupPanel(self.cyclePagersBackward),
|
|
GetDisabledReason: self.canCyclePagers,
|
|
Description: self.c.Tr.CyclePagersReverse,
|
|
Tooltip: self.c.Tr.CyclePagersReverseTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Return),
|
|
Handler: self.escape,
|
|
Description: self.c.Tr.Cancel,
|
|
DescriptionFunc: self.escapeDescription,
|
|
GetDisabledReason: self.escapeEnabled,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
ViewName: "",
|
|
Keys: opts.GetKeys(opts.Config.Universal.OptionMenu),
|
|
Description: self.c.Tr.OpenKeybindingsMenu,
|
|
ShortDescription: self.c.Tr.Keybindings,
|
|
Handler: self.createOptionsMenu,
|
|
GetDisabledReason: self.optionsMenuDisabledReason,
|
|
OpensMenu: true,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
ViewName: "",
|
|
Keys: opts.GetKeys(opts.Config.Universal.FilteringMenu),
|
|
Handler: opts.Guards.NoPopupPanel(self.createFilteringMenu),
|
|
Description: self.c.Tr.OpenFilteringMenu,
|
|
Tooltip: self.c.Tr.OpenFilteringMenuTooltip,
|
|
OpensMenu: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.DiffingMenu),
|
|
Handler: opts.Guards.NoPopupPanel(self.createDiffingMenu),
|
|
Description: self.c.Tr.ViewDiffingOptions,
|
|
Tooltip: self.c.Tr.ViewDiffingOptionsTooltip,
|
|
OpensMenu: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Quit),
|
|
Description: self.c.Tr.Quit,
|
|
Handler: self.quit,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.QuitWithoutChangingDirectory),
|
|
Handler: self.quitWithoutChangingDirectory,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.SuspendApp),
|
|
Handler: self.c.Helpers().SuspendResume.SuspendApp,
|
|
Description: self.c.Tr.SuspendApp,
|
|
GetDisabledReason: func() *types.DisabledReason {
|
|
if !self.c.Helpers().SuspendResume.CanSuspendApp() {
|
|
return &types.DisabledReason{
|
|
Text: self.c.Tr.CannotSuspendApp,
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.ToggleWhitespaceInDiffView),
|
|
Handler: self.toggleWhitespace,
|
|
Description: self.c.Tr.ToggleWhitespaceInDiffView,
|
|
Tooltip: self.c.Tr.ToggleWhitespaceInDiffViewTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.ToggleMouseCapture),
|
|
Handler: self.toggleMouseCapture,
|
|
Description: self.c.Tr.ToggleMouseCapture,
|
|
Tooltip: self.c.Tr.ToggleMouseCaptureTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.EditConfig),
|
|
Handler: self.editConfig,
|
|
Description: self.c.Tr.EditConfig,
|
|
Tooltip: self.c.Tr.EditFileTooltip,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (self *GlobalController) Context() types.Context {
|
|
return nil
|
|
}
|
|
|
|
func (self *GlobalController) shellCommand() error {
|
|
return (&ShellCommandAction{c: self.c}).Call()
|
|
}
|
|
|
|
func (self *GlobalController) createCustomPatchOptionsMenu() error {
|
|
return (&CustomPatchOptionsMenuAction{c: self.c}).Call()
|
|
}
|
|
|
|
func (self *GlobalController) refresh() error {
|
|
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
|
return nil
|
|
}
|
|
|
|
func (self *GlobalController) nextScreenMode() error {
|
|
return (&ScreenModeActions{c: self.c}).Next()
|
|
}
|
|
|
|
func (self *GlobalController) prevScreenMode() error {
|
|
return (&ScreenModeActions{c: self.c}).Prev()
|
|
}
|
|
|
|
func (self *GlobalController) cyclePagers() error {
|
|
self.c.State().GetPagerConfig().CyclePagers()
|
|
self.onPagerChanged()
|
|
return nil
|
|
}
|
|
|
|
func (self *GlobalController) cyclePagersBackward() error {
|
|
self.c.State().GetPagerConfig().CyclePagersBackward()
|
|
self.onPagerChanged()
|
|
return nil
|
|
}
|
|
|
|
// onPagerChanged re-renders the main view so the newly selected pager takes
|
|
// effect, and shows a toast naming it.
|
|
func (self *GlobalController) onPagerChanged() {
|
|
currentSide := self.c.Context().CurrentSide()
|
|
currentKey := self.c.Context().Current().GetKey()
|
|
if currentSide.GetKey() == currentKey ||
|
|
currentKey == context.NORMAL_MAIN_CONTEXT_KEY ||
|
|
currentKey == context.NORMAL_SECONDARY_CONTEXT_KEY {
|
|
currentSide.HandleRenderToMain()
|
|
}
|
|
|
|
pagerConfig := self.c.State().GetPagerConfig()
|
|
current, total := pagerConfig.CurrentPagerIndex()
|
|
name := pagerConfig.CurrentPagerName()
|
|
if name == "" {
|
|
if pagerConfig.CurrentPagerUsesGitConfigDiff() {
|
|
name = self.c.Tr.ExternalDiffPagerName
|
|
} else {
|
|
name = self.c.Tr.DefaultPagerName
|
|
}
|
|
}
|
|
self.c.Toast(utils.ResolvePlaceholderString(self.c.Tr.SelectedPager, map[string]string{
|
|
"name": name,
|
|
"current": strconv.Itoa(current + 1),
|
|
"total": strconv.Itoa(total),
|
|
}))
|
|
}
|
|
|
|
func (self *GlobalController) canCyclePagers() *types.DisabledReason {
|
|
_, total := self.c.State().GetPagerConfig().CurrentPagerIndex()
|
|
if total <= 1 {
|
|
return &types.DisabledReason{
|
|
Text: self.c.Tr.CyclePagersDisabledReason,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *GlobalController) createOptionsMenu() error {
|
|
return (&OptionsMenuAction{c: self.c}).Call()
|
|
}
|
|
|
|
func (self *GlobalController) optionsMenuDisabledReason() *types.DisabledReason {
|
|
ctx := self.c.Context().Current()
|
|
// Don't show options menu while displaying popup.
|
|
if ctx.GetKind() == types.PERSISTENT_POPUP || ctx.GetKind() == types.TEMPORARY_POPUP {
|
|
// The empty error text is intentional. We don't want to show an error
|
|
// toast for this, but only hide it from the options map.
|
|
return &types.DisabledReason{Text: ""}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *GlobalController) createFilteringMenu() error {
|
|
return (&FilteringMenuAction{c: self.c}).Call()
|
|
}
|
|
|
|
func (self *GlobalController) createDiffingMenu() error {
|
|
return (&DiffingMenuAction{c: self.c}).Call()
|
|
}
|
|
|
|
func (self *GlobalController) quit() error {
|
|
return (&QuitActions{c: self.c}).Quit()
|
|
}
|
|
|
|
func (self *GlobalController) quitWithoutChangingDirectory() error {
|
|
return (&QuitActions{c: self.c}).QuitWithoutChangingDirectory()
|
|
}
|
|
|
|
func (self *GlobalController) escape() error {
|
|
return (&QuitActions{c: self.c}).Escape()
|
|
}
|
|
|
|
func (self *GlobalController) escapeDescription() string {
|
|
return (&QuitActions{c: self.c}).EscapeDescription()
|
|
}
|
|
|
|
func (self *GlobalController) escapeEnabled() *types.DisabledReason {
|
|
if (&QuitActions{c: self.c}).EscapeEnabled() {
|
|
return nil
|
|
}
|
|
|
|
// The empty error text is intentional. We don't want to show an error
|
|
// toast for this, but only hide it from the options map.
|
|
return &types.DisabledReason{Text: ""}
|
|
}
|
|
|
|
func (self *GlobalController) toggleWhitespace() error {
|
|
return (&ToggleWhitespaceAction{c: self.c}).Call()
|
|
}
|
|
|
|
func (self *GlobalController) toggleMouseCapture() error {
|
|
userConfig := self.c.UserConfig()
|
|
userConfig.Gui.MouseEvents = !userConfig.Gui.MouseEvents
|
|
self.c.GocuiGui().Mouse = userConfig.Gui.MouseEvents
|
|
|
|
if userConfig.Gui.MouseEvents {
|
|
self.c.Toast(self.c.Tr.MouseCaptureEnabled)
|
|
} else {
|
|
self.c.Toast(self.c.Tr.MouseCaptureDisabled)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *GlobalController) editConfig() error {
|
|
return (&EditConfigAction{c: self.c}).Call()
|
|
}
|
|
|
|
func (self *GlobalController) canShowRebaseOptions() *types.DisabledReason {
|
|
if self.c.Model().WorkingTreeStateAtLastCommitRefresh.None() {
|
|
return &types.DisabledReason{
|
|
Text: self.c.Tr.NotMergingOrRebasing,
|
|
}
|
|
}
|
|
return nil
|
|
}
|