diff --git a/README.md b/README.md index 8a0f988..b70dc9f 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,5 @@ make SYSCONFDIR=/etc make install make install-config SYSCONFDIR=/etc ``` +### Post installation +- (Optional) Download `curl` from your package manager to fetch the amdgpu.ids database for AMD GPUs. It may be safely uninstalled after running stormfetch once with curl installed diff --git a/config/config.yml b/config/config.yml index 76ab440..e9d0072 100644 --- a/config/config.yml +++ b/config/config.yml @@ -1,4 +1,5 @@ distro_ascii: auto +disable_amdgpu_ids_warning: false modules: - name: distribution - name: hostname diff --git a/src/config.go b/src/config.go index 7ba3804..3641025 100644 --- a/src/config.go +++ b/src/config.go @@ -9,10 +9,11 @@ import ( ) type StormfetchConfig struct { - Ascii string `yaml:"distro_ascii"` - Modules []stormfetchModuleConfig `yaml:"modules"` - AnsiiColors []int `yaml:"ansii_colors"` - ForceConfigAnsii bool `yaml:"force_config_ansii"` + Ascii string `yaml:"distro_ascii"` + DisableAmdgpuIdsWarning bool `yaml:"disable_amdgpu_ids_warning"` + Modules []stormfetchModuleConfig `yaml:"modules"` + AnsiiColors []int `yaml:"ansii_colors"` + ForceConfigAnsii bool `yaml:"force_config_ansii"` } var config = StormfetchConfig{ diff --git a/src/go.mod b/src/go.mod index 5cd776c..d537214 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,19 +1,19 @@ module stormfetch -go 1.22 +go 1.24.0 require ( - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a - github.com/jackmordaunt/ghw v1.0.4 + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 + github.com/jackmordaunt/ghw v1.0.5 github.com/mitchellh/go-ps v1.0.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/jackmordaunt/pcidb v1.0.1 // indirect github.com/jackmordaunt/wmi v1.2.4 // indirect github.com/kr/pretty v0.1.0 // indirect - golang.org/x/sys v0.3.0 // indirect + golang.org/x/sys v0.39.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/src/go.sum b/src/go.sum index 8071b55..2bc6f11 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,9 +1,15 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/jackmordaunt/ghw v1.0.4 h1:as+COFuPuXaNQC3WqzoHS/E2JYWZU7gN8ompNTUxNxs= github.com/jackmordaunt/ghw v1.0.4/go.mod h1:4dReYvJ36CoAzIxlEx8du25Qi/YqKYvEGE9QJoRXiK8= +github.com/jackmordaunt/ghw v1.0.5 h1:3rTXwu0D9RkungV7/WlhC8HVuDlPaq0aKY8u0I51jLY= +github.com/jackmordaunt/ghw v1.0.5/go.mod h1:VpFlLXnJErgoRttR3WOxun4v5EE8/xfB4cK26G5q2U0= github.com/jackmordaunt/pcidb v1.0.1 h1:uLLZa6kD5P39r2cwMyJJkxmuHfH9Wq19gYQEbYcB0Z4= github.com/jackmordaunt/pcidb v1.0.1/go.mod h1:OMmhrZOZVu2hYXhBDZXddypxwKR/dp4DbIgzCkQDxdQ= github.com/jackmordaunt/wmi v1.2.4 h1:/XyuMiKby0qXNQp1j0uU0JqTWLM0QGOVok1Hf5Yagtg= @@ -16,8 +22,11 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/src/hardware.go b/src/hardware.go index d18da66..baeecd4 100644 --- a/src/hardware.go +++ b/src/hardware.go @@ -1,9 +1,10 @@ package main import ( + "fmt" "os" - "os/exec" "slices" + "strconv" "strings" "github.com/go-gl/glfw/v3.3/glfw" @@ -11,12 +12,22 @@ import ( ) type CPU struct { - Vendor string Model string Cores int Threads int } +type GPU struct { + PCIAddress string + Vendor string + Name string + Product string + Subsystem string + Driver string + VramTotal string + VramUsed string +} + type Monitor struct { Width int Height int @@ -36,9 +47,23 @@ func GetCPUs(hiddenCPUs []int) []CPU { continue } + // Remove unnecessary information from the CPU model + model := cpu.Model + stringsToRemove := []string{ + " CPU", " FPU", " APU", " Processor", + " Dual-Core", " Quad-Core", " Six-Core", " Eight-Core", " Ten-Core", + " 2-Core", " 4-Core", " 6-Core", " 8-Core", " 10-Core", " 12-Core", " 14-Core", " 16-Core", + } + for _, str := range stringsToRemove { + model = strings.ReplaceAll(model, str, "") + } + model = strings.Split(model, "w/ Radeon ")[0] + model = strings.Split(model, "with Radeon ")[0] + model = strings.Split(model, "@")[0] + model = strings.TrimSpace(model) + ret = append(ret, CPU{ - Vendor: cpu.Vendor, - Model: cpu.Model, + Model: model, Cores: int(cpu.NumCores), Threads: int(cpu.NumThreads), }) @@ -47,22 +72,94 @@ func GetCPUs(hiddenCPUs []int) []CPU { return ret } -func GetGPUModels(hiddenGPUS []int) (ret []string) { - cmd := exec.Command("sh", "-c", "lspci -v -m | grep 'VGA' -A6 | grep '^Device:'") - bytes, err := cmd.Output() +func GetGPUModels(hiddenGPUs []int) []GPU { + ret := make([]GPU, 0) + + // Set stderr to nil to avoid warnings + stderr := os.Stderr + os.Stderr = nil + + gpus, err := ghw.GPU() if err != nil { - return nil + return ret } - for i, gpu := range strings.Split(string(bytes), "\n") { - if slices.Contains(hiddenGPUS, i+1) { + // Restore stderr + os.Stderr = stderr + + for i, gpu := range gpus.GraphicsCards { + if slices.Contains(hiddenGPUs, i+1) { continue } - if gpu == "" { - continue + + // Set alternative names for vendors + var vendor string + switch gpu.DeviceInfo.Vendor.ID { + case "1002": + vendor = "AMD" + case "10de": + vendor = "Nvidia" + case "8086": + vendor = "Intel" + default: + vendor = gpu.DeviceInfo.Vendor.Name } - gpu = strings.TrimPrefix(strings.TrimSpace(gpu), "Device:\t") - ret = append(ret, gpu) + + // Set GPU name + name := "" + + // Use GPU name from amdgpu.ids database + if vendor == "AMD" { + fetchedName, err := fetchAmdGpuName(gpu.DeviceInfo.Product.ID, gpu.DeviceInfo.Revision) + if err == nil && !config.DisableAmdgpuIdsWarning { + name = fetchedName + } else { + fmt.Println("Warning: could not fetch GPU name from amdgpu.ids database! Error: " + err.Error()) + fmt.Println(" You can disable this warning in the configuration file") + } + } + + if name == "" { + if gpu.DeviceInfo.Subsystem.Name == "" || gpu.DeviceInfo.Subsystem.Name == "unknown" { + // Set GPU name to product name + name = gpu.DeviceInfo.Product.Name + } else { + // Set GPU name to subsystem name + name = gpu.DeviceInfo.Subsystem.Name + } + + // Use GPU name in brackets + leftBracket := strings.IndexByte(name, '[') + rightBracket := strings.IndexByte(name, ']') + if leftBracket != -1 && rightBracket != -1 { + name = name[leftBracket+1 : rightBracket] + } + } + + // Get VRAM + vramTotal := "Unknown" + bytes, err := os.ReadFile("/sys/class/drm/card" + strconv.Itoa(gpu.Index) + "/device/mem_info_vram_total") + if err == nil { + vramUint, _ := strconv.ParseUint(strings.TrimSpace(string(bytes)), 10, 64) + vramTotal = FormatBytes(vramUint) + } + vramUsed := "Unknown" + bytes, err = os.ReadFile("/sys/class/drm/card" + strconv.Itoa(gpu.Index) + "/device/mem_info_vram_used") + if err == nil { + vramUint, _ := strconv.ParseUint(strings.TrimSpace(string(bytes)), 10, 64) + vramUsed = FormatBytes(vramUint) + } + + ret = append(ret, GPU{ + PCIAddress: gpu.Address, + Vendor: vendor, + Name: name, + Product: gpu.DeviceInfo.Product.Name, + Subsystem: gpu.DeviceInfo.Subsystem.Name, + Driver: gpu.DeviceInfo.Driver, + VramTotal: vramTotal, + VramUsed: vramUsed, + }) } return ret @@ -73,7 +170,11 @@ func GetMotherboardModel() string { if err != nil { return "" } - return strings.TrimSpace(string(bytes)) + + // Remove duplicate whitespaces + ret := strings.Join(strings.Fields(string(bytes)), " ") + + return ret } func GetMonitors() []Monitor { diff --git a/src/modules.go b/src/modules.go index e7d2ec3..5405cd5 100644 --- a/src/modules.go +++ b/src/modules.go @@ -200,8 +200,6 @@ func initializeModuleMap() { switch s { case "CPU_NUM": return strconv.Itoa(i + 1) - case "CPU_VENDOR": - return cpu.Vendor case "CPU_MODEL": return cpu.Model case "CPU_CORES": @@ -221,7 +219,7 @@ func initializeModuleMap() { RegisterModule(cpusModule) // GPUs module - gpusModule := StormfetchModule{stormfetchModuleConfig: stormfetchModuleConfig{Name: "gpus", Format: "%3GPU: %4$GPU_MODEL"}, Execute: func(sm StormfetchModule) string { + gpusModule := StormfetchModule{stormfetchModuleConfig: stormfetchModuleConfig{Name: "gpus", Format: "%3GPU: %4$GPU_VENDOR $GPU_NAME"}, Execute: func(sm StormfetchModule) string { hiddenGPUsInterface, _ := sm.GetData("hidden_gpus", make([]any, 0)) // Convert interface slices to string slices @@ -238,8 +236,20 @@ func initializeModuleMap() { switch s { case "GPU_NUM": return strconv.Itoa(i + 1) - case "GPU_MODEL": - return gpu + case "GPU_VENDOR": + return gpu.Vendor + case "GPU_NAME": + return gpu.Name + case "GPU_PRODUCT": + return gpu.Product + case "GPU_SUBSYSTEM": + return gpu.Subsystem + case "GPU_DRIVER": + return gpu.Driver + case "GPU_VRAM_TOTAL": + return gpu.VramTotal + case "GPU_VRAM_USED": + return gpu.VramUsed default: return "" } diff --git a/src/utils.go b/src/utils.go index e6841ec..6bba61d 100644 --- a/src/utils.go +++ b/src/utils.go @@ -5,6 +5,7 @@ import ( "math" "os" "os/exec" + "path" "regexp" "strings" ) @@ -58,6 +59,46 @@ func ReadKeyValueFile(filepath string) (map[string]string, error) { return ret, nil } +func fetchAmdGpuName(productId, revision string) (string, error) { + productId = strings.ToUpper(productId) + revision = strings.ToUpper(revision[2:]) + + // Get cache directory + cachedir, err := os.UserCacheDir() + if err != nil { + return "", err + } + + // Ensure amdgpu.ids file exists and download it if it doesn't + if _, err := os.Stat(path.Join(cachedir, "amdgpu.ids")); err != nil { + cmd := exec.Command("curl", "-o", path.Join(cachedir, "amdgpu.ids"), "https://gitlab.freedesktop.org/mesa/libdrm/-/raw/main/data/amdgpu.ids") + err = cmd.Run() + if err != nil { + return "", fmt.Errorf("Could not fetch amdgpu.ids using curl") + } + } + + // Read amdgpu.ids file + amdgpuIds, err := os.ReadFile(path.Join(cachedir, "amdgpu.ids")) + if err != nil { + return "", err + } + + // Parse read data and find GPU + for _, line := range strings.Split(string(amdgpuIds), "\n") { + if len(line) < 2 || line[0] == '#' { + continue + } + + fields := strings.Split(line, ",\t") + if fields[0] == productId && fields[1] == revision { + return strings.TrimPrefix(fields[2], "AMD "), nil + } + } + + return "", nil +} + func runCommand(command string, shell string) string { cmd := exec.Command(shell, "-c", command) workdir, err := os.Getwd()