Processes tab: btop-style cpu/mem/net boxes with labels, gradient bars, block graph, CPU model/GHz/temp, memory breakdown, per-interface rates, and a refresh interval control

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-18 06:05:09 -07:00
co-authored by Claude Fable 5.1
parent e4df878ac0
commit b802dcf313
4 changed files with 197 additions and 51 deletions
+86 -15
View File
@@ -37,9 +37,12 @@ const (
)
type Mem struct {
Total uint64 `json:"total"`
Used uint64 `json:"used"`
Avail uint64 `json:"avail"`
Total uint64 `json:"total"`
Used uint64 `json:"used"`
Avail uint64 `json:"avail"`
Free uint64 `json:"free,omitempty"`
Cached uint64 `json:"cached,omitempty"`
Buffers uint64 `json:"buffers,omitempty"`
}
type Temp struct {
@@ -379,7 +382,7 @@ func meminfo() (Mem, Mem) {
n, _ := strconv.ParseUint(f[0], 10, 64)
m[k] = n * 1024
}
mem := Mem{Total: m["MemTotal"], Avail: m["MemAvailable"]}
mem := Mem{Total: m["MemTotal"], Avail: m["MemAvailable"], Free: m["MemFree"], Buffers: m["Buffers"], Cached: m["Cached"] + m["SReclaimable"]}
if mem.Avail == 0 { // pre-3.14 kernels: approximate
mem.Avail = m["MemFree"] + m["Buffers"] + m["Cached"]
}
@@ -885,15 +888,69 @@ func uidName(uid string) string {
// ProcTable is the Processes tab payload.
type ProcTable struct {
T int64 `json:"t"`
CPU float64 `json:"cpu"`
Cores []float64 `json:"cores"`
Load [3]float64 `json:"load"`
Mem Mem `json:"mem"`
Swap Mem `json:"swap"`
Uptime float64 `json:"uptime"`
Procs []Proc `json:"procs"`
Total int `json:"total"`
T int64 `json:"t"`
CPU float64 `json:"cpu"`
Cores []float64 `json:"cores"`
CPUHist []float64 `json:"cpu_hist"` // recent total-CPU samples, oldest first
CPUModel string `json:"cpu_model,omitempty"`
CPUMHz float64 `json:"cpu_mhz,omitempty"`
CPUTemp float64 `json:"cpu_temp,omitempty"`
Load [3]float64 `json:"load"`
Mem Mem `json:"mem"`
Swap Mem `json:"swap"`
Net []Net `json:"net"`
Uptime float64 `json:"uptime"`
Procs []Proc `json:"procs"`
Total int `json:"total"`
Running int `json:"running"`
}
var cpuModel = func() string {
for _, line := range strings.Split(readStr("/proc/cpuinfo"), "\n") {
if strings.HasPrefix(line, "model name") || strings.HasPrefix(line, "Model") || strings.HasPrefix(line, "Hardware") {
if _, v, ok := strings.Cut(line, ":"); ok {
return strings.Join(strings.Fields(v), " ")
}
}
}
return ""
}()
func cpuMHz() float64 {
var sum float64
n := 0
for _, line := range strings.Split(readStr("/proc/cpuinfo"), "\n") {
if strings.HasPrefix(line, "cpu MHz") {
if _, v, ok := strings.Cut(line, ":"); ok {
f, _ := strconv.ParseFloat(strings.TrimSpace(v), 64)
sum += f
n++
}
}
}
if n == 0 { // ARM etc.: scaling_cur_freq in kHz
if v, ok := readInt("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq"); ok {
return float64(v) / 1000
}
return 0
}
return sum / float64(n)
}
// cpuTemp picks the sensor that best represents the CPU package.
func cpuTemp(temps []Temp) float64 {
best, max := 0.0, 0.0
for _, t := range temps {
l := strings.ToLower(t.Label)
if t.Chip == "coretemp" && strings.HasPrefix(l, "package") || t.Chip == "k10temp" && (l == "tctl" || l == "tdie") ||
t.Chip == "thermal" && strings.Contains(l, "cpu") || t.Chip == "cpu_thermal" || t.Chip == "soc_thermal" {
return t.C
}
if (t.Chip == "coretemp" || t.Chip == "k10temp") && t.C > max {
max, best = t.C, t.C
}
}
return best
}
var bootTime = func() int64 {
@@ -997,10 +1054,24 @@ func (c *Collector) Processes() ProcTable {
}
return procs[i].RSS > procs[j].RSS
})
t := ProcTable{T: now.UnixMilli(), Procs: procs, Total: len(procs), Uptime: uptime(), Load: loadavg()}
t := ProcTable{T: now.UnixMilli(), Procs: procs, Total: len(procs), Uptime: uptime(), Load: loadavg(),
CPUModel: cpuModel, CPUMHz: round1(cpuMHz()), CPUHist: []float64{}, Net: []Net{}}
for _, p := range procs {
if p.State == "R" {
t.Running++
}
}
t.Mem, t.Swap = meminfo()
if n := len(c.ring); n > 0 {
t.CPU, t.Cores = c.ring[n-1].CPU, c.ring[n-1].Cores
last := c.ring[n-1]
t.CPU, t.Cores, t.Net, t.CPUTemp = last.CPU, last.Cores, last.Net, cpuTemp(last.Temps)
from := n - 90
if from < 0 {
from = 0
}
for _, s := range c.ring[from:] {
t.CPUHist = append(t.CPUHist, s.CPU)
}
}
c.lastTable = &t
return t