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

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 {