From f113736aac4e685062351fa9d90b2c3b7c45a1d8 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 7 Sep 2026 18:38:57 +0200 Subject: [PATCH] Truncate the repo and branch names in the recent repos menu Each column of a menu is padded to the width of its widest entry, so a single long name in the first two columns of the recent repos menu pushes the path column off the right edge for every entry. Anyone whose worktree directories are named after their branches hits this: with a 90 column menu and one 42 character name, the path column starts at column 86 of 88. Truncate both names to 30 characters, and put the ones that got truncated into the item's tooltip, so that the full text is still on screen for the selected entry. Filtering keeps matching the full names and the full path, which the columns no longer show in their entirety. Co-authored-by: Claude Opus 5 (1M context) --- pkg/gui/context/menu_context.go | 4 ++ pkg/gui/controllers/helpers/repos_helper.go | 42 +++++++++++++++-- pkg/gui/types/common.go | 6 +++ pkg/i18n/english.go | 4 ++ .../misc/recent_repos_with_long_names.go | 46 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 6 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 pkg/integration/tests/misc/recent_repos_with_long_names.go diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 55a3e5bfa..aa678f263 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -89,6 +89,10 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel { }) } + if item.FilterColumns != nil { + return item.FilterColumns + } + return item.LabelColumns }, ) diff --git a/pkg/gui/controllers/helpers/repos_helper.go b/pkg/gui/controllers/helpers/repos_helper.go index c280d7153..b8e39af76 100644 --- a/pkg/gui/controllers/helpers/repos_helper.go +++ b/pkg/gui/controllers/helpers/repos_helper.go @@ -103,6 +103,35 @@ func (self *ReposHelper) getCurrentBranch(path string) string { return self.c.Tr.BranchUnknown } +// The maximum width of the repo name and branch name columns of the recent +// repos menu. Without a limit, one long name pushes the path column off the +// right edge of the menu for every entry, because each column is padded to the +// width of its widest entry. +const recentReposColumnMaxWidth = 30 + +// Spells out the repo name and the branch name of a recent repos menu item, +// but only the ones that the menu shows in truncated form. +func (self *ReposHelper) recentRepoTooltip(repoName string, branchName string) string { + type field struct { + label string + value string + } + + fields := []field{} + if utils.StringWidth(repoName) > recentReposColumnMaxWidth { + fields = append(fields, field{self.c.Tr.RecentReposRepoLabel, repoName}) + } + if utils.StringWidth(branchName) > recentReposColumnMaxWidth { + fields = append(fields, field{self.c.Tr.RecentReposBranchLabel, branchName}) + } + + labelWidth := utils.MaxFn(fields, func(f field) int { return utils.StringWidth(f.label) }) + + return strings.Join(lo.Map(fields, func(f field, _ int) string { + return utils.WithPadding(f.label, labelWidth, utils.AlignLeft) + " " + f.value + }), "\n") +} + func (self *ReposHelper) CreateRecentReposMenu() error { // we'll show an empty panel if there are no recent repos recentRepoPaths := []string{} @@ -126,20 +155,27 @@ func (self *ReposHelper) CreateRecentReposMenu() error { wg.Wait() menuItems := lo.Map(recentRepoPaths, func(path string, i int) *types.MenuItem { + repoName := filepath.Base(path) branchName := currentBranches[i] + + displayedBranchName := utils.TruncateWithEllipsis(branchName, recentReposColumnMaxWidth) if icons.IsIconEnabled() { - branchName = icons.BRANCH_ICON + " " + branchName + displayedBranchName = icons.BRANCH_ICON + " " + displayedBranchName } return &types.MenuItem{ LabelColumns: []string{ - filepath.Base(path), - style.FgCyan.Sprint(branchName), + utils.TruncateWithEllipsis(repoName, recentReposColumnMaxWidth), + style.FgCyan.Sprint(displayedBranchName), // The last segment of the path is already in the first column, // so showing the directory that contains the repo is enough to // tell repos with the same name apart. style.FgMagenta.Sprint(utils.ContractTilde(filepath.Dir(path))), }, + // Filtering matches the full text, including the parts that the + // columns above truncate or leave out. + FilterColumns: []string{repoName, branchName, path}, + Tooltip: self.recentRepoTooltip(repoName, branchName), OnPress: func() error { // Check before clearing the stack, so a refused switch doesn't // forget the submodule breadcrumb (which would leave escape diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 58ccf6d60..1603df9bb 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -310,6 +310,12 @@ type MenuItem struct { // alternative to Label. Allows specifying columns which will be auto-aligned LabelColumns []string + // The strings that filtering the menu matches against, for menus that + // abbreviate their columns to keep them narrow. If nil, LabelColumns are + // matched, so that a menu only needs to set this if what it displays is not + // the full text. + FilterColumns []string + OnPress func() error // Only applies when Label is used diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 9c72b53df..918247902 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -332,6 +332,8 @@ type TranslationSet struct { NotMidRebase string MustSelectFixupCommit string RecentRepos string + RecentReposRepoLabel string + RecentReposBranchLabel string MergeOptionsTitle string RebaseOptionsTitle string CherryPickOptionsTitle string @@ -1490,6 +1492,8 @@ func EnglishTranslationSet() *TranslationSet { NotMidRebase: "This action only works during an interactive rebase", MustSelectFixupCommit: "This action only works on fixup commits", RecentRepos: "Recent repositories", + RecentReposRepoLabel: "Repo:", + RecentReposBranchLabel: "Branch:", MergeOptionsTitle: "Merge options", RebaseOptionsTitle: "Rebase options", CherryPickOptionsTitle: "Cherry-pick options", diff --git a/pkg/integration/tests/misc/recent_repos_with_long_names.go b/pkg/integration/tests/misc/recent_repos_with_long_names.go new file mode 100644 index 000000000..7396faff3 --- /dev/null +++ b/pkg/integration/tests/misc/recent_repos_with_long_names.go @@ -0,0 +1,46 @@ +package misc + +import ( + "path/filepath" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var RecentReposWithLongNames = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Long repo and branch names are truncated in the recent repositories menu, and shown in full in the tooltip", + ExtraCmdArgs: []string{}, + ExtraEnvVars: map[string]string{ + "SHOW_RECENT_REPOS": "true", + }, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + // the first entry is the repo we're in, so it isn't offered + current, _ := filepath.Abs(".") + target, _ := filepath.Abs("../repo-with-a-name-that-is-far-too-long") + cfg.GetAppState().RecentRepos = []string{current, target} + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("one") + shell.NewBranch("branch-with-a-name-that-is-too-long") + shell.CloneNonBare("repo-with-a-name-that-is-far-too-long") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.ExpectPopup().Menu(). + Title(Equals("Recent repositories")). + Lines( + Contains("repo-with-a-name-that-is-far-… branch-with-a-name-that-is-to…").IsSelected(), + Contains("Cancel"), + ). + Tooltip(Equals("Repo: repo-with-a-name-that-is-far-too-long\nBranch: branch-with-a-name-that-is-too-long")). + // Filtering matches the full names, including the part that the + // menu truncates + Filter("too-long"). + Lines( + Contains("repo-with-a-name-that-is-far-…").IsSelected(), + ). + Confirm() + + t.Views().Status().Content(Contains("repo-with-a-name")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index a4e732cf0..6dcb226f1 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -366,6 +366,7 @@ var tests = []*components.IntegrationTest{ misc.FilterRecentRepos, misc.InitialOpen, misc.RecentReposOnLaunch, + misc.RecentReposWithLongNames, misc.StartInGitDir, patch_building.Apply, patch_building.ApplyInReverse,