@@ -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
253253func 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 , "\n Description: \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
629648func 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
678726func 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.
726793func trailCountDisplay (shown , total int ) string {
0 commit comments