405 lines
8.3 KiB
Go
405 lines
8.3 KiB
Go
//nolint:mnd
|
|
package renderer
|
|
|
|
import (
|
|
"fmt"
|
|
"image/color"
|
|
"strings"
|
|
|
|
"git.eeqj.de/sneak/hdmistat/internal/layout"
|
|
"git.eeqj.de/sneak/hdmistat/internal/statcollector"
|
|
"github.com/dustin/go-humanize"
|
|
)
|
|
|
|
// StatusScreen displays system status overview
|
|
type StatusScreen struct{}
|
|
|
|
// NewStatusScreen creates a new status screen
|
|
func NewStatusScreen() *StatusScreen {
|
|
return &StatusScreen{}
|
|
}
|
|
|
|
// Name returns the screen name
|
|
func (s *StatusScreen) Name() string {
|
|
return "System Status"
|
|
}
|
|
|
|
// Render renders the status screen
|
|
func (s *StatusScreen) Render(
|
|
canvas *layout.Canvas,
|
|
info *statcollector.SystemInfo,
|
|
) error {
|
|
// Use consistent font size for entire screen
|
|
const fontSize = 16
|
|
|
|
// Colors
|
|
textColor := color.RGBA{255, 255, 255, 255}
|
|
dimColor := color.RGBA{150, 150, 150, 255}
|
|
|
|
// Styles
|
|
normalStyle := layout.TextStyle{Size: fontSize, Color: textColor}
|
|
dimStyle := layout.TextStyle{Size: fontSize, Color: dimColor}
|
|
|
|
// Get short hostname
|
|
shortHostname := info.Hostname
|
|
if idx := strings.Index(shortHostname, "."); idx > 0 {
|
|
shortHostname = shortHostname[:idx]
|
|
}
|
|
|
|
// Starting Y position (after header)
|
|
y := 150
|
|
|
|
// Title
|
|
titleText := fmt.Sprintf("%s: system status", shortHostname)
|
|
_ = canvas.DrawText(titleText, layout.Point{X: 16, Y: y}, normalStyle)
|
|
y += 40
|
|
|
|
// CPU section
|
|
cpuLabel := fmt.Sprintf(
|
|
"CPU: %.1f%% average across %d cores",
|
|
getAverageCPU(info.CPUPercent),
|
|
len(info.CPUPercent),
|
|
)
|
|
_ = canvas.DrawText(cpuLabel, layout.Point{X: 16, Y: y}, normalStyle)
|
|
y += 25
|
|
|
|
// CPU progress bar
|
|
_ = canvas.DrawText("0%", layout.Point{X: 100, Y: y}, dimStyle)
|
|
drawProgressBar(
|
|
canvas,
|
|
130,
|
|
y-10,
|
|
getAverageCPU(info.CPUPercent)/100.0,
|
|
textColor,
|
|
)
|
|
_ = canvas.DrawText("100%", layout.Point{X: 985, Y: y}, dimStyle)
|
|
y += 40
|
|
|
|
// Memory section
|
|
memUsedPercent := float64(
|
|
info.MemoryUsed,
|
|
) / float64(
|
|
info.MemoryTotal,
|
|
) * 100.0
|
|
memLabel := fmt.Sprintf(
|
|
"MEMORY: %s of %s (%.1f%%)",
|
|
layout.FormatBytes(info.MemoryUsed),
|
|
layout.FormatBytes(info.MemoryTotal),
|
|
memUsedPercent,
|
|
)
|
|
_ = canvas.DrawText(memLabel, layout.Point{X: 16, Y: y}, normalStyle)
|
|
y += 25
|
|
|
|
// Memory progress bar
|
|
_ = canvas.DrawText("0B", layout.Point{X: 100, Y: y}, dimStyle)
|
|
drawProgressBar(
|
|
canvas,
|
|
130,
|
|
y-10,
|
|
float64(info.MemoryUsed)/float64(info.MemoryTotal),
|
|
textColor,
|
|
)
|
|
_ = canvas.DrawText(
|
|
layout.FormatBytes(info.MemoryTotal),
|
|
layout.Point{X: 985, Y: y},
|
|
dimStyle,
|
|
)
|
|
y += 40
|
|
|
|
// Temperature section
|
|
if len(info.Temperature) > 0 {
|
|
maxTemp, maxSensor := getMaxTemperature(info.Temperature)
|
|
tempLabel := fmt.Sprintf(
|
|
"TEMPERATURE: %.1f°C (%s)",
|
|
maxTemp,
|
|
maxSensor,
|
|
)
|
|
_ = canvas.DrawText(
|
|
tempLabel,
|
|
layout.Point{X: 16, Y: y},
|
|
normalStyle,
|
|
)
|
|
y += 25
|
|
|
|
// Temperature progress bar (30-99°C scale)
|
|
_ = canvas.DrawText("30°C", layout.Point{X: 90, Y: y}, dimStyle)
|
|
tempValue := (maxTemp - 30.0) / (99.0 - 30.0)
|
|
if tempValue < 0 {
|
|
tempValue = 0
|
|
}
|
|
if tempValue > 1 {
|
|
tempValue = 1
|
|
}
|
|
drawProgressBar(canvas, 130, y-10, tempValue, textColor)
|
|
_ = canvas.DrawText("99°C", layout.Point{X: 985, Y: y}, dimStyle)
|
|
y += 40
|
|
}
|
|
|
|
// Disk usage section
|
|
_ = canvas.DrawText(
|
|
"DISK USAGE:",
|
|
layout.Point{X: 16, Y: y},
|
|
normalStyle,
|
|
)
|
|
y += 25
|
|
|
|
for _, disk := range info.DiskUsage {
|
|
// Skip snap disks
|
|
if strings.HasPrefix(disk.Path, "/snap") {
|
|
continue
|
|
}
|
|
|
|
diskLabel := fmt.Sprintf(" * %-12s %s of %s (%.1f%%)",
|
|
disk.Path,
|
|
layout.FormatBytes(disk.Used),
|
|
layout.FormatBytes(disk.Total),
|
|
disk.UsedPercent)
|
|
_ = canvas.DrawText(
|
|
diskLabel,
|
|
layout.Point{X: 16, Y: y},
|
|
normalStyle,
|
|
)
|
|
|
|
// Disk progress bar
|
|
_ = canvas.DrawText("0B", layout.Point{X: 470, Y: y}, dimStyle)
|
|
drawDiskProgressBar(
|
|
canvas,
|
|
500,
|
|
y-10,
|
|
disk.UsedPercent/100.0,
|
|
textColor,
|
|
)
|
|
_ = canvas.DrawText(
|
|
layout.FormatBytes(disk.Total),
|
|
layout.Point{X: 985, Y: y},
|
|
dimStyle,
|
|
)
|
|
y += 30
|
|
|
|
if y > 700 {
|
|
break // Don't overflow
|
|
}
|
|
}
|
|
|
|
// Network section
|
|
if len(info.Network) > 0 {
|
|
y += 15
|
|
_ = canvas.DrawText(
|
|
"NETWORK:",
|
|
layout.Point{X: 16, Y: y},
|
|
normalStyle,
|
|
)
|
|
y += 25
|
|
|
|
for _, net := range info.Network {
|
|
// Interface header
|
|
interfaceText := fmt.Sprintf(" * %s", net.Name)
|
|
if len(net.IPAddresses) > 0 {
|
|
interfaceText = fmt.Sprintf(
|
|
" * %s (%s):",
|
|
net.Name,
|
|
net.IPAddresses[0],
|
|
)
|
|
}
|
|
_ = canvas.DrawText(
|
|
interfaceText,
|
|
layout.Point{X: 16, Y: y},
|
|
normalStyle,
|
|
)
|
|
y += 25
|
|
|
|
// Get link speed for scaling (default to 1 Gbps if unknown)
|
|
linkSpeed := net.LinkSpeed
|
|
linkSpeedText := ""
|
|
if linkSpeed == 0 {
|
|
linkSpeed = 1000 * 1000 * 1000 // 1 Gbps in bits
|
|
linkSpeedText = "1G link"
|
|
} else {
|
|
linkSpeedText = fmt.Sprintf("%s link", humanize.SI(float64(linkSpeed), "bit/s"))
|
|
}
|
|
|
|
// Upload rate
|
|
upLabel := fmt.Sprintf(
|
|
" ↑ %7s (%s)",
|
|
net.FormatSentRate(),
|
|
linkSpeedText,
|
|
)
|
|
_ = canvas.DrawText(
|
|
upLabel,
|
|
layout.Point{X: 16, Y: y},
|
|
normalStyle,
|
|
)
|
|
_ = canvas.DrawText(
|
|
"0 bit/s",
|
|
layout.Point{X: 400, Y: y},
|
|
dimStyle,
|
|
)
|
|
drawNetworkProgressBar(
|
|
canvas,
|
|
500,
|
|
y-10,
|
|
float64(net.BitsSentRate)/float64(linkSpeed),
|
|
textColor,
|
|
)
|
|
_ = canvas.DrawText(
|
|
humanize.SI(float64(linkSpeed), "bit/s"),
|
|
layout.Point{X: 960, Y: y},
|
|
dimStyle,
|
|
)
|
|
y += 25
|
|
|
|
// Download rate
|
|
downLabel := fmt.Sprintf(" ↓ %7s", net.FormatRecvRate())
|
|
_ = canvas.DrawText(
|
|
downLabel,
|
|
layout.Point{X: 16, Y: y},
|
|
normalStyle,
|
|
)
|
|
_ = canvas.DrawText(
|
|
"0 bit/s",
|
|
layout.Point{X: 400, Y: y},
|
|
dimStyle,
|
|
)
|
|
drawNetworkProgressBar(
|
|
canvas,
|
|
500,
|
|
y-10,
|
|
float64(net.BitsRecvRate)/float64(linkSpeed),
|
|
textColor,
|
|
)
|
|
_ = canvas.DrawText(
|
|
humanize.SI(float64(linkSpeed), "bit/s"),
|
|
layout.Point{X: 960, Y: y},
|
|
dimStyle,
|
|
)
|
|
y += 35
|
|
|
|
if y > 900 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// drawProgressBar draws a progress bar matching the mockup style
|
|
func drawProgressBar(
|
|
canvas *layout.Canvas,
|
|
x, y int,
|
|
value float64,
|
|
color color.Color,
|
|
) {
|
|
const barWidth = 850
|
|
|
|
// Draw opening bracket
|
|
_ = canvas.DrawText(
|
|
"[",
|
|
layout.Point{X: x, Y: y + 15},
|
|
layout.TextStyle{Size: 16, Color: color},
|
|
)
|
|
|
|
// Calculate fill
|
|
fillChars := int(value * 80)
|
|
emptyChars := 80 - fillChars
|
|
|
|
// Draw bar content
|
|
barContent := strings.Repeat(
|
|
"█",
|
|
fillChars,
|
|
) + strings.Repeat(
|
|
"▒",
|
|
emptyChars,
|
|
)
|
|
_ = canvas.DrawText(
|
|
barContent,
|
|
layout.Point{X: x + 10, Y: y + 15},
|
|
layout.TextStyle{Size: 16, Color: color},
|
|
)
|
|
|
|
// Draw closing bracket
|
|
_ = canvas.DrawText(
|
|
"]",
|
|
layout.Point{X: x + barWidth - 10, Y: y + 15},
|
|
layout.TextStyle{Size: 16, Color: color},
|
|
)
|
|
}
|
|
|
|
// drawDiskProgressBar draws a smaller progress bar for disk usage
|
|
func drawDiskProgressBar(
|
|
canvas *layout.Canvas,
|
|
x, y int,
|
|
value float64,
|
|
color color.Color,
|
|
) {
|
|
const barWidth = 480
|
|
|
|
// Draw opening bracket
|
|
_ = canvas.DrawText(
|
|
"[",
|
|
layout.Point{X: x, Y: y + 15},
|
|
layout.TextStyle{Size: 16, Color: color},
|
|
)
|
|
|
|
// Calculate fill (50 chars total)
|
|
fillChars := int(value * 50)
|
|
emptyChars := 50 - fillChars
|
|
|
|
// Draw bar content
|
|
barContent := strings.Repeat(
|
|
"█",
|
|
fillChars,
|
|
) + strings.Repeat(
|
|
"▒",
|
|
emptyChars,
|
|
)
|
|
_ = canvas.DrawText(
|
|
barContent,
|
|
layout.Point{X: x + 10, Y: y + 15},
|
|
layout.TextStyle{Size: 16, Color: color},
|
|
)
|
|
|
|
// Draw closing bracket
|
|
_ = canvas.DrawText(
|
|
"]",
|
|
layout.Point{X: x + barWidth - 10, Y: y + 15},
|
|
layout.TextStyle{Size: 16, Color: color},
|
|
)
|
|
}
|
|
|
|
// drawNetworkProgressBar draws a progress bar for network rates
|
|
func drawNetworkProgressBar(
|
|
canvas *layout.Canvas,
|
|
x, y int,
|
|
value float64,
|
|
color color.Color,
|
|
) {
|
|
// Same as disk progress bar
|
|
drawDiskProgressBar(canvas, x, y, value, color)
|
|
}
|
|
|
|
// getAverageCPU calculates average CPU usage across all cores
|
|
func getAverageCPU(cpuPercents []float64) float64 {
|
|
if len(cpuPercents) == 0 {
|
|
return 0
|
|
}
|
|
total := 0.0
|
|
for _, cpu := range cpuPercents {
|
|
total += cpu
|
|
}
|
|
return total / float64(len(cpuPercents))
|
|
}
|
|
|
|
// getMaxTemperature finds the highest temperature and its sensor name
|
|
func getMaxTemperature(temps map[string]float64) (float64, string) {
|
|
maxTemp := 0.0
|
|
maxSensor := ""
|
|
for sensor, temp := range temps {
|
|
if temp > maxTemp {
|
|
maxTemp = temp
|
|
maxSensor = sensor
|
|
}
|
|
}
|
|
return maxTemp, maxSensor
|
|
}
|