Allow cycling pagers in reverse

With more than a couple of pagers, having to cycle forward through all
of them to reach the previous one (or to back out of an accidental press
of `|`) is tedious. Add a second binding that cycles backward.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-06-07 15:51:57 +02:00
co-authored by Claude Opus 4.8
parent 5a4247b234
commit 8534a05a2e
18 changed files with 119 additions and 0 deletions
+1
View File
@@ -675,6 +675,7 @@ keybinding:
nextScreenMode: +
prevScreenMode: _
cyclePagers: '|'
cyclePagersReverse: \
undo: z
redo: Z
filteringMenu: <ctrl+s>
@@ -23,6 +23,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` + `` | Next screen mode (normal/half/fullscreen) | |
| `` _ `` | Prev screen mode | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Cancel | |
| `` ? `` | Open keybindings menu | |
| `` <ctrl+s> `` | View filter options | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -23,6 +23,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` + `` | 次の画面モード(通常/半分/全画面) | |
| `` _ `` | 前の画面モード | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | キャンセル | |
| `` ? `` | キーバインディングメニューを開く | |
| `` <ctrl+s> `` | フィルターオプションを表示 | コミットログのフィルタリングオプションを表示し、フィルタに一致するコミットのみを表示します。 |
@@ -23,6 +23,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` + `` | 다음 스크린 모드 (normal/half/fullscreen) | |
| `` _ `` | 이전 스크린 모드 | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | 취소 | |
| `` ? `` | 매뉴 열기 | |
| `` <ctrl+s> `` | View filter-by-path options | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -23,6 +23,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` + `` | Volgende scherm modus (normaal/half/groot) | |
| `` _ `` | Vorige scherm modus | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Annuleren | |
| `` ? `` | Open menu | |
| `` <ctrl+s> `` | Bekijk scoping opties | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -23,6 +23,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` + `` | Następny tryb ekranu (normalny/półpełny/pełnoekranowy) | |
| `` _ `` | Poprzedni tryb ekranu | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Anuluj | |
| `` ? `` | Otwórz menu przypisań klawiszy | |
| `` <ctrl+s> `` | Pokaż opcje filtrowania | Pokaż opcje filtrowania dziennika commitów, tak aby pokazywane były tylko commity pasujące do filtra. |
@@ -23,6 +23,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` + `` | Modo de tela seguinte (normal/metade/tela cheia) | |
| `` _ `` | Modo de tela anterior | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Cancelar | |
| `` ? `` | Abrir o menu de atalhos do teclado | |
| `` <ctrl+s> `` | Ver opções de filtro | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -23,6 +23,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` + `` | Следующий режим экрана (нормальный/полуэкранный/полноэкранный) | |
| `` _ `` | Предыдущий режим экрана | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Отменить | |
| `` ? `` | Открыть меню | |
| `` <ctrl+s> `` | Просмотреть параметры фильтрации по пути | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -23,6 +23,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` + `` | 下一屏模式(正常/半屏/全屏) | |
| `` _ `` | 上一屏模式 | |
| `` \| `` | 切换分页器 | 从已配置的分页器列表中选择下一个分页器 |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | 取消 | |
| `` ? `` | 打开菜单 | |
| `` <ctrl+s> `` | 查看按路径过滤选项 | 查看用于过滤提交日志的选项,以便仅显示与过滤器匹配的提交。 |
@@ -23,6 +23,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` + `` | 下一個螢幕模式(常規/半螢幕/全螢幕) | |
| `` _ `` | 上一個螢幕模式 | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | 取消 | |
| `` ? `` | 開啟選單 | |
| `` <ctrl+s> `` | 檢視篩選路徑選項 | View options for filtering the commit log, so that only commits matching the filter are shown. |
+5
View File
@@ -78,6 +78,11 @@ func (self *PagerConfig) CyclePagers() {
self.pagerIndex = (self.pagerIndex + 1) % len(self.getUserConfig().Git.Pagers)
}
func (self *PagerConfig) CyclePagersBackward() {
n := len(self.getUserConfig().Git.Pagers)
self.pagerIndex = (self.pagerIndex - 1 + n) % n
}
func (self *PagerConfig) CurrentPagerIndex() (int, int) {
return self.pagerIndex, len(self.getUserConfig().Git.Pagers)
}
+25
View File
@@ -55,3 +55,28 @@ func TestCurrentPagerNameWithoutPagers(t *testing.T) {
assert.Equal(t, "", config.CurrentPagerName())
}
func TestCyclePagers(t *testing.T) {
userConfig := &UserConfig{}
userConfig.Git.Pagers = []PagingConfig{{Name: "a"}, {Name: "b"}, {Name: "c"}}
config := NewPagerConfig(func() *UserConfig { return userConfig })
currentIndex := func() int {
index, _ := config.CurrentPagerIndex()
return index
}
assert.Equal(t, 0, currentIndex())
config.CyclePagers()
assert.Equal(t, 1, currentIndex())
config.CyclePagers()
assert.Equal(t, 2, currentIndex())
config.CyclePagers()
assert.Equal(t, 0, currentIndex(), "cycling forward past the last pager wraps to the first")
config.CyclePagersBackward()
assert.Equal(t, 2, currentIndex(), "cycling backward past the first pager wraps to the last")
config.CyclePagersBackward()
assert.Equal(t, 1, currentIndex())
}
+2
View File
@@ -511,6 +511,7 @@ type KeybindingUniversalConfig struct {
NextScreenMode Keybinding `yaml:"nextScreenMode"`
PrevScreenMode Keybinding `yaml:"prevScreenMode"`
CyclePagers Keybinding `yaml:"cyclePagers"`
CyclePagersReverse Keybinding `yaml:"cyclePagersReverse"`
Undo Keybinding `yaml:"undo"`
Redo Keybinding `yaml:"redo"`
FilteringMenu Keybinding `yaml:"filteringMenu"`
@@ -1019,6 +1020,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
NextScreenMode: Keybinding{"+"},
PrevScreenMode: Keybinding{"_"},
CyclePagers: Keybinding{"|"},
CyclePagersReverse: Keybinding{"\\"},
Undo: Keybinding{"z"},
Redo: Keybinding{"Z"},
FilteringMenu: Keybinding{"<ctrl+s>"},
+13
View File
@@ -68,6 +68,13 @@ func (self *GlobalController) GetKeybindings(opts types.KeybindingsOpts) []*type
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,
@@ -163,6 +170,12 @@ func (self *GlobalController) cyclePagers() error {
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() {
+4
View File
@@ -607,6 +607,8 @@ type TranslationSet struct {
PrevScreenMode string
CyclePagers string
CyclePagersTooltip string
CyclePagersReverse string
CyclePagersReverseTooltip string
CyclePagersDisabledReason string
SelectedPager string
DefaultPagerName string
@@ -1741,6 +1743,8 @@ func EnglishTranslationSet() *TranslationSet {
PrevScreenMode: "Prev screen mode",
CyclePagers: "Cycle pagers",
CyclePagersTooltip: "Choose the next pager in the list of configured pagers.",
CyclePagersReverse: "Cycle pagers (reverse)",
CyclePagersReverseTooltip: "Choose the previous pager in the list of configured pagers.",
CyclePagersDisabledReason: "No other pagers configured",
SelectedPager: "Pager: {{.name}} ({{.current}} of {{.total}})",
DefaultPagerName: "(default)",
@@ -0,0 +1,45 @@
package diff
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CyclePagers = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Cycle forwards and backwards through configured pagers",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().Git.Pagers = []config.PagingConfig{
// an explicit name overrides the derived one
{Name: "custom name", Pager: "cat"},
// no name, so it's derived from the first word of the command
{Pager: "cat -n"},
// neither name nor command, so it falls back to the default label
{},
}
},
SetupRepo: func(shell *Shell) {
shell.CreateNCommits(1)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Press(keys.Universal.CyclePagers)
t.ExpectToast(Equals("Pager: cat (2 of 3)"))
t.Views().Commits().Press(keys.Universal.CyclePagers)
t.ExpectToast(Equals("Pager: (default) (3 of 3)"))
// cycling forward past the last pager wraps around to the first
t.Views().Commits().Press(keys.Universal.CyclePagers)
t.ExpectToast(Equals("Pager: custom name (1 of 3)"))
// cycling backward past the first pager wraps around to the last
t.Views().Commits().Press(keys.Universal.CyclePagersReverse)
t.ExpectToast(Equals("Pager: (default) (3 of 3)"))
t.Views().Commits().Press(keys.Universal.CyclePagersReverse)
t.ExpectToast(Equals("Pager: cat (2 of 3)"))
},
})
+1
View File
@@ -210,6 +210,7 @@ var tests = []*components.IntegrationTest{
demo.Undo,
demo.WorktreeCreateFromBranches,
diff.CopyToClipboard,
diff.CyclePagers,
diff.Diff,
diff.DiffAndApplyPatch,
diff.DiffCommits,
+14
View File
@@ -3127,6 +3127,20 @@
],
"default": "|"
},
"cyclePagersReverse": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string"
},
"type": "array"
}
],
"default": "\\"
},
"undo": {
"oneOf": [
{