2026-04-01 17:27:31 -05:00
|
|
|
//go:build linux
|
|
|
|
|
|
|
|
|
|
package sysinfo
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
2026-05-05 19:31:55 -05:00
|
|
|
"runtime"
|
2026-04-01 17:27:31 -05:00
|
|
|
"strings"
|
2026-05-05 19:31:55 -05:00
|
|
|
"syscall"
|
2026-04-01 17:27:31 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func collectPlatform() Info {
|
2026-05-30 22:44:55 -05:00
|
|
|
percent, charging := batteryStatus()
|
2026-04-01 17:27:31 -05:00
|
|
|
return Info{
|
2026-05-30 22:44:55 -05:00
|
|
|
CPU: cpuName(),
|
|
|
|
|
GPU: gpuName(),
|
|
|
|
|
RAM: totalRAM(),
|
|
|
|
|
BatteryPercent: percent,
|
|
|
|
|
BatteryCharging: charging,
|
2026-04-01 17:27:31 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 22:44:55 -05:00
|
|
|
func OSName() string {
|
|
|
|
|
data, err := os.ReadFile("/etc/os-release")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "Linux"
|
|
|
|
|
}
|
|
|
|
|
values := map[string]string{}
|
|
|
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
|
|
|
line = strings.TrimSpace(line)
|
|
|
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
parts := strings.SplitN(line, "=", 2)
|
|
|
|
|
if len(parts) != 2 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
values[parts[0]] = strings.Trim(strings.TrimSpace(parts[1]), `"`)
|
|
|
|
|
}
|
|
|
|
|
if pretty := strings.TrimSpace(values["PRETTY_NAME"]); pretty != "" {
|
|
|
|
|
return pretty
|
|
|
|
|
}
|
|
|
|
|
if name := strings.TrimSpace(values["NAME"]); name != "" {
|
|
|
|
|
if version := strings.TrimSpace(values["VERSION_ID"]); version != "" {
|
|
|
|
|
return name + " " + version
|
|
|
|
|
}
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
return "Linux"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func batteryStatus() (*int, bool) {
|
|
|
|
|
entries, err := os.ReadDir("/sys/class/power_supply")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
for _, entry := range entries {
|
|
|
|
|
if !strings.HasPrefix(entry.Name(), "BAT") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
base := "/sys/class/power_supply/" + entry.Name()
|
|
|
|
|
capacityRaw, err := os.ReadFile(base + "/capacity")
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
var percent int
|
|
|
|
|
if _, err := fmt.Sscanf(strings.TrimSpace(string(capacityRaw)), "%d", &percent); err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if percent < 0 || percent > 100 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
statusRaw, _ := os.ReadFile(base + "/status")
|
|
|
|
|
status := strings.ToLower(strings.TrimSpace(string(statusRaw)))
|
|
|
|
|
charging := status == "charging" || status == "full"
|
|
|
|
|
return &percent, charging
|
|
|
|
|
}
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 17:27:31 -05:00
|
|
|
func cpuName() string {
|
|
|
|
|
data, err := os.ReadFile("/proc/cpuinfo")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "unknown"
|
|
|
|
|
}
|
|
|
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
|
|
|
if strings.HasPrefix(line, "model name") {
|
|
|
|
|
parts := strings.SplitN(line, ":", 2)
|
|
|
|
|
if len(parts) == 2 {
|
|
|
|
|
return strings.TrimSpace(parts[1])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "unknown"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func gpuName() string {
|
|
|
|
|
entries, err := os.ReadDir("/sys/class/drm")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "unknown"
|
|
|
|
|
}
|
|
|
|
|
var gpus []string
|
|
|
|
|
seen := map[string]bool{}
|
|
|
|
|
for _, e := range entries {
|
|
|
|
|
if !strings.HasPrefix(e.Name(), "card") || strings.Contains(e.Name(), "-") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
labelPath := "/sys/class/drm/" + e.Name() + "/device/label"
|
|
|
|
|
if data, err := os.ReadFile(labelPath); err == nil {
|
|
|
|
|
name := strings.TrimSpace(string(data))
|
|
|
|
|
if name != "" && !seen[name] {
|
|
|
|
|
seen[name] = true
|
|
|
|
|
gpus = append(gpus, name)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ueventPath := "/sys/class/drm/" + e.Name() + "/device/uevent"
|
|
|
|
|
if data, err := os.ReadFile(ueventPath); err == nil {
|
|
|
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
|
|
|
if strings.HasPrefix(line, "DRIVER=") {
|
|
|
|
|
drv := strings.TrimPrefix(line, "DRIVER=")
|
|
|
|
|
if drv != "" && !seen[drv] {
|
|
|
|
|
seen[drv] = true
|
|
|
|
|
gpus = append(gpus, drv)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(gpus) == 0 {
|
|
|
|
|
return "unknown"
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(gpus, ", ")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func totalRAM() string {
|
|
|
|
|
data, err := os.ReadFile("/proc/meminfo")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "unknown"
|
|
|
|
|
}
|
|
|
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
|
|
|
if strings.HasPrefix(line, "MemTotal:") {
|
|
|
|
|
fields := strings.Fields(line)
|
|
|
|
|
if len(fields) >= 2 {
|
|
|
|
|
var kb uint64
|
|
|
|
|
_, err := fmt.Sscanf(fields[1], "%d", &kb)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "unknown"
|
|
|
|
|
}
|
|
|
|
|
gb := float64(kb) / (1024 * 1024)
|
|
|
|
|
if gb >= 1 {
|
|
|
|
|
return fmt.Sprintf("%.0f GB", gb)
|
|
|
|
|
}
|
|
|
|
|
mb := float64(kb) / 1024
|
|
|
|
|
return fmt.Sprintf("%.0f MB", mb)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "unknown"
|
|
|
|
|
}
|
2026-05-05 19:31:55 -05:00
|
|
|
|
|
|
|
|
func HostArch() string {
|
|
|
|
|
var utsname syscall.Utsname
|
|
|
|
|
if err := syscall.Uname(&utsname); err == nil {
|
|
|
|
|
buf := make([]byte, 0, len(utsname.Machine))
|
|
|
|
|
for _, c := range utsname.Machine {
|
|
|
|
|
if c == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
buf = append(buf, byte(c))
|
|
|
|
|
}
|
|
|
|
|
machine := string(buf)
|
|
|
|
|
switch machine {
|
|
|
|
|
case "x86_64":
|
|
|
|
|
return "amd64"
|
|
|
|
|
case "aarch64":
|
|
|
|
|
return "arm64"
|
|
|
|
|
case "armv7l":
|
|
|
|
|
return "armv7"
|
|
|
|
|
case "i686", "i386":
|
|
|
|
|
return "386"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return runtime.GOARCH
|
|
|
|
|
}
|