Skip to content

Commit 6d0c0de

Browse files
gtrrz-victorclaude
andcommitted
fix(repo): keep repo list table colored under the pager
flushThroughPager renders into a buffer that never looks like a TTY, so printTable's writer-based color gate switched repo list to plain output on interactive terminals. Pre-style headers/cells against the real writer before the swap (as the mirror list view already does). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01KY7BSJFSJB67P46XPVH3XG43
1 parent 8632e10 commit 6d0c0de

3 files changed

Lines changed: 82 additions & 2 deletions

File tree

cmd/entire/cli/corecmd.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,43 @@ func printTable[T any](w io.Writer, headers []string, items []T, row func(T) []s
433433
return nil
434434
}
435435

436+
// preStyleTable pre-colors table headers and a row function against w's color
437+
// capability, so a command that renders its table into a pager buffer keeps
438+
// its color. printTable/renderCoreListPage decide color from the writer they
439+
// render into; under flushThroughPager that writer is an in-memory buffer,
440+
// which never looks like a TTY, so a straight render there is always plain.
441+
// Pre-styling against the real output writer here and letting the buffered
442+
// render pass the ANSI through unchanged (its own color gate is off, so it
443+
// never re-styles) restores it — the same approach the mirror-list view takes.
444+
// Identity (no wrapping) when color is off, so pipes, tests, and NO_COLOR see
445+
// bare text byte for byte.
446+
func preStyleTable[T any](w io.Writer, headers []string, row func(T) []string) ([]string, func(T) []string) {
447+
return styleTableWith(newTableStyles(w), headers, row)
448+
}
449+
450+
// styleTableWith is the pure core of preStyleTable: it applies st's header and
451+
// per-column styles to the headers and row cells, matching how printTable
452+
// colors a direct render. Split out from the writer-facing wrapper so the
453+
// enabled path is unit-testable without a real terminal. Identity when st is
454+
// disabled, so plain output stays byte-for-byte unchanged.
455+
func styleTableWith[T any](st tableStyles, headers []string, row func(T) []string) ([]string, func(T) []string) {
456+
if !st.enabled {
457+
return headers, row
458+
}
459+
styledHeaders := make([]string, len(headers))
460+
for i, h := range headers {
461+
styledHeaders[i] = st.style(st.header, h)
462+
}
463+
styledRow := func(t T) []string {
464+
cells := row(t)
465+
for i := range cells {
466+
cells[i] = st.style(st.columnStyle(i), cells[i])
467+
}
468+
return cells
469+
}
470+
return styledHeaders, styledRow
471+
}
472+
436473
// printFields writes a single record as aligned "FIELD value" lines: the
437474
// label in header gray, the value in the same primary/secondary color the
438475
// list view would give that column.

cmd/entire/cli/corecmd_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99
"testing"
1010

11+
"charm.land/lipgloss/v2"
1112
"github.com/spf13/cobra"
1213
"github.com/stretchr/testify/require"
1314
)
@@ -206,6 +207,43 @@ func TestPageModeRequested(t *testing.T) {
206207
require.True(t, mode("--page-token", ""), "an explicit empty cursor still selects page mode")
207208
}
208209

210+
// TestStyleTableWith covers the pre-styling that keeps a paged list command's
211+
// table colored: the render inside flushThroughPager targets a buffer that
212+
// never looks like a TTY, so color is decided against the real writer up front
213+
// and applied here. The enabled path must color the header row and route each
214+
// data cell through its column style (first column primary, rest secondary);
215+
// the disabled path must be an exact identity so pipes, tests, and NO_COLOR
216+
// see bare text byte for byte.
217+
func TestStyleTableWith(t *testing.T) {
218+
t.Parallel()
219+
220+
headers := []string{"ID", "NAME"}
221+
row := func(r []string) []string { return r }
222+
item := []string{"a", "b"}
223+
224+
t.Run("enabled path colors headers and routes cells by column", func(t *testing.T) {
225+
t.Parallel()
226+
st := tableStyles{
227+
enabled: true,
228+
header: lipgloss.NewStyle().Bold(true),
229+
primary: lipgloss.NewStyle().Underline(true),
230+
cell: lipgloss.NewStyle().Faint(true),
231+
}
232+
gotHeaders, gotRow := styleTableWith(st, headers, row)
233+
require.Equal(t, []string{st.header.Render("ID"), st.header.Render("NAME")}, gotHeaders)
234+
// Column 0 is the primary identifier, the rest secondary — the same
235+
// split printTable applies when it colors a direct render.
236+
require.Equal(t, []string{st.primary.Render("a"), st.cell.Render("b")}, gotRow(item))
237+
})
238+
239+
t.Run("disabled path is an exact identity", func(t *testing.T) {
240+
t.Parallel()
241+
gotHeaders, gotRow := styleTableWith(tableStyles{}, headers, row)
242+
require.Equal(t, headers, gotHeaders)
243+
require.Equal(t, item, gotRow(item))
244+
})
245+
}
246+
209247
// printTable/printFields render plain (no color/escape) when the writer
210248
// isn't a TTY — which a bytes.Buffer never is — so these assert the plain
211249
// layout directly.

cmd/entire/cli/repo.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ func newRepoListCmd() *cobra.Command {
175175
return validatePageSize(cmd, pageSize)
176176
},
177177
RunE: func(cmd *cobra.Command, args []string) error {
178+
// Decide color against the real output writer before
179+
// flushThroughPager swaps stdout for a buffer that never looks
180+
// like a TTY; the buffered render passes the pre-styled cells
181+
// through unchanged (see preStyleTable).
182+
headers, row := preStyleTable(cmd.OutOrStdout(), repoColumns, repoRow)
178183
if pageModeRequested(cmd) {
179184
return flushThroughPager(cmd, noPager, func() error {
180185
return runCore(cmd, func(ctx context.Context, c *coreapi.Client) error {
@@ -193,12 +198,12 @@ func newRepoListCmd() *cobra.Command {
193198
if err != nil {
194199
return err
195200
}
196-
return renderCoreListPage(cmd, "No repositories found in this project.", repoColumns, repoRow, out.Repos, out.NextPageToken.Or(""))
201+
return renderCoreListPage(cmd, "No repositories found in this project.", headers, row, out.Repos, out.NextPageToken.Or(""))
197202
})
198203
})
199204
}
200205
return flushThroughPager(cmd, noPager, func() error {
201-
return runCoreList(cmd, "No repositories found in this project.", repoColumns, repoRow, func(ctx context.Context, c *coreapi.Client) ([]coreapi.Repo, error) {
206+
return runCoreList(cmd, "No repositories found in this project.", headers, row, func(ctx context.Context, c *coreapi.Client) ([]coreapi.Repo, error) {
202207
projID, err := resolveProjectRef(ctx, c, args[0])
203208
if err != nil {
204209
return nil, err

0 commit comments

Comments
 (0)