Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
5.3 kB · 198 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199import chalk from 'chalk'import type { DailyActivity } from './stats.js'import { toDateString } from './statsCache.js'
export type HeatmapOptions = { terminalWidth?: number // Terminal width in characters showMonthLabels?: boolean}
type Percentiles = { p25: number p50: number p75: number}
/** * Pre-calculates percentiles from activity data for use in intensity calculations */function calculatePercentiles( dailyActivity: DailyActivity[],): Percentiles | null { const counts = dailyActivity .map(a => a.messageCount) .filter(c => c > 0) .sort((a, b) => a - b)
if (counts.length === 0) return null
return { p25: counts[Math.floor(counts.length * 0.25)]!, p50: counts[Math.floor(counts.length * 0.5)]!, p75: counts[Math.floor(counts.length * 0.75)]!, }}
/** * Generates a GitHub-style activity heatmap for the terminal */export function generateHeatmap( dailyActivity: DailyActivity[], options: HeatmapOptions = {},): string { const { terminalWidth = 80, showMonthLabels = true } = options
// Day labels take 4 characters ("Mon "), calculate weeks that fit // Cap at 52 weeks (1 year) to match GitHub style const dayLabelWidth = 4 const availableWidth = terminalWidth - dayLabelWidth const width = Math.min(52, Math.max(10, availableWidth))
// Build activity map by date const activityMap = new Map<string, DailyActivity>() for (const activity of dailyActivity) { activityMap.set(activity.date, activity) }
// Pre-calculate percentiles once for all intensity lookups const percentiles = calculatePercentiles(dailyActivity)
// Calculate date range - end at today, go back N weeks const today = new Date() today.setHours(0, 0, 0, 0)
// Find the Sunday of the current week (start of the week containing today) const currentWeekStart = new Date(today) currentWeekStart.setDate(today.getDate() - today.getDay())
// Go back (width - 1) weeks from the current week start const startDate = new Date(currentWeekStart) startDate.setDate(startDate.getDate() - (width - 1) * 7)
// Generate grid (7 rows for days of week, width columns for weeks) // Also track which week each month starts for labels const grid: string[][] = Array.from({ length: 7 }, () => Array(width).fill(''), ) const monthStarts: { month: number; week: number }[] = [] let lastMonth = -1
const currentDate = new Date(startDate) for (let week = 0; week < width; week++) { for (let day = 0; day < 7; day++) { // Don't show future dates if (currentDate > today) { grid[day]![week] = ' ' currentDate.setDate(currentDate.getDate() + 1) continue }
const dateStr = toDateString(currentDate) const activity = activityMap.get(dateStr)
// Track month changes (on day 0 = Sunday of each week) if (day === 0) { const month = currentDate.getMonth() if (month !== lastMonth) { monthStarts.push({ month, week }) lastMonth = month } }
// Determine intensity level based on message count const intensity = getIntensity(activity?.messageCount || 0, percentiles) grid[day]![week] = getHeatmapChar(intensity)
currentDate.setDate(currentDate.getDate() + 1) } }
// Build output const lines: string[] = []
// Month labels - evenly spaced across the grid if (showMonthLabels) { const monthNames = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]
// Build label line with fixed-width month labels const uniqueMonths = monthStarts.map(m => m.month) const labelWidth = Math.floor(width / Math.max(uniqueMonths.length, 1)) const monthLabels = uniqueMonths .map(month => monthNames[month]!.padEnd(labelWidth)) .join('')
// 4 spaces for day label column prefix lines.push(' ' + monthLabels) }
// Day labels const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
// Grid for (let day = 0; day < 7; day++) { // Only show labels for Mon, Wed, Fri const label = [1, 3, 5].includes(day) ? dayLabels[day]!.padEnd(3) : ' ' const row = label + ' ' + grid[day]!.join('') lines.push(row) }
// Legend lines.push('') lines.push( ' Less ' + [ claudeOrange('░'), claudeOrange('▒'), claudeOrange('▓'), claudeOrange('█'), ].join(' ') + ' More', )
return lines.join('\n')}
function getIntensity( messageCount: number, percentiles: Percentiles | null,): number { if (messageCount === 0 || !percentiles) return 0
if (messageCount >= percentiles.p75) return 4 if (messageCount >= percentiles.p50) return 3 if (messageCount >= percentiles.p25) return 2 return 1}
// Claude orange color (hex #da7756)const claudeOrange = chalk.hex('#da7756')
function getHeatmapChar(intensity: number): string { switch (intensity) { case 0: return chalk.gray('·') case 1: return claudeOrange('░') case 2: return claudeOrange('▒') case 3: return claudeOrange('▓') case 4: return claudeOrange('█') default: return chalk.gray('·') }}