mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-04-07 19:17:06 +02:00
This commit implements the ability to cycle backward through different all branches log visualization modes, complementing the existing forward cycling functionality. Users can now press 'A' (Shift+a) to cycle in reverse through the available log graph views, improving navigation efficiency when they overshoot their desired view. Changes: - Added RotateAllBranchesLogIdxBackward() method to BranchCommands for backward rotation through log command candidates - Introduced AllBranchesLogGraphReverse keybinding configuration with default key 'A' (uppercase) - Implemented switchToOrRotateAllBranchesLogsBackward() handler in StatusController that mirrors forward cycling logic - Added English translation for "Show/cycle all branch logs (reverse)" The implementation uses modulo arithmetic with proper handling of negative indices to ensure seamless backward cycling through the available log visualization options.
260 lines
8.1 KiB
Go
260 lines
8.1 KiB
Go
package controllers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jesseduffield/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/constants"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type StatusController struct {
|
|
baseController
|
|
c *ControllerCommon
|
|
}
|
|
|
|
var _ types.IController = &StatusController{}
|
|
|
|
func NewStatusController(
|
|
c *ControllerCommon,
|
|
) *StatusController {
|
|
return &StatusController{
|
|
baseController: baseController{},
|
|
c: c,
|
|
}
|
|
}
|
|
|
|
func (self *StatusController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
|
bindings := []*types.Binding{
|
|
{
|
|
Key: opts.GetKey(opts.Config.Universal.OpenFile),
|
|
Handler: self.openConfig,
|
|
Description: self.c.Tr.OpenConfig,
|
|
Tooltip: self.c.Tr.OpenFileTooltip,
|
|
},
|
|
{
|
|
Key: opts.GetKey(opts.Config.Universal.Edit),
|
|
Handler: self.editConfig,
|
|
Description: self.c.Tr.EditConfig,
|
|
Tooltip: self.c.Tr.EditFileTooltip,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Key: opts.GetKey(opts.Config.Status.CheckForUpdate),
|
|
Handler: self.handleCheckForUpdate,
|
|
Description: self.c.Tr.CheckForUpdate,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Key: opts.GetKey(opts.Config.Status.RecentRepos),
|
|
Handler: self.c.Helpers().Repos.CreateRecentReposMenu,
|
|
Description: self.c.Tr.SwitchRepo,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Key: opts.GetKey(opts.Config.Status.AllBranchesLogGraph),
|
|
Handler: func() error { self.switchToOrRotateAllBranchesLogs(); return nil },
|
|
Description: self.c.Tr.AllBranchesLogGraph,
|
|
},
|
|
{
|
|
Key: opts.GetKey(opts.Config.Status.AllBranchesLogGraphReverse),
|
|
Handler: func() error { self.switchToOrRotateAllBranchesLogsBackward(); return nil },
|
|
Description: self.c.Tr.AllBranchesLogGraphReverse,
|
|
},
|
|
}
|
|
|
|
return bindings
|
|
}
|
|
|
|
func (self *StatusController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
|
|
return []*gocui.ViewMouseBinding{
|
|
{
|
|
ViewName: self.Context().GetViewName(),
|
|
Key: gocui.MouseLeft,
|
|
Handler: self.onClick,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (self *StatusController) GetOnRenderToMain() func() {
|
|
return func() {
|
|
switch self.c.UserConfig().Gui.StatusPanelView {
|
|
case "dashboard":
|
|
self.showDashboard()
|
|
case "allBranchesLog":
|
|
self.showAllBranchLogs()
|
|
default:
|
|
self.showDashboard()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *StatusController) Context() types.Context {
|
|
return self.c.Contexts().Status
|
|
}
|
|
|
|
func (self *StatusController) onClick(opts gocui.ViewMouseBindingOpts) error {
|
|
// TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives)
|
|
currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
|
|
if currentBranch == nil {
|
|
// need to wait for branches to refresh
|
|
return nil
|
|
}
|
|
|
|
self.c.Context().Push(self.Context(), types.OnFocusOpts{})
|
|
|
|
upstreamStatus := utils.Decolorise(presentation.BranchStatus(currentBranch, types.ItemOperationNone, self.c.Tr, time.Now(), self.c.UserConfig()))
|
|
repoName := self.c.Git().RepoPaths.RepoName()
|
|
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
|
if workingTreeState.Any() {
|
|
workingTreeStatus := fmt.Sprintf("(%s)", workingTreeState.LowerCaseTitle(self.c.Tr))
|
|
if cursorInSubstring(opts.X, upstreamStatus+" ", workingTreeStatus) {
|
|
return self.c.Helpers().MergeAndRebase.CreateRebaseOptionsMenu()
|
|
}
|
|
if cursorInSubstring(opts.X, upstreamStatus+" "+workingTreeStatus+" ", repoName) {
|
|
return self.c.Helpers().Repos.CreateRecentReposMenu()
|
|
}
|
|
} else if cursorInSubstring(opts.X, upstreamStatus+" ", repoName) {
|
|
return self.c.Helpers().Repos.CreateRecentReposMenu()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runeCount(str string) int {
|
|
return len([]rune(str))
|
|
}
|
|
|
|
func cursorInSubstring(cx int, prefix string, substring string) bool {
|
|
return cx >= runeCount(prefix) && cx < runeCount(prefix+substring)
|
|
}
|
|
|
|
func lazygitTitle() string {
|
|
return `
|
|
_ _ _
|
|
| | (_) |
|
|
| | __ _ _____ _ __ _ _| |_
|
|
| |/ _` + "`" + ` |_ / | | |/ _` + "`" + ` | | __|
|
|
| | (_| |/ /| |_| | (_| | | |_
|
|
|_|\__,_/___|\__, |\__, |_|\__|
|
|
__/ | __/ |
|
|
|___/ |___/ `
|
|
}
|
|
|
|
func (self *StatusController) askForConfigFile(action func(file string) error) error {
|
|
confPaths := self.c.GetConfig().GetUserConfigPaths()
|
|
switch len(confPaths) {
|
|
case 0:
|
|
return errors.New(self.c.Tr.NoConfigFileFoundErr)
|
|
case 1:
|
|
return action(confPaths[0])
|
|
default:
|
|
menuItems := lo.Map(confPaths, func(path string, _ int) *types.MenuItem {
|
|
return &types.MenuItem{
|
|
Label: path,
|
|
OnPress: func() error {
|
|
return action(path)
|
|
},
|
|
}
|
|
})
|
|
|
|
return self.c.Menu(types.CreateMenuOptions{
|
|
Title: self.c.Tr.SelectConfigFile,
|
|
Items: menuItems,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (self *StatusController) openConfig() error {
|
|
return self.askForConfigFile(self.c.Helpers().Files.OpenFile)
|
|
}
|
|
|
|
func (self *StatusController) editConfig() error {
|
|
return self.askForConfigFile(func(file string) error {
|
|
return self.c.Helpers().Files.EditFiles([]string{file})
|
|
})
|
|
}
|
|
|
|
func (self *StatusController) showAllBranchLogs() {
|
|
cmdObj := self.c.Git().Branch.AllBranchesLogCmdObj()
|
|
task := types.NewRunPtyTask(cmdObj.GetCmd())
|
|
|
|
title := self.c.Tr.LogTitle
|
|
if i, n := self.c.Git().Branch.GetAllBranchesLogIdxAndCount(); n > 1 {
|
|
title = fmt.Sprintf(self.c.Tr.LogXOfYTitle, i+1, n)
|
|
}
|
|
self.c.RenderToMainViews(types.RefreshMainOpts{
|
|
Pair: self.c.MainViewPairs().Normal,
|
|
Main: &types.ViewUpdateOpts{
|
|
Title: title,
|
|
Task: task,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Switches to the all branches view, or, if already on that view,
|
|
// rotates to the next command in the list, and then renders it.
|
|
func (self *StatusController) switchToOrRotateAllBranchesLogs() {
|
|
// A bit of a hack to ensure we only rotate to the next branch log command
|
|
// if we currently are looking at a branch log. Otherwise, we should just show
|
|
// the current index (if we are coming from the dashboard).
|
|
if self.c.Views().Main.Title != self.c.Tr.StatusTitle {
|
|
self.c.Git().Branch.RotateAllBranchesLogIdx()
|
|
}
|
|
self.showAllBranchLogs()
|
|
}
|
|
|
|
// Switches to the all branches view, or, if already on that view,
|
|
// rotates to the previous command in the list, and then renders it.
|
|
func (self *StatusController) switchToOrRotateAllBranchesLogsBackward() {
|
|
// A bit of a hack to ensure we only rotate to the previous branch log command
|
|
// if we currently are looking at a branch log. Otherwise, we should just show
|
|
// the current index (if we are coming from the dashboard).
|
|
if self.c.Views().Main.Title != self.c.Tr.StatusTitle {
|
|
self.c.Git().Branch.RotateAllBranchesLogIdxBackward()
|
|
}
|
|
self.showAllBranchLogs()
|
|
}
|
|
|
|
func (self *StatusController) showDashboard() {
|
|
versionStr := "master"
|
|
version, err := types.ParseVersionNumber(self.c.GetConfig().GetVersion())
|
|
if err == nil {
|
|
// Don't just take the version string as is, but format it again. This
|
|
// way it will be correct even if a distribution omits the "v", or the
|
|
// ".0" at the end.
|
|
versionStr = fmt.Sprintf("v%d.%d.%d", version.Major, version.Minor, version.Patch)
|
|
}
|
|
|
|
dashboardString := strings.Join(
|
|
[]string{
|
|
lazygitTitle(),
|
|
fmt.Sprintf("Copyright %d Jesse Duffield", time.Now().Year()),
|
|
fmt.Sprintf("Keybindings: %s", fmt.Sprintf(constants.Links.Docs.Keybindings, versionStr)),
|
|
fmt.Sprintf("Config Options: %s", fmt.Sprintf(constants.Links.Docs.Config, versionStr)),
|
|
fmt.Sprintf("Tutorial: %s", constants.Links.Docs.Tutorial),
|
|
fmt.Sprintf("Raise an Issue: %s", constants.Links.Issues),
|
|
fmt.Sprintf("Release Notes: %s", constants.Links.Releases),
|
|
style.FgMagenta.Sprintf("Become a sponsor: %s", constants.Links.Donate), // caffeine ain't free
|
|
}, "\n\n") + "\n"
|
|
|
|
self.c.RenderToMainViews(types.RefreshMainOpts{
|
|
Pair: self.c.MainViewPairs().Normal,
|
|
Main: &types.ViewUpdateOpts{
|
|
Title: self.c.Tr.StatusTitle,
|
|
Task: types.NewRenderStringTask(dashboardString),
|
|
},
|
|
})
|
|
}
|
|
|
|
func (self *StatusController) handleCheckForUpdate() error {
|
|
return self.c.Helpers().Update.CheckForUpdateInForeground()
|
|
}
|