mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 11:31:46 +00:00
feat: improve network status graph with sparklines and responsive width
This commit is contained in:
@@ -12,6 +12,50 @@ import (
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
)
|
||||
|
||||
// RingBuffer is a fixed-size circular buffer for float64 values.
|
||||
type RingBuffer struct {
|
||||
data []float64
|
||||
index int // Current insert position (oldest value)
|
||||
size int // Number of valid elements
|
||||
cap int // Total capacity
|
||||
}
|
||||
|
||||
func NewRingBuffer(capacity int) *RingBuffer {
|
||||
return &RingBuffer{
|
||||
data: make([]float64, capacity),
|
||||
cap: capacity,
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *RingBuffer) Add(val float64) {
|
||||
rb.data[rb.index] = val
|
||||
rb.index = (rb.index + 1) % rb.cap
|
||||
if rb.size < rb.cap {
|
||||
rb.size++
|
||||
}
|
||||
}
|
||||
|
||||
// Slice returns the data in chronological order (oldest to newest).
|
||||
func (rb *RingBuffer) Slice() []float64 {
|
||||
if rb.size == 0 {
|
||||
return nil
|
||||
}
|
||||
res := make([]float64, rb.size)
|
||||
if rb.size < rb.cap {
|
||||
// Not full yet: data is at [0 : size]
|
||||
copy(res, rb.data[:rb.size])
|
||||
} else {
|
||||
// Full: oldest is at index, then wrapped
|
||||
// data: [4, 5, 1, 2, 3] (cap=5, index=2, oldest=1)
|
||||
// want: [1, 2, 3, 4, 5]
|
||||
// part1: [index:] -> [1, 2, 3]
|
||||
// part2: [:index] -> [4, 5]
|
||||
copy(res, rb.data[rb.index:])
|
||||
copy(res[rb.cap-rb.index:], rb.data[:rb.index])
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type MetricsSnapshot struct {
|
||||
CollectedAt time.Time
|
||||
Host string
|
||||
@@ -105,12 +149,13 @@ type NetworkStatus struct {
|
||||
TxRateMBs float64
|
||||
IP string
|
||||
}
|
||||
// NetworkHistory holds the global network usage history.
|
||||
type NetworkHistory struct {
|
||||
RxHistory []float64
|
||||
TxHistory []float64
|
||||
}
|
||||
|
||||
const NetworkHistorySize = 20 // number of checks to keep
|
||||
const NetworkHistorySize = 120 // Increased history size for wider graph
|
||||
|
||||
type ProxyStatus struct {
|
||||
Enabled bool
|
||||
@@ -161,18 +206,21 @@ type Collector struct {
|
||||
lastBT []BluetoothDevice
|
||||
|
||||
// Fast metrics (1s).
|
||||
prevNet map[string]net.IOCountersStat
|
||||
lastNetAt time.Time
|
||||
netHistory NetworkHistory
|
||||
lastGPUAt time.Time
|
||||
cachedGPU []GPUStatus
|
||||
prevDiskIO disk.IOCountersStat
|
||||
lastDiskAt time.Time
|
||||
prevNet map[string]net.IOCountersStat
|
||||
lastNetAt time.Time
|
||||
rxHistoryBuf *RingBuffer
|
||||
txHistoryBuf *RingBuffer
|
||||
lastGPUAt time.Time
|
||||
cachedGPU []GPUStatus
|
||||
prevDiskIO disk.IOCountersStat
|
||||
lastDiskAt time.Time
|
||||
}
|
||||
|
||||
func NewCollector() *Collector {
|
||||
return &Collector{
|
||||
prevNet: make(map[string]net.IOCountersStat),
|
||||
prevNet: make(map[string]net.IOCountersStat),
|
||||
rxHistoryBuf: NewRingBuffer(NetworkHistorySize),
|
||||
txHistoryBuf: NewRingBuffer(NetworkHistorySize),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,8 +319,11 @@ func (c *Collector) Collect() (MetricsSnapshot, error) {
|
||||
Disks: diskStats,
|
||||
DiskIO: diskIO,
|
||||
Network: netStats,
|
||||
NetworkHistory: c.netHistory,
|
||||
Proxy: proxyStats,
|
||||
NetworkHistory: NetworkHistory{
|
||||
RxHistory: c.rxHistoryBuf.Slice(),
|
||||
TxHistory: c.txHistoryBuf.Slice(),
|
||||
},
|
||||
Proxy: proxyStats,
|
||||
Batteries: batteryStats,
|
||||
Thermal: thermalStats,
|
||||
Sensors: sensorStats,
|
||||
|
||||
@@ -75,18 +75,50 @@ func (c *Collector) collectNetwork(now time.Time) ([]NetworkStatus, error) {
|
||||
totalRx += r.RxRateMBs
|
||||
totalTx += r.TxRateMBs
|
||||
}
|
||||
c.netHistory.RxHistory = append(c.netHistory.RxHistory, totalRx)
|
||||
c.netHistory.TxHistory = append(c.netHistory.TxHistory, totalTx)
|
||||
if len(c.netHistory.RxHistory) > NetworkHistorySize {
|
||||
c.netHistory.RxHistory = c.netHistory.RxHistory[len(c.netHistory.RxHistory)-NetworkHistorySize:]
|
||||
}
|
||||
if len(c.netHistory.TxHistory) > NetworkHistorySize {
|
||||
c.netHistory.TxHistory = c.netHistory.TxHistory[len(c.netHistory.TxHistory)-NetworkHistorySize:]
|
||||
}
|
||||
|
||||
// Update history using the global/aggregated stats
|
||||
c.rxHistoryBuf.Add(totalRx)
|
||||
c.txHistoryBuf.Add(totalTx)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Rewriting slightly more of the file to inject history update logic correctly inside the loop.
|
||||
// The previous "tail" logic for totalRx history was actually not what I wrote in the *previous* step
|
||||
// (Wait, did the `pull` bring in my changes? No, I implemented them, then did `git reset` then `git pull`.
|
||||
// The `git pull` brought in the changes from `dev`.
|
||||
// In `dev` (which I pulled), the code at the bottom of `collectNetwork` (lines 73-86 in View)
|
||||
// seems to be appending to `c.netHistory.RxHistory`.
|
||||
// So the merged code uses a GLOBAL history in `MetricsSnapshot` (or `Collector`?)
|
||||
// Let's check `metrics.go` again.
|
||||
// In the pulled `metrics.go` (before my generic change):
|
||||
// type NetworkHistory struct { RxHistory []float64 ... }
|
||||
// type Collector struct { ... netHistory NetworkHistory ... }
|
||||
// So the user's merged code uses a SINGLE global history struct, not a map per interface.
|
||||
// This simplifies things! It aggregates ALL traffic history?
|
||||
// Or does it just append the totals?
|
||||
// Line 73-77 calculates `totalRx`.
|
||||
// Line 78 appends `totalRx` to `c.netHistory.RxHistory`.
|
||||
// Yes, it tracks GLOBAL network usage.
|
||||
// So I should adapt my RingBuffer to replace `NetworkHistory` struct usage.
|
||||
|
||||
// I will replace `collectNetwork` to use the new `map[string][2]*RingBuffer`?
|
||||
// User asked to "optimize". Global history is easier for the UI ("Total Down/Up").
|
||||
// Per-interface history is more detailed but if UI only shows one sparkline, Global is better.
|
||||
// The user said "responsive width... reference Proxy System".
|
||||
// And "generic history structure".
|
||||
// If I use RingBuffer, I should probably stick to the GLOBAL history design if that's what `dev` has,
|
||||
// OR change `Collector` to use `RingBuffer` for that global history.
|
||||
//
|
||||
// Let's look at `metrics.go` again (my previous edit).
|
||||
// I changed `netHistory` to `map[string][2]*RingBuffer`.
|
||||
// This contradicts the `dev` branch's `NetworkHistory` (global).
|
||||
// I should probably revert to a SINGLE `RingBuffer` pair for global history if the UI expects global.
|
||||
// Usage in `view.go` (which I haven't read fully yet after pull) will tell me.
|
||||
// If `view.go` uses `m.NetworkHistory.RxHistory`, then it expects global.
|
||||
// Let's check `view.go` first before editing `metrics_network.go`.
|
||||
|
||||
|
||||
func getInterfaceIPs() map[string]string {
|
||||
result := make(map[string]string)
|
||||
ifaces, err := net.Interfaces()
|
||||
|
||||
@@ -201,20 +201,7 @@ func getScoreStyle(score int) lipgloss.Style {
|
||||
}
|
||||
}
|
||||
|
||||
func buildCards(m MetricsSnapshot, _ int) []cardData {
|
||||
cards := []cardData{
|
||||
renderCPUCard(m.CPU, m.Thermal),
|
||||
renderMemoryCard(m.Memory),
|
||||
renderDiskCard(m.Disks, m.DiskIO),
|
||||
renderBatteryCard(m.Batteries, m.Thermal),
|
||||
renderProcessCard(m.TopProcesses),
|
||||
renderNetworkCard(m.Network, m.NetworkHistory, m.Proxy),
|
||||
}
|
||||
if hasSensorData(m.Sensors) {
|
||||
cards = append(cards, renderSensorsCard(m.Sensors))
|
||||
}
|
||||
return cards
|
||||
}
|
||||
|
||||
|
||||
func hasSensorData(sensors []SensorReading) bool {
|
||||
for _, s := range sensors {
|
||||
@@ -417,6 +404,21 @@ func renderProcessCard(procs []ProcessInfo) cardData {
|
||||
return cardData{icon: iconProcs, title: "Processes", lines: lines}
|
||||
}
|
||||
|
||||
func buildCards(m MetricsSnapshot, width int) []cardData {
|
||||
cards := []cardData{
|
||||
renderCPUCard(m.CPU, m.Thermal),
|
||||
renderMemoryCard(m.Memory),
|
||||
renderDiskCard(m.Disks, m.DiskIO),
|
||||
renderBatteryCard(m.Batteries, m.Thermal),
|
||||
renderProcessCard(m.TopProcesses),
|
||||
renderNetworkCard(m.Network, m.NetworkHistory, m.Proxy, width),
|
||||
}
|
||||
if hasSensorData(m.Sensors) {
|
||||
cards = append(cards, renderSensorsCard(m.Sensors))
|
||||
}
|
||||
return cards
|
||||
}
|
||||
|
||||
func miniBar(percent float64) string {
|
||||
filled := min(int(percent/20), 5)
|
||||
if filled < 0 {
|
||||
@@ -425,7 +427,7 @@ func miniBar(percent float64) string {
|
||||
return colorizePercent(percent, strings.Repeat("▮", filled)+strings.Repeat("▯", 5-filled))
|
||||
}
|
||||
|
||||
func renderNetworkCard(netStats []NetworkStatus, history NetworkHistory, proxy ProxyStatus) cardData {
|
||||
func renderNetworkCard(netStats []NetworkStatus, history NetworkHistory, proxy ProxyStatus, cardWidth int) cardData {
|
||||
var lines []string
|
||||
var totalRx, totalTx float64
|
||||
var primaryIP string
|
||||
@@ -441,9 +443,21 @@ func renderNetworkCard(netStats []NetworkStatus, history NetworkHistory, proxy P
|
||||
if len(netStats) == 0 {
|
||||
lines = []string{subtleStyle.Render("Collecting...")}
|
||||
} else {
|
||||
// Calculate dynamic width
|
||||
// Layout: "Down " (7) + graph + " " (2) + rate (approx 10-12)
|
||||
// Safe margin: 22 chars.
|
||||
// We target 16 chars to match progressBar implementation for visual consistency.
|
||||
graphWidth := cardWidth - 22
|
||||
if graphWidth < 5 {
|
||||
graphWidth = 5
|
||||
}
|
||||
if graphWidth > 16 {
|
||||
graphWidth = 16 // Match progressBar fixed width
|
||||
}
|
||||
|
||||
// sparkline graphs
|
||||
rxSparkline := sparkline(history.RxHistory, totalRx)
|
||||
txSparkline := sparkline(history.TxHistory, totalTx)
|
||||
rxSparkline := sparkline(history.RxHistory, totalRx, graphWidth)
|
||||
txSparkline := sparkline(history.TxHistory, totalTx, graphWidth)
|
||||
lines = append(lines, fmt.Sprintf("Down %s %s", rxSparkline, formatRate(totalRx)))
|
||||
lines = append(lines, fmt.Sprintf("Up %s %s", txSparkline, formatRate(totalTx)))
|
||||
// Show proxy and IP on one line.
|
||||
@@ -477,8 +491,7 @@ func netBar(rate float64) string {
|
||||
}
|
||||
|
||||
// 8 levels: ▁▂▃▄▅▆▇█
|
||||
func sparkline(history []float64, current float64) string {
|
||||
const width = 16
|
||||
func sparkline(history []float64, current float64, width int) string {
|
||||
blocks := []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||
|
||||
data := make([]float64, 0, width)
|
||||
|
||||
Reference in New Issue
Block a user