Merge pull request #1593 from entireio/color-trail-output · Entire
Merge pull request #1593 from entireio/color-trail-output
2f38e3a→main·
gtrrz-victor·2d ago·2 files·+109 added/-40 removed
Colorize entire trail list and show output
Changes
2
cmd/entire/cli
Mstatus_style.go+10/-8
Mtrail_cmd.go+99/-32
21 unmodified lines
width int
// Styles
green lipgloss.Style
red lipgloss.Style
gray lipgloss.Style
bold lipgloss.Style
dim lipgloss.Style
agent lipgloss.Style // accent (magenta) for agent names
cyan lipgloss.Style
yellow lipgloss.Style // yellow for stale warnings
green lipgloss.Style
red lipgloss.Style
gray lipgloss.Style
bold lipgloss.Style
dim lipgloss.Style
agent lipgloss.Style // accent (magenta) for agent names
cyan lipgloss.Style
yellow lipgloss.Style // yellow for stale warnings
magenta lipgloss.Style
}
// newStatusStyles creates styles appropriate for the output writer.
15 unmodified lines
s.agent = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(palette.Accent))
s.cyan = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Info))
s.yellow = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Warning))
s.magenta = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Magenta))
}
Mcmd/entire/cli/status_style.go+10/-8
10 unmodified lines
11
12
13
14
14
15
16
3 unmodified lines
20
21
22
23
24
25
26
224 unmodified lines
251
252
253
254
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
256
271
272
273
259
274
275
261
262
263
264
276
277
278
279
280
266
281
282
283
284
285
286
287
269
288
289
290
272
291
292
293
275
294
295
277
278
296
297
298
280
299
300
301
302
343 unmodified lines
646
647
648
630
631
632
649
650
651
635
652
653
654
655
656
637
657
658
659
640
660
661
662
643
663
664
645
665
666
647
667
668
649
650
669
670
671
672
673
674
2 unmodified lines
677
678
679
659
680
681
682
683
661
684
685
686
687
688
689
690
664
691
692
693
694
695
696
697
667
698
699
700
701
702
703
704
705
706
707
673
708
709
675
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
42 unmodified lines
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
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"
224 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)
}
}
343 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 {