hdmistat/internal/renderer/overview_screen.go
2025-07-24 16:09:00 +02:00

263 lines
5.9 KiB
Go

// Package renderer provides screen rendering implementations for hdmistat
//
//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"
)
// OverviewScreen displays system overview
type OverviewScreen struct{}
// NewOverviewScreen creates a new overview screen renderer
func NewOverviewScreen() *OverviewScreen {
return &OverviewScreen{}
}
// Name returns the name of this screen
func (s *OverviewScreen) Name() string {
return "System Overview"
}
// Render draws the overview screen to the provided canvas
func (s *OverviewScreen) Render(
canvas *layout.Canvas,
info *statcollector.SystemInfo,
) error {
_, _ = canvas.Size()
// Colors
textColor := color.RGBA{255, 255, 255, 255}
headerColor := color.RGBA{100, 200, 255, 255}
// Styles
titleStyle := layout.TextStyle{Size: 48, Color: headerColor}
headerStyle := layout.TextStyle{Size: 18, Color: headerColor, Bold: true}
normalStyle := layout.TextStyle{Size: 18, Color: textColor}
y := 120 // Start below header
// Get short hostname
shortHostname := info.Hostname
if idx := strings.Index(shortHostname, "."); idx > 0 {
shortHostname = shortHostname[:idx]
}
// Title - left aligned at consistent position
titleText := fmt.Sprintf("%s: status", shortHostname)
_ = canvas.DrawText(
titleText,
layout.Point{X: 50, Y: y},
layout.TextStyle{
Size: 36, // Smaller than before
Color: titleStyle.Color,
Alignment: layout.AlignLeft,
},
)
y += 60
// Standard bar dimensions
barWidth := 400
barHeight := 20
sectionSpacing := 60
// CPU section
_ = canvas.DrawText("CPU", layout.Point{X: 50, Y: y}, headerStyle)
y += 30
// Calculate average CPU usage
totalCPU := 0.0
for _, cpu := range info.CPUPercent {
totalCPU += cpu
}
avgCPU := totalCPU / float64(len(info.CPUPercent))
// Draw composite CPU bar below header
cpuBar := &layout.ProgressBar{
X: 50, Y: y,
Width: barWidth, Height: barHeight,
Value: avgCPU / 100.0,
Label: fmt.Sprintf(
"%.1f%% average across %d cores",
avgCPU,
len(info.CPUPercent),
),
LeftLabel: "0%",
RightLabel: "100%",
BarColor: color.RGBA{255, 100, 100, 255},
}
cpuBar.Draw(canvas)
y += sectionSpacing
// Memory section
_ = canvas.DrawText("MEMORY", layout.Point{X: 50, Y: y}, headerStyle)
y += 30
memUsedPercent := float64(info.MemoryUsed) / float64(info.MemoryTotal)
memoryBar := &layout.ProgressBar{
X: 50, Y: y,
Width: barWidth, Height: barHeight,
Value: memUsedPercent,
Label: fmt.Sprintf(
"%s of %s",
layout.FormatBytes(info.MemoryUsed),
layout.FormatBytes(info.MemoryTotal),
),
LeftLabel: "0B",
RightLabel: layout.FormatBytes(info.MemoryTotal),
BarColor: color.RGBA{100, 200, 100, 255},
}
memoryBar.Draw(canvas)
y += sectionSpacing
// Temperature section
if len(info.Temperature) > 0 {
_ = canvas.DrawText(
"TEMPERATURE",
layout.Point{X: 50, Y: y},
headerStyle,
)
y += 30
// Find the highest temperature
maxTemp := 0.0
maxSensor := ""
for sensor, temp := range info.Temperature {
if temp > maxTemp {
maxTemp = temp
maxSensor = sensor
}
}
// Temperature scale from 30°C to 99°C
tempValue := (maxTemp - 30.0) / (99.0 - 30.0)
if tempValue < 0 {
tempValue = 0
}
if tempValue > 1 {
tempValue = 1
}
tempBar := &layout.ProgressBar{
X: 50, Y: y,
Width: barWidth, Height: barHeight,
Value: tempValue,
Label: fmt.Sprintf("%s: %.1f°C", maxSensor, maxTemp),
LeftLabel: "30°C",
RightLabel: "99°C",
BarColor: color.RGBA{255, 150, 50, 255},
}
tempBar.Draw(canvas)
y += sectionSpacing
}
// Disk usage section
_ = canvas.DrawText("DISK USAGE", layout.Point{X: 50, Y: y}, headerStyle)
y += 30
for _, disk := range info.DiskUsage {
// Skip snap disks
if strings.HasPrefix(disk.Path, "/snap") {
continue
}
diskBar := &layout.ProgressBar{
X: 50, Y: y,
Width: barWidth, Height: barHeight,
Value: disk.UsedPercent / 100.0,
Label: fmt.Sprintf(
"%s: %s of %s",
disk.Path,
layout.FormatBytes(disk.Used),
layout.FormatBytes(disk.Total),
),
LeftLabel: "0B",
RightLabel: layout.FormatBytes(disk.Total),
BarColor: color.RGBA{200, 200, 100, 255},
}
diskBar.Draw(canvas)
y += 40
if y > 700 {
break // Don't overflow the screen
}
}
y += sectionSpacing
// Network section
if len(info.Network) > 0 {
_ = canvas.DrawText(
"NETWORK",
layout.Point{X: 50, Y: y},
headerStyle,
)
y += 30
for _, net := range info.Network {
// Network interface info
interfaceText := net.Name
if len(net.IPAddresses) > 0 {
interfaceText = fmt.Sprintf(
"%s (%s)",
net.Name,
net.IPAddresses[0],
)
}
_ = canvas.DrawText(
interfaceText,
layout.Point{X: 50, Y: y},
normalStyle,
)
y += 25
// Get link speed for scaling (default to 1 Gbps if unknown)
linkSpeed := net.LinkSpeed
if linkSpeed == 0 {
linkSpeed = 1000 * 1000 * 1000 // 1 Gbps in bits
}
// TX rate bar
txValue := float64(net.BitsSentRate) / float64(linkSpeed)
txBar := &layout.ProgressBar{
X: 50, Y: y,
Width: barWidth/2 - 10, Height: barHeight,
Value: txValue,
Label: fmt.Sprintf("↑ %s", net.FormatSentRate()),
LeftLabel: "0",
RightLabel: humanize.SI(float64(linkSpeed), "bit/s"),
BarColor: color.RGBA{100, 255, 100, 255},
}
txBar.Draw(canvas)
// RX rate bar
rxValue := float64(net.BitsRecvRate) / float64(linkSpeed)
rxBar := &layout.ProgressBar{
X: 50 + barWidth/2 + 10, Y: y,
Width: barWidth/2 - 10, Height: barHeight,
Value: rxValue,
Label: fmt.Sprintf("↓ %s", net.FormatRecvRate()),
LeftLabel: "0",
RightLabel: humanize.SI(float64(linkSpeed), "bit/s"),
BarColor: color.RGBA{100, 100, 255, 255},
}
rxBar.Draw(canvas)
y += 60
if y > 900 {
break // Don't overflow the screen
}
}
}
return nil
}