package main import ( "fmt" "sort" "strconv" "strings" "time" "github.com/charmbracelet/lipgloss" ) var ( titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#C79FD7")).Bold(true) subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#9E9E9E")) warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD75F")) dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF6B6B")).Bold(true) okStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#87D787")) lineStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#5A5A5A")) hatStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")) ) const ( colWidth = 38 iconCPU = "⚙" iconMemory = "▦" iconGPU = "▣" iconDisk = "▤" iconNetwork = "⇅" iconBattery = "▮" iconSensors = "♨" iconProcs = "▶" ) // Check if it's Christmas season (Dec 10-31) func isChristmasSeason() bool { now := time.Now() month := now.Month() day := now.Day() return month == time.December && day >= 10 && day <= 31 } // Mole body frames (legs animate) var moleBody = [][]string{ { ` /\_/\`, ` ___/ o o \`, `/___ =-= /`, `\____)-m-m)`, }, { ` /\_/\`, ` ___/ o o \`, `/___ =-= /`, `\____)mm__)`, }, { ` /\_/\`, ` ___/ · · \`, `/___ =-= /`, `\___)-m__m)`, }, { ` /\_/\`, ` ___/ o o \`, `/___ =-= /`, `\____)-mm-)`, }, } // Mole body frames with Christmas hat var moleBodyWithHat = [][]string{ { ` *`, ` /o\`, ` {/\_/\}`, ` ___/ o o \`, ` /___ =-= /`, ` \____)-m-m)`, }, { ` *`, ` /o\`, ` {/\_/\}`, ` ___/ o o \`, ` /___ =-= /`, ` \____)mm__)`, }, { ` *`, ` /o\`, ` {/\_/\}`, ` ___/ · · \`, ` /___ =-= /`, ` \___)-m__m)`, }, { ` *`, ` /o\`, ` {/\_/\}`, ` ___/ o o \`, ` /___ =-= /`, ` \____)-mm-)`, }, } // Generate frames with horizontal movement func getMoleFrame(animFrame int, termWidth int) string { var body []string var bodyIdx int isChristmas := isChristmasSeason() if isChristmas { bodyIdx = animFrame % len(moleBodyWithHat) body = moleBodyWithHat[bodyIdx] } else { bodyIdx = animFrame % len(moleBody) body = moleBody[bodyIdx] } // Calculate mole width (approximate) moleWidth := 15 // Move across terminal width maxPos := termWidth - moleWidth if maxPos < 0 { maxPos = 0 } // Move position: 0 -> maxPos -> 0 cycleLength := maxPos * 2 if cycleLength == 0 { cycleLength = 1 } pos := animFrame % cycleLength if pos > maxPos { pos = cycleLength - pos } padding := strings.Repeat(" ", pos) var lines []string if isChristmas { // Render with red hat on first 3 lines for i, line := range body { if i < 3 { lines = append(lines, padding+hatStyle.Render(line)) } else { lines = append(lines, padding+line) } } } else { for _, line := range body { lines = append(lines, padding+line) } } return strings.Join(lines, "\n") } type cardData struct { icon string title string lines []string } func renderHeader(m MetricsSnapshot, errMsg string, animFrame int, termWidth int) string { // Title title := titleStyle.Render("Mole Status") // Health Score with color and label scoreStyle := getScoreStyle(m.HealthScore) scoreText := subtleStyle.Render("Health ") + scoreStyle.Render(fmt.Sprintf("● %d", m.HealthScore)) // Hardware info infoParts := []string{} if m.Hardware.Model != "" { infoParts = append(infoParts, m.Hardware.Model) } if m.Hardware.CPUModel != "" { cpuInfo := m.Hardware.CPUModel if len(m.GPU) > 0 && m.GPU[0].CoreCount > 0 { cpuInfo += fmt.Sprintf(" (%d GPU cores)", m.GPU[0].CoreCount) } infoParts = append(infoParts, cpuInfo) } if m.Hardware.TotalRAM != "" { infoParts = append(infoParts, m.Hardware.TotalRAM) } if m.Hardware.DiskSize != "" { infoParts = append(infoParts, m.Hardware.DiskSize) } if m.Hardware.OSVersion != "" { infoParts = append(infoParts, m.Hardware.OSVersion) } headerLine := title + " " + scoreText + " " + subtleStyle.Render(strings.Join(infoParts, " · ")) // Running mole animation mole := getMoleFrame(animFrame, termWidth) if errMsg != "" { return lipgloss.JoinVertical(lipgloss.Left, headerLine, "", mole, dangerStyle.Render(errMsg), "") } return headerLine + "\n" + mole } func getScoreStyle(score int) lipgloss.Style { if score >= 90 { // Excellent - Bright Green return lipgloss.NewStyle().Foreground(lipgloss.Color("#87FF87")).Bold(true) } else if score >= 75 { // Good - Green return lipgloss.NewStyle().Foreground(lipgloss.Color("#87D787")).Bold(true) } else if score >= 60 { // Fair - Yellow return lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD75F")).Bold(true) } else if score >= 40 { // Poor - Orange return lipgloss.NewStyle().Foreground(lipgloss.Color("#FFAF5F")).Bold(true) } else { // Critical - Red return lipgloss.NewStyle().Foreground(lipgloss.Color("#FF6B6B")).Bold(true) } } func buildCards(m MetricsSnapshot, _ int) []cardData { cards := []cardData{ renderCPUCard(m.CPU), renderMemoryCard(m.Memory), renderDiskCard(m.Disks, m.DiskIO), renderBatteryCard(m.Batteries, m.Thermal), renderProcessCard(m.TopProcesses), renderNetworkCard(m.Network, m.Proxy), } // Only show sensors if we have valid temperature readings if hasSensorData(m.Sensors) { cards = append(cards, renderSensorsCard(m.Sensors)) } return cards } func hasSensorData(sensors []SensorReading) bool { for _, s := range sensors { if s.Note == "" && s.Value > 0 { return true } } return false } func renderCPUCard(cpu CPUStatus) cardData { var lines []string lines = append(lines, fmt.Sprintf("Total %s %5.1f%%", progressBar(cpu.Usage), cpu.Usage)) if cpu.PCoreCount > 0 && cpu.ECoreCount > 0 { lines = append(lines, subtleStyle.Render(fmt.Sprintf("Load %.2f / %.2f / %.2f (%dP+%dE)", cpu.Load1, cpu.Load5, cpu.Load15, cpu.PCoreCount, cpu.ECoreCount))) } else { lines = append(lines, subtleStyle.Render(fmt.Sprintf("%.2f / %.2f / %.2f (%d cores)", cpu.Load1, cpu.Load5, cpu.Load15, cpu.LogicalCPU))) } if cpu.PerCoreEstimated { lines = append(lines, subtleStyle.Render("Per-core data unavailable (using averaged load)")) } else if len(cpu.PerCore) > 0 { type coreUsage struct { idx int val float64 } var cores []coreUsage for i, v := range cpu.PerCore { cores = append(cores, coreUsage{i, v}) } sort.Slice(cores, func(i, j int) bool { return cores[i].val > cores[j].val }) maxCores := 3 if len(cores) < maxCores { maxCores = len(cores) } for i := 0; i < maxCores; i++ { c := cores[i] lines = append(lines, fmt.Sprintf("Core%-2d %s %5.1f%%", c.idx+1, progressBar(c.val), c.val)) } } return cardData{icon: iconCPU, title: "CPU", lines: lines} } func renderGPUCard(gpus []GPUStatus) cardData { var lines []string if len(gpus) == 0 { lines = append(lines, subtleStyle.Render("No GPU detected")) } else { for _, g := range gpus { if g.Usage >= 0 { lines = append(lines, fmt.Sprintf("Total %s %5.1f%%", progressBar(g.Usage), g.Usage)) } coreInfo := "" if g.CoreCount > 0 { coreInfo = fmt.Sprintf(" (%d cores)", g.CoreCount) } lines = append(lines, subtleStyle.Render(g.Name+coreInfo)) if g.Usage < 0 { lines = append(lines, subtleStyle.Render("Run with sudo for usage metrics")) } } } return cardData{icon: iconGPU, title: "GPU", lines: lines} } func renderMemoryCard(mem MemoryStatus) cardData { var lines []string lines = append(lines, fmt.Sprintf("Used %s %5.1f%%", progressBar(mem.UsedPercent), mem.UsedPercent)) lines = append(lines, subtleStyle.Render(fmt.Sprintf("%s / %s total", humanBytes(mem.Used), humanBytes(mem.Total)))) available := mem.Total - mem.Used freePercent := 100 - mem.UsedPercent lines = append(lines, fmt.Sprintf("Free %s %5.1f%%", progressBar(freePercent), freePercent)) lines = append(lines, subtleStyle.Render(fmt.Sprintf("%s available", humanBytes(available)))) if mem.SwapTotal > 0 || mem.SwapUsed > 0 { var swapPercent float64 if mem.SwapTotal > 0 { swapPercent = (float64(mem.SwapUsed) / float64(mem.SwapTotal)) * 100.0 } swapText := subtleStyle.Render(fmt.Sprintf("(%s/%s)", humanBytesCompact(mem.SwapUsed), humanBytesCompact(mem.SwapTotal))) lines = append(lines, fmt.Sprintf("Swap %s %5.1f%% %s", progressBar(swapPercent), swapPercent, swapText)) } else { lines = append(lines, fmt.Sprintf("Swap %s", subtleStyle.Render("not in use"))) } // Memory pressure if mem.Pressure != "" { pressureStyle := okStyle pressureText := "Status " + mem.Pressure if mem.Pressure == "warn" { pressureStyle = warnStyle } else if mem.Pressure == "critical" { pressureStyle = dangerStyle } lines = append(lines, pressureStyle.Render(pressureText)) } return cardData{icon: iconMemory, title: "Memory", lines: lines} } func renderDiskCard(disks []DiskStatus, io DiskIOStatus) cardData { var lines []string if len(disks) == 0 { lines = append(lines, subtleStyle.Render("Collecting...")) } else { internal, external := splitDisks(disks) addGroup := func(prefix string, list []DiskStatus) { if len(list) == 0 { return } for i, d := range list { label := diskLabel(prefix, i, len(list)) lines = append(lines, formatDiskLine(label, d)) } } addGroup("INTR", internal) addGroup("EXTR", external) if len(lines) == 0 { lines = append(lines, subtleStyle.Render("No disks detected")) } } readBar := ioBar(io.ReadRate) writeBar := ioBar(io.WriteRate) lines = append(lines, fmt.Sprintf("Read %s %.1f MB/s", readBar, io.ReadRate)) lines = append(lines, fmt.Sprintf("Write %s %.1f MB/s", writeBar, io.WriteRate)) return cardData{icon: iconDisk, title: "Disk", lines: lines} } func splitDisks(disks []DiskStatus) (internal, external []DiskStatus) { for _, d := range disks { if d.External { external = append(external, d) } else { internal = append(internal, d) } } return internal, external } func diskLabel(prefix string, index int, total int) string { if total <= 1 { return prefix } return fmt.Sprintf("%s%d", prefix, index+1) } func formatDiskLine(label string, d DiskStatus) string { if label == "" { label = "DISK" } bar := progressBar(d.UsedPercent) used := humanBytesShort(d.Used) total := humanBytesShort(d.Total) return fmt.Sprintf("%-6s %s %5.1f%% (%s/%s)", label, bar, d.UsedPercent, used, total) } func ioBar(rate float64) string { // Scale: 0-50 MB/s maps to 0-5 blocks filled := int(rate / 10.0) if filled > 5 { filled = 5 } if filled < 0 { filled = 0 } bar := strings.Repeat("▮", filled) + strings.Repeat("▯", 5-filled) if rate > 80 { return dangerStyle.Render(bar) } if rate > 30 { return warnStyle.Render(bar) } return okStyle.Render(bar) } func renderProcessCard(procs []ProcessInfo) cardData { var lines []string maxProcs := 3 for i, p := range procs { if i >= maxProcs { break } name := shorten(p.Name, 12) cpuBar := miniBar(p.CPU) lines = append(lines, fmt.Sprintf("%-12s %s %5.1f%%", name, cpuBar, p.CPU)) } if len(lines) == 0 { lines = append(lines, subtleStyle.Render("No data")) } return cardData{icon: iconProcs, title: "Processes", lines: lines} } func miniBar(percent float64) string { filled := int(percent / 20) // 5 chars max for 100% if filled > 5 { filled = 5 } if filled < 0 { filled = 0 } return colorizePercent(percent, strings.Repeat("▮", filled)+strings.Repeat("▯", 5-filled)) } func renderNetworkCard(netStats []NetworkStatus, proxy ProxyStatus) cardData { var lines []string var totalRx, totalTx float64 var primaryIP string for _, n := range netStats { totalRx += n.RxRateMBs totalTx += n.TxRateMBs if primaryIP == "" && n.IP != "" && n.Name == "en0" { primaryIP = n.IP } } if len(netStats) == 0 { lines = []string{subtleStyle.Render("Collecting...")} } else { rxBar := netBar(totalRx) txBar := netBar(totalTx) lines = append(lines, fmt.Sprintf("Down %s %s", rxBar, formatRate(totalRx))) lines = append(lines, fmt.Sprintf("Up %s %s", txBar, formatRate(totalTx))) // Show proxy and IP in one line var infoParts []string if proxy.Enabled { infoParts = append(infoParts, "Proxy "+proxy.Type) } if primaryIP != "" { infoParts = append(infoParts, primaryIP) } if len(infoParts) > 0 { lines = append(lines, subtleStyle.Render(strings.Join(infoParts, " · "))) } } return cardData{icon: iconNetwork, title: "Network", lines: lines} } func netBar(rate float64) string { // Scale: 0-10 MB/s maps to 0-5 blocks filled := int(rate / 2.0) if filled > 5 { filled = 5 } if filled < 0 { filled = 0 } bar := strings.Repeat("▮", filled) + strings.Repeat("▯", 5-filled) if rate > 8 { return dangerStyle.Render(bar) } if rate > 3 { return warnStyle.Render(bar) } return okStyle.Render(bar) } func renderBatteryCard(batts []BatteryStatus, thermal ThermalStatus) cardData { var lines []string if len(batts) == 0 { lines = append(lines, subtleStyle.Render("No battery")) } else { b := batts[0] // Line 1: label + bar + percentage (consistent with other cards) // Only show red when battery is critically low statusLower := strings.ToLower(b.Status) percentText := fmt.Sprintf("%5.1f%%", b.Percent) if b.Percent < 20 && statusLower != "charging" && statusLower != "charged" { percentText = dangerStyle.Render(percentText) } lines = append(lines, fmt.Sprintf("Level %s %s", batteryProgressBar(b.Percent), percentText)) // Line 2: status statusIcon := "" statusStyle := subtleStyle if statusLower == "charging" || statusLower == "charged" { statusIcon = " ⚡" statusStyle = okStyle } else if b.Percent < 20 { statusStyle = dangerStyle } // Capitalize first letter statusText := b.Status if len(statusText) > 0 { statusText = strings.ToUpper(statusText[:1]) + strings.ToLower(statusText[1:]) } if b.TimeLeft != "" { statusText += " · " + b.TimeLeft } lines = append(lines, statusStyle.Render(statusText+statusIcon)) // Line 3: Health + cycles + temp healthParts := []string{} if b.Health != "" { healthParts = append(healthParts, b.Health) } if b.CycleCount > 0 { healthParts = append(healthParts, fmt.Sprintf("%d cycles", b.CycleCount)) } // Add temperature if available if thermal.CPUTemp > 0 { tempStyle := subtleStyle if thermal.CPUTemp > 80 { tempStyle = dangerStyle } else if thermal.CPUTemp > 60 { tempStyle = warnStyle } healthParts = append(healthParts, tempStyle.Render(fmt.Sprintf("%.0f°C", thermal.CPUTemp))) } // Add fan speed if available if thermal.FanSpeed > 0 { healthParts = append(healthParts, fmt.Sprintf("%d RPM", thermal.FanSpeed)) } if len(healthParts) > 0 { lines = append(lines, subtleStyle.Render(strings.Join(healthParts, " · "))) } } return cardData{icon: iconBattery, title: "Power", lines: lines} } func renderSensorsCard(sensors []SensorReading) cardData { var lines []string for _, s := range sensors { if s.Note != "" { continue } lines = append(lines, fmt.Sprintf("%-12s %s", shorten(s.Label, 12), colorizeTemp(s.Value)+s.Unit)) } if len(lines) == 0 { lines = append(lines, subtleStyle.Render("No sensors")) } return cardData{icon: iconSensors, title: "Sensors", lines: lines} } func renderCard(data cardData, width int, height int) string { titleText := data.icon + " " + data.title lineLen := width - lipgloss.Width(titleText) - 1 if lineLen < 4 { lineLen = 4 } header := titleStyle.Render(titleText) + " " + lineStyle.Render(strings.Repeat("─", lineLen)) content := header + "\n" + strings.Join(data.lines, "\n") // Pad to target height lines := strings.Split(content, "\n") for len(lines) < height { lines = append(lines, "") } return strings.Join(lines, "\n") } func progressBar(percent float64) string { total := 18 if percent < 0 { percent = 0 } if percent > 100 { percent = 100 } filled := int(percent / 100 * float64(total)) if filled > total { filled = total } var builder strings.Builder for i := 0; i < total; i++ { if i < filled { builder.WriteString("█") } else { builder.WriteString("░") } } return colorizePercent(percent, builder.String()) } func batteryProgressBar(percent float64) string { total := 18 if percent < 0 { percent = 0 } if percent > 100 { percent = 100 } filled := int(percent / 100 * float64(total)) if filled > total { filled = total } var builder strings.Builder for i := 0; i < total; i++ { if i < filled { builder.WriteString("█") } else { builder.WriteString("░") } } return colorizeBattery(percent, builder.String()) } func colorizePercent(percent float64, s string) string { switch { case percent >= 90: return dangerStyle.Render(s) case percent >= 70: return warnStyle.Render(s) default: return okStyle.Render(s) } } func colorizeBattery(percent float64, s string) string { switch { case percent < 20: return dangerStyle.Render(s) case percent < 50: return warnStyle.Render(s) default: return okStyle.Render(s) } } func colorizeTemp(t float64) string { switch { case t >= 85: return dangerStyle.Render(fmt.Sprintf("%.1f", t)) case t >= 70: return warnStyle.Render(fmt.Sprintf("%.1f", t)) default: return subtleStyle.Render(fmt.Sprintf("%.1f", t)) } } func formatRate(mb float64) string { if mb < 0.01 { return "0 MB/s" } if mb < 1 { return fmt.Sprintf("%.2f MB/s", mb) } if mb < 10 { return fmt.Sprintf("%.1f MB/s", mb) } return fmt.Sprintf("%.0f MB/s", mb) } func humanBytes(v uint64) string { switch { case v > 1<<40: return fmt.Sprintf("%.1f TB", float64(v)/(1<<40)) case v > 1<<30: return fmt.Sprintf("%.1f GB", float64(v)/(1<<30)) case v > 1<<20: return fmt.Sprintf("%.1f MB", float64(v)/(1<<20)) case v > 1<<10: return fmt.Sprintf("%.1f KB", float64(v)/(1<<10)) default: return strconv.FormatUint(v, 10) + " B" } } func humanBytesShort(v uint64) string { switch { case v >= 1<<40: return fmt.Sprintf("%.0fT", float64(v)/(1<<40)) case v >= 1<<30: return fmt.Sprintf("%.0fG", float64(v)/(1<<30)) case v >= 1<<20: return fmt.Sprintf("%.0fM", float64(v)/(1<<20)) case v >= 1<<10: return fmt.Sprintf("%.0fK", float64(v)/(1<<10)) default: return strconv.FormatUint(v, 10) } } func humanBytesCompact(v uint64) string { switch { case v >= 1<<40: return fmt.Sprintf("%.1fT", float64(v)/(1<<40)) case v >= 1<<30: return fmt.Sprintf("%.1fG", float64(v)/(1<<30)) case v >= 1<<20: return fmt.Sprintf("%.1fM", float64(v)/(1<<20)) case v >= 1<<10: return fmt.Sprintf("%.1fK", float64(v)/(1<<10)) default: return strconv.FormatUint(v, 10) } } func shorten(s string, max int) string { if len(s) <= max { return s } return s[:max-1] + "…" } func renderTwoColumns(cards []cardData, width int) string { if len(cards) == 0 { return "" } cw := colWidth if width > 0 && width/2-2 > cw { cw = width/2 - 2 } var rows []string for i := 0; i < len(cards); i += 2 { left := renderCard(cards[i], cw, 0) right := "" if i+1 < len(cards) { right = renderCard(cards[i+1], cw, 0) } targetHeight := maxInt(lipgloss.Height(left), lipgloss.Height(right)) left = renderCard(cards[i], cw, targetHeight) if right != "" { right = renderCard(cards[i+1], cw, targetHeight) rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, left, " ", right)) } else { rows = append(rows, left) } } // Add empty lines between rows for separation var spacedRows []string for i, r := range rows { if i > 0 { spacedRows = append(spacedRows, "") } spacedRows = append(spacedRows, r) } return lipgloss.JoinVertical(lipgloss.Left, spacedRows...) } func maxInt(a, b int) int { if a > b { return a } return b }