diff --git a/app/app.go b/app/app.go index 84ae9bc..af44ce1 100644 --- a/app/app.go +++ b/app/app.go @@ -1,8 +1,8 @@ package app import ( - "finally-a-monolithic-linux-hw-monitor/app/cpu" - "finally-a-monolithic-linux-hw-monitor/app/cpu/proc" + "finally-a-monolithic-linux-hw-monitor/app/mem" + proc "finally-a-monolithic-linux-hw-monitor/app/proc" "fmt" "os" "os/signal" @@ -25,18 +25,18 @@ func Run() { os.Exit(0) }() - proc, err := proc.GetProc() + p, _ := proc.GetProc() + m, _ := mem.GetMem() ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { console := &strings.Builder{} + // Move cursor to top-left (0,0) without clearing full buffer (flicker-free) + fmt.Fprint(console, "\033[H") - if err != nil { - return - } - - cpu.RenderCPU(console, &proc) + proc.RenderProc(console, &p) + mem.RenderMem(console, &m) fmt.Fprint(console, "\nPress Ctrl+C to exit.\033[K\n") fmt.Print(console.String()) diff --git a/app/cpu/cpu.go b/app/cpu/cpu.go deleted file mode 100644 index dff31d5..0000000 --- a/app/cpu/cpu.go +++ /dev/null @@ -1,46 +0,0 @@ -package cpu - -import ( - "finally-a-monolithic-linux-hw-monitor/app/cpu/proc" - "fmt" - "strings" -) - -func RenderCPU(console *strings.Builder, proc *proc.Proc) { - // Move cursor to top-left (0,0) without clearing full buffer (flicker-free) - fmt.Fprint(console, "\033[H") - fmt.Fprintf(console, "=== %s ===\n", proc.Model()) - - RenderTemperatureSensors(console, proc) - RenderPowerSensors(console, proc) - RenderClockSensors(console, proc) -} - -func RenderTemperatureSensors(console *strings.Builder, proc *proc.Proc) { - temps, _ := proc.SensorReader.ReadTemperatureSensors() - fmt.Fprint(console, "--- Temperature ---\n") - for _, sensor := range temps { - tempC := sensor.TempC - tempF := (tempC * 9 / 5) + 32 - // \033[K clears from cursor to end of line (prevents lingering chars) - fmt.Fprintf(console, "%-16s | %.3f °C (%.3f °F)\033[K\n", sensor.Name, tempC, tempF) - } -} - -func RenderPowerSensors(console *strings.Builder, proc *proc.Proc) { - fmt.Fprint(console, "--- Power ---\n") - power, _ := proc.SensorReader.ReadPowerSensors() - for _, sensor := range power { - // \033[K clears from cursor to end of line (prevents lingering chars) - fmt.Fprintf(console, "%-16s | %.3f W\033[K\n", sensor.Name, sensor.Watts) - } -} - -func RenderClockSensors(console *strings.Builder, proc *proc.Proc) { - fmt.Fprint(console, "--- Clock Frequency ---\n") - power, _ := proc.SensorReader.ReadClockSensors() - for _, sensor := range power { - // \033[K clears from cursor to end of line (prevents lingering chars) - fmt.Fprintf(console, "%-16s | %.3f GHz\033[K\n", sensor.Name, sensor.FrequencyGHz) - } -} diff --git a/app/cpu/proc/proc.go b/app/cpu/proc/proc.go deleted file mode 100644 index 255765a..0000000 --- a/app/cpu/proc/proc.go +++ /dev/null @@ -1,87 +0,0 @@ -package proc - -import ( - "bufio" - driver "finally-a-monolithic-linux-hw-monitor/app/cpu/drivers" - "fmt" - "os" - "strings" -) - -type Proc struct { - Info map[string]string - SensorReader CPUSensorReader -} - -type CPUSensorReader interface { - ReadTemperatureSensors() ([]driver.TemperatureSensor, error) - ReadPowerSensors() ([]driver.PowerSensor, error) - ReadClockSensors() ([]driver.ClockSensor, error) -} - -const ( - VendorIntel string = "GenuineIntel" - VendorAMD string = "AuthenticAMD" - VendorUnknown string = "Unknown" -) - -func GetProc() (Proc, error) { - // open and parse cpuinfo - proc := Proc{} - procFile, err := os.Open("/proc/cpuinfo") - if err != nil { - return proc, err - } - defer procFile.Close() - - proc.Info = map[string]string{} - - scanner := bufio.NewScanner(procFile) - if scanner.Err() != nil { - return proc, scanner.Err() - } - - for scanner.Scan() { - line := scanner.Text() - line = strings.ReplaceAll(line, "\t", "") - if line != "" { - x := strings.Split(line, ": ") - if len(x) != 2 { - continue - } - key := x[0] - value := x[1] - proc.Info[key] = value - } - } - - return proc, proc.getSensorReader() -} - -func (proc *Proc) getSensorReader() error { - // get vendor string and initialize sensor reader - - vendor := proc.Vendor() - - if vendor == VendorIntel { - intelReader, err := NewIntelReader() - proc.SensorReader = intelReader - return err - } - - if vendor == VendorAMD { - amdReader, err := NewAMDReader() - proc.SensorReader = amdReader - return err - } - - return fmt.Errorf("proc type %s is not supported", vendor) -} - -func (proc *Proc) Vendor() string { - return proc.Info["vendor_id"] -} - -func (proc *Proc) Model() string { - return proc.Info["model name"] -} diff --git a/app/mem/drivers/dmidecode.go b/app/mem/drivers/dmidecode.go new file mode 100644 index 0000000..16664d1 --- /dev/null +++ b/app/mem/drivers/dmidecode.go @@ -0,0 +1,161 @@ +package drivers + +import ( + "bufio" + "fmt" + "os/exec" + "strconv" + "strings" +) + +type DmiDecodeDriver struct{} + +func NewDmiDecodeDriver() *DmiDecodeDriver { + return &DmiDecodeDriver{} +} + +// Parses dmidecode to extract physical hardware limits, slot counts, and module details +func (r *DmiDecodeDriver) ReadHardwareSensors() (MemoryHardware, error) { + hw := MemoryHardware{} + + // Query Memory Array (Type 16) for Max Capacity and Slot Count + arrayCmd := exec.Command("dmidecode", "-t", "16") + arrayOutput, err := arrayCmd.Output() + if err != nil { + return hw, fmt.Errorf("dmidecode -t 16 failed: %w", err) + } + + // Query Memory Device (Type 17) for active slots, speed, type, form factor, and model name + deviceCmd := exec.Command("dmidecode", "-t", "17") + deviceOutput, err := deviceCmd.Output() + if err != nil { + return hw, fmt.Errorf("dmidecode -t 17 failed: %w", err) + } + + // Parse Type 16 Output + scanner := bufio.NewScanner(strings.NewReader(string(arrayOutput))) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "Maximum Capacity:") { + hw.MaxCapacityGB = parseCapacityToGB(line) + } else if strings.HasPrefix(line, "Number Of Devices:") { + parts := strings.Split(line, ":") + if len(parts) == 2 { + hw.TotalSlots, _ = strconv.Atoi(strings.TrimSpace(parts[1])) + } else { + return hw, fmt.Errorf("parsing dmidecode -t 16 failed") + } + } + } + + // Parse Type 17 Output + var currentSize, currentType, currentSpeed, currentForm, currentVendor, currentPart string + + deviceScanner := bufio.NewScanner(strings.NewReader(string(deviceOutput))) + for deviceScanner.Scan() { + line := strings.TrimSpace(deviceScanner.Text()) + + if strings.HasPrefix(line, "Memory Device") { + processDeviceBlock(currentSize, currentType, currentSpeed, currentForm, currentVendor, currentPart, &hw) + currentSize, currentType, currentSpeed, currentForm, currentVendor, currentPart = "", "", "", "", "", "" + continue + } + + if strings.HasPrefix(line, "Size:") { + currentSize = strings.TrimSpace(strings.TrimPrefix(line, "Size:")) + } else if strings.HasPrefix(line, "Type:") { + currentType = strings.TrimSpace(strings.TrimPrefix(line, "Type:")) + } else if strings.HasPrefix(line, "Speed:") || strings.HasPrefix(line, "Configured Memory Speed:") { + val := strings.TrimSpace(strings.Split(line, ":")[1]) + if val != "Unknown" && val != "" && currentSpeed == "" { + currentSpeed = val + } + } else if strings.HasPrefix(line, "Form Factor:") { + currentForm = strings.TrimSpace(strings.TrimPrefix(line, "Form Factor:")) + } else if strings.HasPrefix(line, "Manufacturer:") { + currentVendor = strings.TrimSpace(strings.TrimPrefix(line, "Manufacturer:")) + } else if strings.HasPrefix(line, "Part Number:") { + currentPart = strings.TrimSpace(strings.TrimPrefix(line, "Part Number:")) + } + } + // Process the final block + processDeviceBlock(currentSize, currentType, currentSpeed, currentForm, currentVendor, currentPart, &hw) + + return hw, nil +} + +func processDeviceBlock(size, memType, speed, form, vendor, part string, hw *MemoryHardware) { + if size != "" && size != "No Module Installed" && size != "Unknown" { + hw.SlotsUsed++ + + if memType != "" && memType != "Unknown" { + hw.Generations = append(hw.Generations, memType) + } else { + hw.Generations = append(hw.Generations, "Unknown") + } + + if speed != "" && speed != "Unknown" { + hw.Speeds = append(hw.Speeds, speed) + } else { + hw.Speeds = append(hw.Speeds, "Unknown") + } + + if form != "" && form != "Unknown" { + hw.FormFactors = append(hw.FormFactors, form) + } else { + hw.FormFactors = append(hw.FormFactors, "Unknown") + } + + // Construct model/part identifier + partClean := cleanString(part) + vendorClean := cleanString(vendor) + + if partClean != "" && vendorClean != "" { + hw.ModelNames = append(hw.ModelNames, fmt.Sprintf("%s (%s)", partClean, vendorClean)) + } else if partClean != "" { + hw.ModelNames = append(hw.ModelNames, partClean) + } else if vendorClean != "" { + hw.ModelNames = append(hw.ModelNames, vendorClean) + } else { + hw.ModelNames = append(hw.ModelNames, "Unknown") + } + } +} + +func cleanString(val string) string { + v := strings.TrimSpace(val) + if v == "" || v == "Unknown" || v == "NO DIMM" || strings.HasPrefix(v, "0x") { + return "" + } + return v +} + +func parseCapacityToGB(line string) float64 { + parts := strings.Split(line, ":") + if len(parts) < 2 { + return 0 + } + valStr := strings.TrimSpace(parts[1]) + fields := strings.Fields(valStr) + if len(fields) < 2 { + return 0 + } + val, err := strconv.ParseFloat(fields[0], 64) + if err != nil { + return 0 + } + unit := strings.ToUpper(fields[1]) + if strings.HasPrefix(unit, "TB") { + return val * 1024 + } else if strings.HasPrefix(unit, "MB") { + return val / 1024 + } + return val +} + +func formatSlice(s []string) string { + if len(s) == 0 { + return "N/A" + } + return strings.Join(s, ", ") +} diff --git a/app/mem/drivers/drivers.go b/app/mem/drivers/drivers.go new file mode 100644 index 0000000..1cc48f0 --- /dev/null +++ b/app/mem/drivers/drivers.go @@ -0,0 +1,28 @@ +package drivers + +// MemoryUsage holds live RAM stats from /proc/meminfo +type MemoryUsage struct { + TotalGB float64 + AvailableGB float64 + UsedGB float64 + UsedPercent float64 +} + +type UsageDriver interface { + ReadUsageSensors() (MemoryUsage, error) +} + +// MemoryHardware holds physical RAM slot and module details +type MemoryHardware struct { + MaxCapacityGB float64 + TotalSlots int + SlotsUsed int + Generations []string + Speeds []string + FormFactors []string + ModelNames []string +} + +type HardwareDriver interface { + ReadHardwareSensors() (MemoryHardware, error) +} diff --git a/app/mem/drivers/meminfo.go b/app/mem/drivers/meminfo.go new file mode 100644 index 0000000..3bb9f8c --- /dev/null +++ b/app/mem/drivers/meminfo.go @@ -0,0 +1,63 @@ +package drivers + +import ( + "bufio" + "os" + "strconv" + "strings" +) + +type MemInfoDriver struct{} + +func NewMemInfoDriver() *MemInfoDriver { + return &MemInfoDriver{} +} + +// Read /proc/meminfo for active system memory statistics +func (r *MemInfoDriver) ReadUsageSensors() (MemoryUsage, error) { + usage := MemoryUsage{} + file, err := os.Open("/proc/meminfo") + if err != nil { + return usage, err + } + defer file.Close() + + var memTotal, memAvailable float64 + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + + val, err := strconv.ParseFloat(fields[1], 64) + if err != nil { + continue + } + + switch fields[0] { + case "MemTotal:": + memTotal = val // stored in kB + case "MemAvailable:": + memAvailable = val // stored in kB + } + } + + if err := scanner.Err(); err != nil { + return usage, err + } + + used := memTotal - memAvailable + usedPct := (used / memTotal) * 100 + + usage = MemoryUsage{ + TotalGB: memTotal / (1024 * 1024), + AvailableGB: memAvailable / (1024 * 1024), + UsedGB: used / (1024 * 1024), + UsedPercent: usedPct, + } + + return usage, nil +} diff --git a/app/mem/mem.go b/app/mem/mem.go new file mode 100644 index 0000000..1604458 --- /dev/null +++ b/app/mem/mem.go @@ -0,0 +1,69 @@ +package mem + +import ( + "finally-a-monolithic-linux-hw-monitor/app/mem/drivers" + "fmt" + "strings" +) + +func RenderMem(console *strings.Builder, mem *Mem) { + fmt.Fprintf(console, "=== %s ===\n", "Memory") + RenderUsageSensors(console, mem) + RenderHardwareSensors(console, mem) +} + +func RenderUsageSensors(console *strings.Builder, mem *Mem) { + usage, _ := mem.SensorReader.ReadUsageSensors() + fmt.Fprintf(console, "--- Usage ---\n") + fmt.Fprintf(console, "%-16s | %.2f GiB\033[K\n", "Total", usage.TotalGB) + fmt.Fprintf(console, "%-16s | %.2f GiB (%.2f%%) \033[K\n", "Used", usage.UsedGB, usage.UsedPercent) + fmt.Fprintf(console, "%-16s | %.2f GiB\033[K\n", "Avail", usage.AvailableGB) +} + +func RenderHardwareSensors(console *strings.Builder, mem *Mem) { + hardware, _ := mem.SensorReader.ReadHardwareSensors() + for i := range len(hardware.ModelNames) { + fmt.Fprintf(console, "--- RAM Module %d ---\n", i) + modelName := hardware.ModelNames[i] + fmt.Fprintf(console, "%-16s | %-16s \033[K\n", "Model Name", modelName) + generation := hardware.Generations[i] + speed := hardware.Speeds[i] + fmt.Fprintf(console, "%-16s | %s@%s \033[K\n", "Speed", generation, speed) + formFactor := hardware.FormFactors[i] + fmt.Fprintf(console, "%-16s | %s \033[K\n", "Form Factor", formFactor) + } +} + +type Mem struct { + SensorReader MemSensorReader +} + +func GetMem() (Mem, error) { + memInfoDriver := drivers.NewMemInfoDriver() + dmiDecodeDriver := drivers.NewDmiDecodeDriver() + reader := &StandardMemSensorReader{ + UsageReader: memInfoDriver, + HardwareReader: dmiDecodeDriver, + } + return Mem{ + SensorReader: reader, + }, nil +} + +type MemSensorReader interface { + ReadUsageSensors() (drivers.MemoryUsage, error) + ReadHardwareSensors() (drivers.MemoryHardware, error) +} + +type StandardMemSensorReader struct { + UsageReader drivers.UsageDriver + HardwareReader drivers.HardwareDriver +} + +func (r *StandardMemSensorReader) ReadUsageSensors() (drivers.MemoryUsage, error) { + return r.UsageReader.ReadUsageSensors() +} + +func (r *StandardMemSensorReader) ReadHardwareSensors() (drivers.MemoryHardware, error) { + return r.HardwareReader.ReadHardwareSensors() +} diff --git a/app/cpu/drivers/drivers.go b/app/proc/drivers/drivers.go similarity index 100% rename from app/cpu/drivers/drivers.go rename to app/proc/drivers/drivers.go diff --git a/app/cpu/drivers/hwmon.go b/app/proc/drivers/hwmon.go similarity index 100% rename from app/cpu/drivers/hwmon.go rename to app/proc/drivers/hwmon.go diff --git a/app/cpu/drivers/rapl.go b/app/proc/drivers/rapl.go similarity index 100% rename from app/cpu/drivers/rapl.go rename to app/proc/drivers/rapl.go diff --git a/app/cpu/drivers/sys.go b/app/proc/drivers/sys.go similarity index 100% rename from app/cpu/drivers/sys.go rename to app/proc/drivers/sys.go diff --git a/app/proc/proc.go b/app/proc/proc.go new file mode 100644 index 0000000..b5376af --- /dev/null +++ b/app/proc/proc.go @@ -0,0 +1,117 @@ +package proc + +import ( + "bufio" + "finally-a-monolithic-linux-hw-monitor/app/proc/drivers" + "fmt" + "os" + "strings" +) + +func RenderProc(console *strings.Builder, proc *Proc) { + fmt.Fprintf(console, "=== %s ===\n", proc.Model()) + + RenderTemperatureSensors(console, proc) + RenderPowerSensors(console, proc) + RenderClockSensors(console, proc) +} + +func RenderTemperatureSensors(console *strings.Builder, proc *Proc) { + temps, _ := proc.SensorReader.ReadTemperatureSensors() + fmt.Fprint(console, "--- Temperature ---\n") + for _, sensor := range temps { + tempC := sensor.TempC + tempF := (tempC * 9 / 5) + 32 + // \033[K clears from cursor to end of line (prevents lingering chars) + fmt.Fprintf(console, "%-16s | %.2f °C (%.3f °F)\033[K\n", sensor.Name, tempC, tempF) + } +} + +func RenderPowerSensors(console *strings.Builder, proc *Proc) { + fmt.Fprint(console, "--- Power ---\n") + power, _ := proc.SensorReader.ReadPowerSensors() + for _, sensor := range power { + // \033[K clears from cursor to end of line (prevents lingering chars) + fmt.Fprintf(console, "%-16s | %.2f W\033[K\n", sensor.Name, sensor.Watts) + } +} + +func RenderClockSensors(console *strings.Builder, proc *Proc) { + fmt.Fprint(console, "--- Clock Frequency ---\n") + power, _ := proc.SensorReader.ReadClockSensors() + for _, sensor := range power { + // \033[K clears from cursor to end of line (prevents lingering chars) + fmt.Fprintf(console, "%-16s | %.2f GHz\033[K\n", sensor.Name, sensor.FrequencyGHz) + } +} + +type Proc struct { + SensorReader ProcSensorReader + Info map[string]string +} + +type ProcSensorReader interface { + ReadTemperatureSensors() ([]drivers.TemperatureSensor, error) + ReadPowerSensors() ([]drivers.PowerSensor, error) + ReadClockSensors() ([]drivers.ClockSensor, error) +} + +const ( + VendorIntel string = "GenuineIntel" + VendorAMD string = "AuthenticAMD" + VendorUnknown string = "Unknown" +) + +func GetProc() (Proc, error) { + // open and parse cpuinfo + proc := Proc{} + procFile, err := os.Open("/proc/cpuinfo") + if err != nil { + return proc, err + } + defer procFile.Close() + + proc.Info = map[string]string{} + + scanner := bufio.NewScanner(procFile) + if scanner.Err() != nil { + return proc, scanner.Err() + } + + for scanner.Scan() { + line := scanner.Text() + line = strings.ReplaceAll(line, "\t", "") + if line != "" { + x := strings.Split(line, ": ") + if len(x) != 2 { + continue + } + key := x[0] + value := x[1] + proc.Info[key] = value + } + } + + vendor := proc.Vendor() + + switch vendor { + case VendorIntel: + intelReader, err := NewIntelReader() + proc.SensorReader = intelReader + return proc, err + case VendorAMD: + amdReader, err := NewAMDReader() + proc.SensorReader = amdReader + return proc, err + default: + return proc, fmt.Errorf("proc type %s is not supported", vendor) + } +} + +func (proc *Proc) Vendor() string { + return proc.Info["vendor_id"] +} + +func (proc *Proc) Model() string { + return proc.Info["model name"] +} diff --git a/app/cpu/proc/vendor_amd.go b/app/proc/vendor_amd.go similarity index 94% rename from app/cpu/proc/vendor_amd.go rename to app/proc/vendor_amd.go index 351e8a3..fe3350d 100644 --- a/app/cpu/proc/vendor_amd.go +++ b/app/proc/vendor_amd.go @@ -1,7 +1,7 @@ package proc import ( - driver "finally-a-monolithic-linux-hw-monitor/app/cpu/drivers" + driver "finally-a-monolithic-linux-hw-monitor/app/proc/drivers" "fmt" ) diff --git a/app/cpu/proc/vendor_intel.go b/app/proc/vendor_intel.go similarity index 94% rename from app/cpu/proc/vendor_intel.go rename to app/proc/vendor_intel.go index c1c9898..d8f4c63 100644 --- a/app/cpu/proc/vendor_intel.go +++ b/app/proc/vendor_intel.go @@ -1,7 +1,7 @@ package proc import ( - driver "finally-a-monolithic-linux-hw-monitor/app/cpu/drivers" + driver "finally-a-monolithic-linux-hw-monitor/app/proc/drivers" "fmt" )