diff --git a/pkg/gui/context/list_context_trait.go b/pkg/gui/context/list_context_trait.go index 74ff2f388..5dd9b50e0 100644 --- a/pkg/gui/context/list_context_trait.go +++ b/pkg/gui/context/list_context_trait.go @@ -13,6 +13,8 @@ type ListContextTrait struct { c *ContextCommon list types.IList getDisplayStrings func(startIdx int, length int) [][]string + // alignment for each column. If nil, the default is left alignment + columnAlignments []utils.Alignment } func (self *ListContextTrait) IsListContext() {} @@ -52,7 +54,10 @@ func (self *ListContextTrait) HandleFocusLost(opts types.OnFocusLostOpts) error // OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view func (self *ListContextTrait) HandleRender() error { self.list.RefreshSelectedIdx() - content := utils.RenderDisplayStrings(self.getDisplayStrings(0, self.list.Len())) + content := utils.RenderDisplayStrings( + self.getDisplayStrings(0, self.list.Len()), + self.columnAlignments, + ) self.GetViewTrait().SetContent(content) self.c.Render() self.setFooter() diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 667ea228b..6f84a8274 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -5,6 +5,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -37,6 +38,7 @@ func NewMenuContext( getDisplayStrings: viewModel.GetDisplayStrings, list: viewModel, c: c, + columnAlignments: []utils.Alignment{utils.AlignRight, utils.AlignLeft}, }, } } diff --git a/pkg/gui/context/viewport_list_context_trait.go b/pkg/gui/context/viewport_list_context_trait.go index b89dea832..120e9f967 100644 --- a/pkg/gui/context/viewport_list_context_trait.go +++ b/pkg/gui/context/viewport_list_context_trait.go @@ -17,6 +17,6 @@ func (self *ViewportListContextTrait) FocusLine() { startIdx, length := self.GetViewTrait().ViewPortYBounds() displayStrings := self.ListContextTrait.getDisplayStrings(startIdx, length) - content := utils.RenderDisplayStrings(displayStrings) + content := utils.RenderDisplayStrings(displayStrings, nil) self.GetViewTrait().SetViewPortContent(content) } diff --git a/pkg/gui/presentation/authors/authors.go b/pkg/gui/presentation/authors/authors.go index d4eb8e465..8f6325841 100644 --- a/pkg/gui/presentation/authors/authors.go +++ b/pkg/gui/presentation/authors/authors.go @@ -42,7 +42,7 @@ func LongAuthor(authorName string) string { return value } - paddedAuthorName := utils.WithPadding(authorName, 17) + paddedAuthorName := utils.WithPadding(authorName, 17, utils.AlignLeft) truncatedName := utils.TruncateWithEllipsis(paddedAuthorName, 17) value := AuthorStyle(authorName).Sprint(truncatedName) authorNameCache[authorName] = value diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go index 7d68b04d5..bb4570654 100644 --- a/pkg/gui/presentation/branches.go +++ b/pkg/gui/presentation/branches.go @@ -35,7 +35,7 @@ func getBranchDisplayStrings(b *models.Branch, fullDescription bool, diffed bool } coloredName := nameTextStyle.Sprint(displayName) - branchStatus := utils.WithPadding(ColoredBranchStatus(b, tr), 2) + branchStatus := utils.WithPadding(ColoredBranchStatus(b, tr), 2, utils.AlignLeft) coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus) recencyColor := style.FgCyan diff --git a/pkg/gui/presentation/commits_test.go b/pkg/gui/presentation/commits_test.go index 55155a704..a976d4809 100644 --- a/pkg/gui/presentation/commits_test.go +++ b/pkg/gui/presentation/commits_test.go @@ -283,7 +283,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) { s.showYouAreHereLabel, ) - renderedResult := utils.RenderDisplayStrings(result) + renderedResult := utils.RenderDisplayStrings(result, nil) t.Logf("\n%s", renderedResult) assert.EqualValues(t, s.expected, renderedResult) diff --git a/pkg/utils/formatting.go b/pkg/utils/formatting.go index 657d1d2eb..2a900d207 100644 --- a/pkg/utils/formatting.go +++ b/pkg/utils/formatting.go @@ -8,20 +8,52 @@ import ( "github.com/samber/lo" ) +type Alignment int + +const ( + AlignLeft Alignment = iota + AlignRight +) + +type ColumnConfig struct { + Width int + Alignment Alignment +} + // WithPadding pads a string as much as you want -func WithPadding(str string, padding int) string { +func WithPadding(str string, padding int, alignment Alignment) string { uncoloredStr := Decolorise(str) width := runewidth.StringWidth(uncoloredStr) if padding < width { return str } - return str + strings.Repeat(" ", padding-width) + space := strings.Repeat(" ", padding-width) + if alignment == AlignLeft { + return str + space + } else { + return space + str + } } -func RenderDisplayStrings(displayStringsArr [][]string) string { +// defaults to left-aligning each column. If you want to set the alignment of +// each column, pass in a slice of Alignment values. +func RenderDisplayStrings(displayStringsArr [][]string, columnAlignments []Alignment) string { displayStringsArr = excludeBlankColumns(displayStringsArr) padWidths := getPadWidths(displayStringsArr) - output := getPaddedDisplayStrings(displayStringsArr, padWidths) + columnConfigs := make([]ColumnConfig, len(padWidths)) + for i, padWidth := range padWidths { + // gracefully handle when columnAlignments is shorter than padWidths + alignment := AlignLeft + if len(columnAlignments) > i { + alignment = columnAlignments[i] + } + + columnConfigs[i] = ColumnConfig{ + Width: padWidth, + Alignment: alignment, + } + } + output := getPaddedDisplayStrings(displayStringsArr, columnConfigs) return output } @@ -59,23 +91,23 @@ outer: return displayStringsArr } -func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) string { +func getPaddedDisplayStrings(stringArrays [][]string, columnConfigs []ColumnConfig) string { builder := strings.Builder{} for i, stringArray := range stringArrays { if len(stringArray) == 0 { continue } - for j, padWidth := range padWidths { + for j, columnConfig := range columnConfigs { if len(stringArray)-1 < j { continue } - builder.WriteString(WithPadding(stringArray[j], padWidth)) + builder.WriteString(WithPadding(stringArray[j], columnConfig.Width, columnConfig.Alignment)) builder.WriteString(" ") } - if len(stringArray)-1 < len(padWidths) { + if len(stringArray)-1 < len(columnConfigs) { continue } - builder.WriteString(stringArray[len(padWidths)]) + builder.WriteString(stringArray[len(columnConfigs)]) if i < len(stringArrays)-1 { builder.WriteString("\n") diff --git a/pkg/utils/formatting_test.go b/pkg/utils/formatting_test.go index 5b9393d50..27c1d172c 100644 --- a/pkg/utils/formatting_test.go +++ b/pkg/utils/formatting_test.go @@ -6,34 +6,49 @@ import ( "github.com/stretchr/testify/assert" ) -// TestWithPadding is a function. func TestWithPadding(t *testing.T) { type scenario struct { - str string - padding int - expected string + str string + padding int + alignment Alignment + expected string } scenarios := []scenario{ { - "hello world !", - 1, - "hello world !", + str: "hello world !", + padding: 1, + alignment: AlignLeft, + expected: "hello world !", }, { - "hello world !", - 14, - "hello world ! ", + str: "hello world !", + padding: 14, + alignment: AlignLeft, + expected: "hello world ! ", }, { - "Güçlü", - 7, - "Güçlü ", + str: "hello world !", + padding: 14, + alignment: AlignRight, + expected: " hello world !", + }, + { + str: "Güçlü", + padding: 7, + alignment: AlignLeft, + expected: "Güçlü ", + }, + { + str: "Güçlü", + padding: 7, + alignment: AlignRight, + expected: " Güçlü", }, } for _, s := range scenarios { - assert.EqualValues(t, s.expected, WithPadding(s.str, s.padding)) + assert.EqualValues(t, s.expected, WithPadding(s.str, s.padding, s.alignment)) } } @@ -144,39 +159,66 @@ func TestTruncateWithEllipsis(t *testing.T) { func TestRenderDisplayStrings(t *testing.T) { type scenario struct { - input [][]string - expected string + input [][]string + columnAlignments []Alignment + expected string } tests := []scenario{ { - [][]string{{""}, {""}}, - "", + input: [][]string{{""}, {""}}, + columnAlignments: nil, + expected: "", }, { - [][]string{{"a"}, {""}}, - "a\n", + input: [][]string{{"a"}, {""}}, + columnAlignments: nil, + expected: "a\n", }, { - [][]string{{"a"}, {"b"}}, - "a\nb", + input: [][]string{{"a"}, {"b"}}, + columnAlignments: nil, + expected: "a\nb", }, { - [][]string{{"a", "b"}, {"c", "d"}}, - "a b\nc d", + input: [][]string{{"a", "b"}, {"c", "d"}}, + columnAlignments: nil, + expected: "a b\nc d", }, { - [][]string{{"a", "", "c"}, {"d", "", "f"}}, - "a c\nd f", + input: [][]string{{"a", "", "c"}, {"d", "", "f"}}, + columnAlignments: nil, + expected: "a c\nd f", }, { - [][]string{{"a", "", "c", ""}, {"d", "", "f", ""}}, - "a c\nd f", + input: [][]string{{"a", "", "c", ""}, {"d", "", "f", ""}}, + columnAlignments: nil, + expected: "a c\nd f", + }, + { + input: [][]string{{"abc", "", "d", ""}, {"e", "", "f", ""}}, + columnAlignments: nil, + expected: "abc d\ne f", + }, + { + input: [][]string{{"abc", "", "d", ""}, {"e", "", "f", ""}}, + columnAlignments: []Alignment{AlignLeft, AlignLeft}, // same as nil (default) + expected: "abc d\ne f", + }, + { + input: [][]string{{"abc", "", "d", ""}, {"e", "", "f", ""}}, + columnAlignments: []Alignment{AlignRight, AlignLeft}, + expected: "abc d\n e f", + }, + { + input: [][]string{{"abc", "", "d", ""}, {"e", "", "f", ""}}, + columnAlignments: []Alignment{AlignRight}, // gracefully defaults unspecified columns to left-align + expected: "abc d\n e f", }, } for _, test := range tests { - output := RenderDisplayStrings(test.input) + output := RenderDisplayStrings(test.input, test.columnAlignments) if !assert.EqualValues(t, output, test.expected) { t.Errorf("RenderDisplayStrings(%v) = %v, want %v", test.input, output, test.expected) }