Skip to content

Commit b123fac

Browse files
authored
Merge pull request #1345 from entireio/soph/labs-command-alignment
better labs command alignment
2 parents eb75c06 + 8798c52 commit b123fac

2 files changed

Lines changed: 102 additions & 3 deletions

File tree

cmd/entire/cli/labs.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"fmt"
55
"strings"
6+
"unicode/utf8"
67

78
"github.com/spf13/cobra"
89
)
@@ -94,10 +95,17 @@ Try:
9495
}
9596

9697
func renderExperimentalCommands(commands []experimentalCommandInfo) string {
98+
width := 0
99+
for _, info := range commands {
100+
if w := utf8.RuneCountInString(info.Invocation); w > width {
101+
width = w
102+
}
103+
}
104+
97105
var out strings.Builder
98106
for _, info := range commands {
99107
out.WriteString(" ")
100-
out.WriteString(padRight(info.Invocation, 16))
108+
out.WriteString(padRight(info.Invocation, width))
101109
out.WriteByte(' ')
102110
out.WriteString(info.Summary)
103111
out.WriteByte('\n')
@@ -106,8 +114,9 @@ func renderExperimentalCommands(commands []experimentalCommandInfo) string {
106114
}
107115

108116
func padRight(value string, width int) string {
109-
if len(value) >= width {
117+
n := utf8.RuneCountInString(value)
118+
if n >= width {
110119
return value
111120
}
112-
return value + strings.Repeat(" ", width-len(value))
121+
return value + strings.Repeat(" ", width-n)
113122
}

cmd/entire/cli/labs_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"strings"
66
"testing"
7+
"unicode/utf8"
78
)
89

910
func TestLabsCmd_PrintsExperimentalCommandList(t *testing.T) {
@@ -97,6 +98,95 @@ func TestRootHelp_ShowsLabsButHidesReview(t *testing.T) {
9798
}
9899
}
99100

101+
// summaryColumns returns, for each non-empty rendered row, the rune offset at
102+
// which the summary begins (i.e. the column after the padded invocation).
103+
func summaryColumns(t *testing.T, commands []experimentalCommandInfo) []int {
104+
t.Helper()
105+
var cols []int
106+
for _, line := range strings.Split(renderExperimentalCommands(commands), "\n") {
107+
if line == "" {
108+
continue
109+
}
110+
info := indexOfSummary(t, line, commands)
111+
cols = append(cols, info)
112+
}
113+
return cols
114+
}
115+
116+
// indexOfSummary finds the rune offset of a row's summary text within the line.
117+
func indexOfSummary(t *testing.T, line string, commands []experimentalCommandInfo) int {
118+
t.Helper()
119+
for _, info := range commands {
120+
if idx := strings.Index(line, info.Summary); idx >= 0 {
121+
return utf8.RuneCountInString(line[:idx])
122+
}
123+
}
124+
t.Fatalf("no known summary found in rendered line %q", line)
125+
return -1
126+
}
127+
128+
func TestRenderExperimentalCommands_SummariesAlign(t *testing.T) {
129+
t.Parallel()
130+
131+
cols := summaryColumns(t, experimentalCommands)
132+
if len(cols) < 2 {
133+
t.Fatalf("expected multiple experimental commands, got %d", len(cols))
134+
}
135+
for i, col := range cols {
136+
if col != cols[0] {
137+
t.Fatalf("summary column %d (%d) does not match first column (%d); descriptions are misaligned", i, col, cols[0])
138+
}
139+
}
140+
}
141+
142+
func TestRenderExperimentalCommands_ColumnWidthAdjustsToLongest(t *testing.T) {
143+
t.Parallel()
144+
145+
short := []experimentalCommandInfo{
146+
{Name: "a", Invocation: "entire a", Summary: "first"},
147+
{Name: "b", Invocation: "entire b", Summary: "second"},
148+
}
149+
long := []experimentalCommandInfo{
150+
{Name: "a", Invocation: "entire a", Summary: "first"},
151+
{Name: "verylongcommand", Invocation: "entire verylongcommand", Summary: "second"},
152+
}
153+
154+
shortCol := summaryColumns(t, short)[0]
155+
longCol := summaryColumns(t, long)[0]
156+
157+
if longCol <= shortCol {
158+
t.Fatalf("column should widen for a longer invocation: short=%d long=%d", shortCol, longCol)
159+
}
160+
// All rows in the long set must still align despite differing invocation lengths.
161+
for i, col := range summaryColumns(t, long) {
162+
if col != longCol {
163+
t.Fatalf("row %d column %d does not match %d", i, col, longCol)
164+
}
165+
}
166+
}
167+
168+
func TestRenderExperimentalCommands_MultiByteInvocationAligns(t *testing.T) {
169+
t.Parallel()
170+
171+
// "entire ▶▶" is 9 runes but 13 bytes (each ▶ is 3 bytes). The longest
172+
// invocation below is 12 runes, so the column width is 12. With byte-based
173+
// padding, len("entire ▶▶") == 13 >= 12 would skip padding and misalign the
174+
// row; rune-based padding correctly adds 3 spaces.
175+
commands := []experimentalCommandInfo{
176+
{Name: "long", Invocation: "entire aaaaa", Summary: "first"},
177+
{Name: "multibyte", Invocation: "entire ▶▶", Summary: "second"},
178+
}
179+
180+
if got := len("entire ▶▶"); got < 12 {
181+
t.Fatalf("test precondition broken: byte length %d should exceed column width 12", got)
182+
}
183+
184+
cols := summaryColumns(t, commands)
185+
if cols[0] != cols[1] {
186+
t.Fatalf("multi-byte invocation summary misaligned: %v", cols)
187+
}
188+
}
189+
100190
func TestLabsRegistryCommandsExistAtCanonicalPaths(t *testing.T) {
101191
t.Parallel()
102192

0 commit comments

Comments
 (0)