Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions pkg/cli/compile_schedule_calendar.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"strconv"
"strings"

lipgloss "charm.land/lipgloss/v2"
"github.com/github/gh-aw/pkg/colorwriter"
"github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/styles"
"github.com/github/gh-aw/pkg/tty"
Expand Down Expand Up @@ -195,14 +195,17 @@ func intensityChar(count int) string {
}
}

// intensityStyle returns a lipgloss style appropriate for the given trigger
// count. Styling is TTY-gated so non-interactive output does not emit ANSI.
func intensityStyle(count int, isTerminal bool) lipgloss.Style {
if !isTerminal {
// Keep glyph rendering unchanged while preventing ANSI escapes in piped output.
return lipgloss.NewStyle()
}
// scheduleCalendarRenderer abstracts the shared Render method implemented by
// both lipgloss.Style (normal builds) and styles.WasmStyle (js/wasm builds).
// Using this interface keeps intensityStyle free of a concrete lipgloss.Style
// return type that would fail to compile for wasm style tokens.
type scheduleCalendarRenderer interface {
Render(...string) string
}

// intensityStyle returns a centralized style appropriate for the given trigger
// count.
func intensityStyle(count int) scheduleCalendarRenderer {
switch {
case count == 0:
return styles.ScheduleCalendarEmpty
Expand All @@ -217,6 +220,13 @@ func intensityStyle(count int, isTerminal bool) lipgloss.Style {
}
}

func renderScheduleCalendarCell(count int, text string, isTerminal bool, environ []string) string {
Comment thread
github-actions[bot] marked this conversation as resolved.
if !isTerminal || console.IsAccessibleMode() {
return text
}
return colorwriter.Degrade(intensityStyle(count).Render(text), environ)
}

// displayScheduleCalendar renders a text heatmap of scheduled workflow times to
// stderr. The grid shows days of the week (Mon–Sun) against hours of the day
// (00–23, UTC). Each cell intensity indicates how many workflows fire at that
Expand All @@ -231,6 +241,7 @@ func displayScheduleCalendar(statsList []*WorkflowStats) {
}

isTerminal := tty.IsStderrTerminal()
environ := os.Environ()

// Title
fmt.Fprintln(os.Stderr)
Expand Down Expand Up @@ -263,7 +274,7 @@ func displayScheduleCalendar(statsList []*WorkflowStats) {
ch := intensityChar(count)
// Pad each cell to cellWidth with a trailing space.
cell := ch + strings.Repeat(" ", cellWidth-len([]rune(ch)))
row.WriteString(intensityStyle(count, isTerminal).Render(cell))
row.WriteString(renderScheduleCalendarCell(count, cell, isTerminal, environ))
}

fmt.Fprintln(os.Stderr, row.String())
Expand All @@ -281,7 +292,7 @@ func displayScheduleCalendar(statsList []*WorkflowStats) {
legend.WriteString("Legend: ")
for _, e := range entries {
ch := intensityChar(e.count)
legend.WriteString(intensityStyle(e.count, isTerminal).Render(ch))
legend.WriteString(renderScheduleCalendarCell(e.count, ch, isTerminal, environ))
legend.WriteString(" = " + e.label + " ")
}
fmt.Fprintln(os.Stderr, legend.String())
Expand Down
34 changes: 31 additions & 3 deletions pkg/cli/compile_schedule_calendar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"strings"
"testing"

lipgloss "charm.land/lipgloss/v2"
"github.com/github/gh-aw/pkg/parser"
"github.com/github/gh-aw/pkg/styles"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -205,13 +207,39 @@ func TestIntensityChar(t *testing.T) {
}
}

func TestIntensityStyle_NoANSIWhenNotTerminal(t *testing.T) {
func TestRenderScheduleCalendarCell_NoANSIWhenNotTerminal(t *testing.T) {
for _, count := range []int{0, 1, 2, 5, 8} {
got := intensityStyle(count, false).Render(intensityChar(count))
assert.NotContains(t, got, "\x1b[", "non-TTY output should not contain ANSI escapes")
text := intensityChar(count)
got := renderScheduleCalendarCell(count, text, false, []string{"TERM=xterm-256color"})
assert.Equal(t, text, got, "non-TTY output should return plain text")
Comment thread
github-actions[bot] marked this conversation as resolved.
}
}

func TestRenderScheduleCalendarCell_NoANSIWhenNoColor(t *testing.T) {
for _, count := range []int{0, 1, 2, 5, 8} {
text := intensityChar(count)
got := renderScheduleCalendarCell(count, text, true, []string{"NO_COLOR=1", "TERM=xterm-256color"})
assert.Equal(t, text, got, "NO_COLOR output should return plain text")
}
}

func TestRenderScheduleCalendarCell_UsesANSIInColorTerminal(t *testing.T) {
// This test intentionally avoids t.Parallel because it temporarily overrides
// a shared style token; running in parallel could race with other tests that
// read styles.ScheduleCalendarCritical.
prevCritical := styles.ScheduleCalendarCritical
styles.ScheduleCalendarCritical = lipgloss.NewStyle().Transform(func(s string) string {
return "\x1b[31m" + s + "\x1b[0m"
})
t.Cleanup(func() {
styles.ScheduleCalendarCritical = prevCritical
})

got := renderScheduleCalendarCell(8, intensityChar(8), true, []string{"CLICOLOR_FORCE=1", "TERM=xterm-256color"})
assert.Contains(t, got, "\x1b[", "TTY with color support should contain ANSI escapes")
assert.Contains(t, got, "\x1b[31m", "TTY path should apply the mocked critical style")
}

// ---------------------------------------------------------------------------
// displayScheduleCalendar (integration-style: captures stderr)
// ---------------------------------------------------------------------------
Expand Down
Loading