Skip to content

Commit 2f38e3a

Browse files
authored
Merge pull request #1593 from entireio/color-trail-output
Colorize entire trail list and show output
2 parents 0622f44 + 78bb1d1 commit 2f38e3a

2 files changed

Lines changed: 109 additions & 40 deletions

File tree

cmd/entire/cli/status_style.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ type statusStyles struct {
2222
width int
2323

2424
// Styles
25-
green lipgloss.Style
26-
red lipgloss.Style
27-
gray lipgloss.Style
28-
bold lipgloss.Style
29-
dim lipgloss.Style
30-
agent lipgloss.Style // accent (magenta) for agent names
31-
cyan lipgloss.Style
32-
yellow lipgloss.Style // yellow for stale warnings
25+
green lipgloss.Style
26+
red lipgloss.Style
27+
gray lipgloss.Style
28+
bold lipgloss.Style
29+
dim lipgloss.Style
30+
agent lipgloss.Style // accent (magenta) for agent names
31+
cyan lipgloss.Style
32+
yellow lipgloss.Style // yellow for stale warnings
33+
magenta lipgloss.Style
3334
}
3435

3536
// newStatusStyles creates styles appropriate for the output writer.
@@ -51,6 +52,7 @@ func newStatusStyles(w io.Writer) statusStyles {
5152
s.agent = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(palette.Accent))
5253
s.cyan = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Info))
5354
s.yellow = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Warning))
55+
s.magenta = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Magenta))
5456
}
5557

5658
return s

cmd/entire/cli/trail_cmd.go

Lines changed: 99 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"os/exec"
1212
"strconv"
1313
"strings"
14-
"text/tabwriter"
1514
"time"
1615

