Skip to content

Commit b8eb15c

Browse files
committed
feat(pb): colorize pretty log output
- colorize pretty log labels, event types, issue IDs, and detail values - keep table/json output uncolored; add colorized log test coverage - stabilize pretty log tests by disabling color for assertions Refs: pb-3ee Regeneration-Prompt: | Add ANSI color styling to `pb log` pretty output only (not table/json), respecting NO_COLOR/PB_NO_COLOR and existing colorEnabled logic. Use distinct colors for event types and issue IDs, dim field labels, and reuse existing status/priority/type color rules for detail values. Update pretty log tests to be deterministic regardless of TTY, and add a color-enabled test to assert ANSI codes appear in output.
1 parent ff42668 commit b8eb15c

File tree

3 files changed

+125
-7
lines changed

3 files changed

+125
-7
lines changed

.pebbles/events.jsonl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,5 @@
231231
{"type":"close","timestamp":"2026-01-19T23:14:04.98486Z","issue_id":"pb-9d7","payload":{}}
232232
{"type":"close","timestamp":"2026-01-19T23:14:09.877599Z","issue_id":"pb-d59","payload":{}}
233233
{"type":"close","timestamp":"2026-01-19T23:14:16.391561Z","issue_id":"pb-fc0","payload":{}}
234+
{"type":"status_update","timestamp":"2026-01-19T22:46:23.832916Z","issue_id":"pb-3ee","payload":{"status":"in_progress"}}
235+
{"type":"close","timestamp":"2026-01-19T22:52:26.673547Z","issue_id":"pb-3ee","payload":{}}

cmd/pb/log.go

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -512,25 +512,93 @@ func formatPayloadValue(key, value string) string {
512512
return value
513513
}
514514

