1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2026-04-07 19:17:06 +02:00
Files
lazygit/pkg/gui/controllers/status_controller.go
Zak Siddiqui 7d32e45f73 Add backward cycling support for all branches log view
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.
2026-03-08 17:06:17 +01:00

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()
}