1716
"github.com/entireio/cli/cmd/entire/cli/api"
@@ -21,6 +20,7 @@ import (
2120
"github.com/entireio/cli/cmd/entire/cli/trail"
2221

2322
"charm.land/huh/v2"
23+
"charm.land/lipgloss/v2"
2424
"github.com/go-git/go-git/v6"
2525
"github.com/go-git/go-git/v6/plumbing"
2626
"github.com/spf13/cobra"
@@ -251,33 +251,52 @@ func resolveTrailBySelector(ctx context.Context, client *api.Client, forge, owne
251251
}
252252

253253
func printTrailDetails(w io.Writer, m *trail.Metadata, webURL, bodyText string) {
254-
fmt.Fprintf(w, "Trail: %s\n", m.Title)
254+
// Color the same fields as the list view (STATUS/PHASE/AUTHOR); everything
255+
// else stays plain. Values are pre-colored, so alignment is unaffected.
256+
styles := newStatusStyles(w)
257+
status := string(m.Status)
258+
if style, ok := trailStatusColor(styles, m.Status); ok {
259+
status = styles.render(style, status)
260+
}
261+
author := m.AuthorLogin()
262+
if styles.colorEnabled && author != "" {
263+
author = styles.render(styles.cyan, author)
264+
}
265+
266+
// Field labels and the title line render yellow (matching the list header).
267+
label := func(s string) string { return styles.render(styles.yellow, s) }
268+
269+
fmt.Fprintf(w, "%s\n", styles.render(styles.yellow, "Trail: "+m.Title))
255270
if m.Number > 0 {
256-
fmt.Fprintf(w, " Number: %d\n", m.Number)
271+
fmt.Fprintf(w, " %s%d\n", label("Number: "), m.Number)
257272
}
258273
if !m.TrailID.IsEmpty() {
259-
fmt.Fprintf(w, " ID: %s\n", m.TrailID)
274+
fmt.Fprintf(w, " %s%s\n", label("ID: "), m.TrailID)
260275
}
261-
fmt.Fprintf(w, " Branch: %s\n", m.Branch)
262-
fmt.Fprintf(w, " Base: %s\n", m.Base)
263-
fmt.Fprintf(w, " Status: %s\n", m.Status)
264-
fmt.Fprintf(w, " Author: %s\n", m.AuthorLogin())
276+
fmt.Fprintf(w, " %s%s\n", label("Branch: "), m.Branch)
277+
fmt.Fprintf(w, " %s%s\n", label("Base: "), m.Base)
278+
fmt.Fprintf(w, " %s%s\n", label("Status: "), status)
279+
fmt.Fprintf(w, " %s%s\n", label("Author: "), author)
265280
if strings.TrimSpace(m.Phase) != "" {
266-
fmt.Fprintf(w, " Phase: %s\n", trailPhaseDisplay(m.Phase))
281+
phase := trailPhaseDisplay(m.Phase)
282+
if styles.colorEnabled {
283+
phase = styles.render(styles.yellow, phase)
284+
}
285+
fmt.Fprintf(w, " %s%s\n", label("Phase: "), phase)
267286
}
268287
if webURL != "" {
269-
fmt.Fprintf(w, " URL: %s\n", webURL)
288+
fmt.Fprintf(w, " %s%s\n", label("URL: "), webURL)
270289
}
271290
if len(m.Labels) > 0 {
272-
fmt.Fprintf(w, " Labels: %s\n", strings.Join(m.Labels, ", "))
291+
fmt.Fprintf(w, " %s%s\n", label("Labels: "), strings.Join(m.Labels, ", "))
273292
}
274293
if len(m.Assignees) > 0 {
275-
fmt.Fprintf(w, " Assignees: %s\n", strings.Join(m.Assignees, ", "))
294+
fmt.Fprintf(w, " %s%s\n", label("Assignees: "), strings.Join(m.Assignees, ", "))
276295
}
277-
fmt.Fprintf(w, " Created: %s\n", m.CreatedAt.Format("2006-01-02T15:04:05Z07:00"))
278-
fmt.Fprintf(w, " Updated: %s\n", m.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"))
296+
fmt.Fprintf(w, " %s%s\n", label("Created: "), m.CreatedAt.Format("2006-01-02T15:04:05Z07:00"))
297+
fmt.Fprintf(w, " %s%s\n", label("Updated: "), m.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"))
279298
if strings.TrimSpace(bodyText) != "" {
280-
fmt.Fprintf(w, "\nDescription:\n%s\n", bodyText)
299+
fmt.Fprintf(w, "\n%s\n%s\n", label("Description:"), bodyText)
281300
}
282301
}
283302

@@ -627,27 +646,29 @@ func printTrailListHeader(w io.Writer, opts trailListDisplayOptions, count int)
627646
}
628647

629648
func printTrailRows(w io.Writer, trails []*trail.Metadata, showAuthor, showStatus bool) {
630-
// tabwriter aligns by display columns instead of bytes, so multi-byte
631-
// branch names or logins don't throw off the table.
632-
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
649+
styles := newStatusStyles(w)
633650
showPhase := trailListHasPhase(trails)
634651
showURL := trailListHasURL(trails)
635-
columns := []string{"NUM", "BRANCH", "TITLE"}
652+
653+
// The leading two-space indent is folded into the first column so the shared
654+
// table renderer (columnWidths/writeTableRow) reproduces the list's layout.
655+
headers := []string{" NUM", "BRANCH", "TITLE"}
636656
if showStatus {
637-
columns = append(columns, "STATUS")
657+
headers = append(headers, "STATUS")
638658
}
639659
if showPhase {
640-
columns = append(columns, "PHASE")
660+
headers = append(headers, "PHASE")
641661
}
642662
if showAuthor {
643-
columns = append(columns, "AUTHOR")
663+
headers = append(headers, "AUTHOR")
644664
}
645-
columns = append(columns, "UPDATED")
665+
headers = append(headers, "UPDATED")
646666
if showURL {
647-
columns = append(columns, "URL")
667+
headers = append(headers, "URL")
648668
}
649-
fmt.Fprintln(tw, " "+strings.Join(columns, "\t"))
650-
for _, t := range trails {
669+
670+
rows := make([][]string, len(trails))
671+
for i, t := range trails {
651672
number := "-"
652673
if t.Number > 0 {
653674
number = strconv.Itoa(t.Number)
@@ -656,23 +677,50 @@ func printTrailRows(w io.Writer, trails []*trail.Metadata, showAuthor, showStatu
656677
if title == "" {
657678
title = "(untitled)"
658679
}
659-
fields := []string{number, t.Branch, title}
680+
fields := []string{" " + number, t.Branch, title}
681+
// Cells are pre-colored here; columnWidths/writeTableRow measure width
682+
// with lipgloss.Width (ANSI-agnostic), so color never shifts columns.
660683
if showStatus {
661-
fields = append(fields, trailStatusDisplay(t.Status))
684+
status := trailStatusDisplay(t.Status)
685+
if style, ok := trailStatusColor(styles, t.Status); ok {
686+
status = styles.render(style, status)
687+
}
688+
fields = append(fields, status)
662689
}
663690
if showPhase {
664-
fields = append(fields, trailPhaseDisplay(t.Phase))
691+
phase := trailPhaseDisplay(t.Phase)
692+
if styles.colorEnabled && phase != "-" {
693+
phase = styles.render(styles.yellow, phase)
694+
}
695+
fields = append(fields, phase)
665696
}
666697
if showAuthor {
667-
fields = append(fields, t.AuthorLogin())
698+
author := t.AuthorLogin()
699+
if styles.colorEnabled && author != "" {
700+
author = styles.render(styles.cyan, author)
701+
}
702+
fields = append(fields, author)
668703
}
669704
fields = append(fields, timeAgo(t.UpdatedAt))
670705
if showURL {
671706
fields = append(fields, t.URL)
672707
}
673-
fmt.Fprintln(tw, " "+strings.Join(fields, "\t"))
708+
rows[i] = fields
709+
}
710+
711+
widths := columnWidths(headers, rows)
712+
var b strings.Builder
713+
// Header row is yellow; data cells are already pre-colored, so they pass
714+
// through a disabled style. tblSt only supplies the color-enabled gate for
715+
// the header style.
716+
tblSt := newTableStyles(w)
717+
headerStyle := func(int) lipgloss.Style { return styles.yellow }
718+
plain := func(int) lipgloss.Style { return lipgloss.Style{} }
719+
writeTableRow(&b, headers, widths, headerStyle, tblSt)
720+
for _, r := range rows {
721+
writeTableRow(&b, r, widths, plain, tableStyles{})
674722
}
675-
_ = tw.Flush()
723+
fmt.Fprint(w, b.String())
676724
}
677725

678726
func trailListHasPhase(trails []*trail.Metadata) bool {
@@ -721,6 +769,25 @@ func trailStatusDisplay(status trail.Status) string {
721769
return strings.ReplaceAll(string(status), "_", " ")
722770
}
723771

772+
// trailStatusColor returns the style for a trail status: open green, merged
773+
// magenta, closed red. draft (the in-progress/building state) and any unknown
774+
// status stay uncolored. The colors avoid AUTHOR's cyan and PHASE's yellow so
775+
// the columns stay distinguishable.
776+
func trailStatusColor(styles statusStyles, status trail.Status) (lipgloss.Style, bool) {
777+
switch status {
778+
case trail.StatusOpen:
779+
return styles.green, true
780+
case trail.StatusMerged:
781+
return styles.magenta, true
782+
case trail.StatusClosed:
783+
return styles.red, true
784+
case trail.StatusDraft:
785+
return lipgloss.Style{}, false
786+
default:
787+
return lipgloss.Style{}, false
788+
}
789+
}
790+
724791
// trailCountDisplay renders a count as "shown/total" when --limit truncated
725792
// the list, so a capped page doesn't read as the total number of matches.
726793
func trailCountDisplay(shown, total int) string {

0 commit comments

Comments
 (0)