From bae4d4c035fa595390ab8dffd2f00db31f3dc08a Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 7 Sep 2026 18:29:28 +0200 Subject: [PATCH] Show the containing directory instead of the full path in the recent repos menu The third column of the recent repos menu spells out the full path of each repo. That repeats the directory name which the first column already shows, and it writes out the home directory in full. Both are wasted width in a menu that is limited to 90 columns; the path column is the first thing to run off the right edge, and users who don't know that 'L' scrolls the menu horizontally never see it at all. Show the directory that contains the repo instead, with the home directory abbreviated to '~'. For a list of 106 recent repos this takes the column from 111 characters down to 97 at its longest, and from 47 down to 28 in the median. Co-authored-by: Claude Opus 5 (1M context) --- pkg/gui/controllers/helpers/repos_helper.go | 5 ++++- pkg/utils/utils.go | 21 ++++++++++++++++++ pkg/utils/utils_test.go | 24 +++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/pkg/gui/controllers/helpers/repos_helper.go b/pkg/gui/controllers/helpers/repos_helper.go index 1fb8588e7..c280d7153 100644 --- a/pkg/gui/controllers/helpers/repos_helper.go +++ b/pkg/gui/controllers/helpers/repos_helper.go @@ -135,7 +135,10 @@ func (self *ReposHelper) CreateRecentReposMenu() error { LabelColumns: []string{ filepath.Base(path), style.FgCyan.Sprint(branchName), - style.FgMagenta.Sprint(path), + // 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))), }, OnPress: func() error { // Check before clearing the stack, so a refused switch doesn't diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 0494bb035..d706fe719 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -120,3 +120,24 @@ func ExpandTilde(path string) string { } return filepath.Join(home, path[2:]) } + +// ContractTilde is the inverse of ExpandTilde: it replaces the current user's +// home directory at the start of a path with "~", so that paths can be shown +// in a shorter form. Paths outside the home directory are left untouched, as +// is the path if the home directory can't be determined. +func ContractTilde(path string) string { + home, err := os.UserHomeDir() + if err != nil { + return path + } + + if path == home { + return "~" + } + + if rest, found := strings.CutPrefix(path, home+string(filepath.Separator)); found { + return "~" + string(filepath.Separator) + rest + } + + return path +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 8304e7ba4..4d51575fc 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -125,3 +125,27 @@ func TestExpandTilde(t *testing.T) { }) } } + +func TestContractTilde(t *testing.T) { + home, err := os.UserHomeDir() + assert.NoError(t, err) + + scenarios := []struct { + name string + path string + expected string + }{ + {"home directory", home, "~"}, + {"path inside the home directory", filepath.Join(home, "worktrees"), filepath.Join("~", "worktrees")}, + {"path outside the home directory is untouched", filepath.Join("/absolute", "path"), filepath.Join("/absolute", "path")}, + {"path merely starting with the home directory's name is untouched", home + "-backup", home + "-backup"}, + {"relative path is untouched", filepath.Join("relative", "path"), filepath.Join("relative", "path")}, + {"empty string is untouched", "", ""}, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + assert.Equal(t, s.expected, ContractTilde(s.path)) + }) + } +}