515+
// logEventTypeColor returns the ANSI color for a log event label.
516+
func logEventTypeColor(eventType string) string {
517+
// Keep event colors distinct so each log entry is easy to scan.
518+
switch strings.ToLower(eventType) {
519+
case "create":
520+
return ansiBrightBlue
521+
case "status":
522+
return ansiBrightYellow
523+
case "close":
524+
return ansiBrightGreen
525+
case "comment":
526+
return ansiBrightMagenta
527+
case "dep_add":
528+
return ansiCyan
529+
case "dep_rm":
530+
return ansiRed
531+
default:
532+
return ansiYellow
533+
}
534+
}
535+
536+
// renderLogEventType returns a colored event label when enabled.
537+
func renderLogEventType(eventType string) string {
538+
return colorize(eventType, logEventTypeColor(eventType))
539+
}
540+
541+
// renderLogLabel applies muted styling to log field labels.
542+
func renderLogLabel(label string) string {
543+
return colorize(label, ansiDim+ansiGray)
544+
}
545+
546+
// renderLogIssueID returns a colored issue ID for log output.
547+
func renderLogIssueID(issueID string) string {
548+
return colorize(issueID, ansiCyan)
549+
}
550+
551+
// renderLogDetail formats a log detail key/value with ANSI styling.
552+
func renderLogDetail(detail string) string {
553+
parts := strings.SplitN(detail, "=", 2)
554+
if len(parts) != 2 {
555+
return detail
556+
}
557+
key := parts[0]
558+
value := parts[1]
559+
return renderLogLabel(key) + "=" + renderLogDetailValue(key, value)
560+
}
561+
562+
// renderLogDetailValue returns a colored detail value when enabled.
563+
func renderLogDetailValue(key, value string) string {
564+
// Use existing list/show color rules for known detail values.
565+
switch key {
566+
case "status":
567+
return renderStatusValue(value)
568+
case "priority":
569+
priority, err := pebbles.ParsePriority(value)
570+
if err != nil {
571+
return value
572+
}
573+
return renderPriorityLabel(priority)
574+
case "type":
575+
return renderIssueType(value)
576+
case "depends_on":
577+
return renderLogIssueID(value)
578+
default:
579+
return value
580+
}
581+
}
582+
515583
// formatPrettyLog renders a multi-line log entry for humans.
516584
func formatPrettyLog(entry logEntry, line logLine) string {
517585
var output strings.Builder
518586
// Header line includes the log line number, event type, and issue id.
519-
output.WriteString(fmt.Sprintf("event %d %s %s\n", entry.Entry.Line, line.EventType, line.IssueID))
587+
output.WriteString(fmt.Sprintf("%s %d %s %s\n", renderLogLabel("event"), entry.Entry.Line, renderLogEventType(line.EventType), renderLogIssueID(line.IssueID)))
520588
// Add core metadata lines with aligned labels.
521-
output.WriteString(fmt.Sprintf("Title: %s\n", line.IssueTitle))
522-
output.WriteString(fmt.Sprintf("When: %s\n", line.EventTime))
523-
output.WriteString(fmt.Sprintf("Actor: %s (%s)\n", line.Actor, line.ActorDate))
589+
output.WriteString(fmt.Sprintf("%s %s\n", renderLogLabel("Title:"), colorize(line.IssueTitle, ansiBold)))
590+
output.WriteString(fmt.Sprintf("%s %s\n", renderLogLabel("When:"), line.EventTime))
591+
output.WriteString(fmt.Sprintf("%s %s (%s)\n", renderLogLabel("Actor:"), line.Actor, line.ActorDate))
524592
// Render payload details with indentation or an explicit none marker.
525593
details := logEventDetailLines(entry.Entry.Event)
526594
if len(details) == 0 {
527-
output.WriteString("Details: (none)")
595+
output.WriteString(fmt.Sprintf("%s %s", renderLogLabel("Details:"), renderLogLabel("(none)")))
528596
return output.String()
529597
}
530-
output.WriteString("Details:\n")
598+
output.WriteString(fmt.Sprintf("%s\n", renderLogLabel("Details:")))
531599
for index, detail := range details {
532600
output.WriteString(" ")
533-
output.WriteString(detail)
601+
output.WriteString(renderLogDetail(detail))
534602
if index < len(details)-1 {
535603
output.WriteString("\n")
536604
}

cmd/pb/log_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ func TestParseGitBlame(t *testing.T) {
182182

183183
// TestFormatPrettyLogWithDetails ensures pretty output includes details lines.
184184
func TestFormatPrettyLogWithDetails(t *testing.T) {
185+
previous := colorEnabled
186+
colorEnabled = false
187+
defer func() {
188+
colorEnabled = previous
189+
}()
190+
185191
entry := logEntry{
186192
Entry: pebbles.EventLogEntry{
187193
Line: 5,
@@ -209,6 +215,12 @@ func TestFormatPrettyLogWithDetails(t *testing.T) {
209215

210216
// TestFormatPrettyLogNoDetails ensures empty payloads show (none).
211217
func TestFormatPrettyLogNoDetails(t *testing.T) {
218+
previous := colorEnabled
219+
colorEnabled = false
220+
defer func() {
221+
colorEnabled = previous
222+
}()
223+
212224
entry := logEntry{
213225
Entry: pebbles.EventLogEntry{
214226
Line: 2,
@@ -229,6 +241,42 @@ func TestFormatPrettyLogNoDetails(t *testing.T) {
229241
}
230242
}
231243

244+
// TestFormatPrettyLogColors verifies ANSI styling is applied when enabled.
245+
func TestFormatPrettyLogColors(t *testing.T) {
246+
previous := colorEnabled
247+
colorEnabled = true
248+
defer func() {
249+
colorEnabled = previous
250+
}()
251+
252+
entry := logEntry{
253+
Entry: pebbles.EventLogEntry{
254+
Line: 7,
255+
Event: pebbles.Event{Type: pebbles.EventTypeCreate, IssueID: "pb-9", Payload: map[string]string{"type": "task", "priority": "1"}},
256+
},
257+
ParsedTime: time.Date(2026, 1, 19, 10, 0, 0, 0, time.UTC),
258+
ParsedOK: true,
259+
}
260+
line := logLine{
261+
Actor: "Josh",
262+
ActorDate: "2026-01-19",
263+
EventTime: "2026-01-19 10:00:00",
264+
EventType: "create",
265+
IssueID: "pb-9",
266+
IssueTitle: "Pretty Log",
267+
}
268+
output := formatPrettyLog(entry, line)
269+
if !strings.Contains(output, ansiBrightBlue) {
270+
t.Fatalf("expected event type color in output: %q", output)
271+
}
272+
if !strings.Contains(output, ansiCyan) {
273+
t.Fatalf("expected issue id color in output: %q", output)
274+
}
275+
if !strings.Contains(output, ansiBold) {
276+
t.Fatalf("expected title color in output: %q", output)
277+
}
278+
}
279+
232280
// TestShouldUsePager verifies pager selection logic.
233281
func TestShouldUsePager(t *testing.T) {
234282
if shouldUsePager(true, true) {

0 commit comments

Comments
 (0)