Colorize entire trail list and show output · Entire
Colorize entire trail list and show output
a353982·
computermode·2w ago·2 files·+139 added/-32 removed
Color the STATUS (open=green, merged=magenta, closed=red; draft/unknown plain), PHASE (yellow), and AUTHOR (cyan) fields in both 'entire trail list' and 'entire trail show', plus yellow headers/labels. Coloring is gated on a TTY, so piped/non-terminal output stays byte-identical.
The list table reuses corecmd's columnWidths/writeTableRow, which pad on lipgloss.Width (ANSI-agnostic) and style after padding, so color never shifts column alignment.
Sessions
f64fb36fd424View transcript
Changes
2
cmd/entire/cli
Mtrail_cmd.go+99/-32
Mtrail_cmd_test.go+40
10 unmodified lines
11
12
13
14
14
15
16
3 unmodified lines
20
21
22
23
24
25
26
221 unmodified lines
248
249
250
251
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
253
268
269
270
256
271
272
258
259
260
261
273
274
275
276
277
263
278
279
280
281
282
283
284
266
285
286
287
269
288
289
290
272
291
292
274
275
293
294
295
277
296
297
298
299
351 unmodified lines
651
652
653
635
636
637
654
655
656
640
657
658
659
660
661
642
662
663
664
645
665
666
667
648
668
669
650
670
671
652
672
673
654
655
674
675
676
677
678
679
2 unmodified lines
682
683
684
664
685
686
687
688
666
689
690
691
692
693
694
695
669
696
697
698
699
700
701
702
672
703
704
705
706
707
708
709
710
711
712
678
713
714
680
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
42 unmodified lines
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
10 unmodified lines
"os/exec"
"strconv"
"strings"
"text/tabwriter"
"time"
"github.com/entireio/cli/cmd/entire/cli/api"
3 unmodified lines
"github.com/entireio/cli/cmd/entire/cli/trail"
"charm.land/huh/v2"
"charm.land/lipgloss/v2"
"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/plumbing"
"github.com/spf13/cobra"
221 unmodified lines
}
func printTrailDetails(w io.Writer, m *trail.Metadata, webURL, bodyText string) {
fmt.Fprintf(w, "Trail: %s\n", m.Title)
// Color the same fields as the list view (STATUS/PHASE/AUTHOR); everything
// else stays plain. Values are pre-colored, so alignment is unaffected.
styles := newStatusStyles(w)
status := string(m.Status)
if style, ok := trailStatusColor(styles, m.Status); ok {
status = styles.render(style, status)
}
author := m.AuthorLogin()
if styles.colorEnabled && author != "" {
author = styles.render(styles.cyan, author)
}
// Field labels and the title line render yellow (matching the list header).
label := func(s string) string { return styles.render(styles.yellow, s) }
fmt.Fprintf(w, "%s\n", styles.render(styles.yellow, "Trail: "+m.Title))
if m.Number > 0 {
fmt.Fprintf(w, " Number: %d\n", m.Number)
fmt.Fprintf(w, " %s%d\n", label("Number: "), m.Number)
}
if !m.TrailID.IsEmpty() {
fmt.Fprintf(w, " ID: %s\n", m.TrailID)
fmt.Fprintf(w, " %s%s\n", label("ID: "), m.TrailID)
}
fmt.Fprintf(w, " Branch: %s\n", m.Branch)
fmt.Fprintf(w, " Base: %s\n", m.Base)
fmt.Fprintf(w, " Status: %s\n", m.Status)
fmt.Fprintf(w, " Author: %s\n", m.AuthorLogin())
fmt.Fprintf(w, " %s%s\n", label("Branch: "), m.Branch)
fmt.Fprintf(w, " %s%s\n", label("Base: "), m.Base)
fmt.Fprintf(w, " %s%s\n", label("Status: "), status)
fmt.Fprintf(w, " %s%s\n", label("Author: "), author)
if strings.TrimSpace(m.Phase) != "" {
fmt.Fprintf(w, " Phase: %s\n", trailPhaseDisplay(m.Phase))
phase := trailPhaseDisplay(m.Phase)
if styles.colorEnabled {
phase = styles.render(styles.yellow, phase)
}
fmt.Fprintf(w, " %s%s\n", label("Phase: "), phase)
}
if webURL != "" {
fmt.Fprintf(w, " URL: %s\n", webURL)
fmt.Fprintf(w, " %s%s\n", label("URL: "), webURL)
}
if len(m.Labels) > 0 {
fmt.Fprintf(w, " Labels: %s\n", strings.Join(m.Labels, ", "))
fmt.Fprintf(w, " %s%s\n", label("Labels: "), strings.Join(m.Labels, ", "))
}
if len(m.Assignees) > 0 {
fmt.Fprintf(w, " Assignees: %s\n", strings.Join(m.Assignees, ", "))
fmt.Fprintf(w, " %s%s\n", label("Assignees: "), strings.Join(m.Assignees, ", "))
}
fmt.Fprintf(w, " Created: %s\n", m.CreatedAt.Format("2006-01-02T15:04:05Z07:00"))
fmt.Fprintf(w, " Updated: %s\n", m.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"))
fmt.Fprintf(w, " %s%s\n", label("Created: "), m.CreatedAt.Format("2006-01-02T15:04:05Z07:00"))
fmt.Fprintf(w, " %s%s\n", label("Updated: "), m.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"))
if strings.TrimSpace(bodyText) != "" {
fmt.Fprintf(w, "\nDescription:\n%s\n", bodyText)
fmt.Fprintf(w, "\n%s\n%s\n", label("Description:"), bodyText)
}
}
351 unmodified lines
}
func printTrailRows(w io.Writer, trails []*trail.Metadata, showAuthor, showStatus bool) {
// tabwriter aligns by display columns instead of bytes, so multi-byte
// branch names or logins don't throw off the table.
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
styles := newStatusStyles(w)
showPhase := trailListHasPhase(trails)
showURL := trailListHasURL(trails)
columns := []string{"NUM", "BRANCH", "TITLE"}
// The leading two-space indent is folded into the first column so the shared
// table renderer (columnWidths/writeTableRow) reproduces the list's layout.
headers := []string{" NUM", "BRANCH", "TITLE"}
if showStatus {
columns = append(columns, "STATUS")
headers = append(headers, "STATUS")
}
if showPhase {
columns = append(columns, "PHASE")
headers = append(headers, "PHASE")
}
if showAuthor {
columns = append(columns, "AUTHOR")
headers = append(headers, "AUTHOR")
}
columns = append(columns, "UPDATED")
headers = append(headers, "UPDATED")
if showURL {
columns = append(columns, "URL")
headers = append(headers, "URL")
}
fmt.Fprintln(tw, " "+strings.Join(columns, "\t"))
for _, t := range trails {
rows := make([][]string, len(trails))
for i, t := range trails {
number := "-"
if t.Number > 0 {
number = strconv.Itoa(t.Number)
2 unmodified lines
if title == "" {
title = "(untitled)"
}
fields := []string{number, t.Branch, title}
fields := []string{" " + number, t.Branch, title}
// Cells are pre-colored here; columnWidths/writeTableRow measure width
// with lipgloss.Width (ANSI-agnostic), so color never shifts columns.
if showStatus {
fields = append(fields, trailStatusDisplay(t.Status))
status := trailStatusDisplay(t.Status)
if style, ok := trailStatusColor(styles, t.Status); ok {
status = styles.render(style, status)
}
fields = append(fields, status)
}
if showPhase {
fields = append(fields, trailPhaseDisplay(t.Phase))
phase := trailPhaseDisplay(t.Phase)
if styles.colorEnabled && phase != "-" {
phase = styles.render(styles.yellow, phase)
}
fields = append(fields, phase)
}
if showAuthor {
fields = append(fields, t.AuthorLogin())
author := t.AuthorLogin()
if styles.colorEnabled && author != "" {
author = styles.render(styles.cyan, author)
}
fields = append(fields, author)
}
fields = append(fields, timeAgo(t.UpdatedAt))
if showURL {
fields = append(fields, t.URL)
}
fmt.Fprintln(tw, " "+strings.Join(fields, "\t"))
rows[i] = fields
}
_ = tw.Flush()
widths := columnWidths(headers, rows)
var b strings.Builder
// Header row is yellow; data cells are already pre-colored, so they pass
// through a disabled style. tblSt only supplies the color-enabled gate for
// the header style.
tblSt := newTableStyles(w)
headerStyle := func(int) lipgloss.Style { return styles.yellow }
plain := func(int) lipgloss.Style { return lipgloss.Style{} }
writeTableRow(&b, headers, widths, headerStyle, tblSt)
for _, r := range rows {
writeTableRow(&b, r, widths, plain, tableStyles{})
}
fmt.Fprint(w, b.String())
}
func trailListHasPhase(trails []*trail.Metadata) bool {
42 unmodified lines
return strings.ReplaceAll(string(status), "_", " ")
}
// trailStatusColor returns the style for a trail status: open green, merged
// magenta, closed red. draft (the in-progress/building state) and any unknown
// status stay uncolored. The colors avoid AUTHOR's cyan and PHASE's yellow so
// the columns stay distinguishable.
func trailStatusColor(styles statusStyles, status trail.Status) (lipgloss.Style, bool) {
switch status {
case trail.StatusOpen:
return styles.green, true
case trail.StatusMerged:
return styles.magenta, true
case trail.StatusClosed:
return styles.red, true
case trail.StatusDraft:
return lipgloss.Style{}, false
default:
return lipgloss.Style{}, false
}
}
// trailCountDisplay renders a count as "shown/total" when --limit truncated
// the list, so a capped page doesn't read as the total number of matches.
func trailCountDisplay(shown, total int) string {