Compare commits

...

11 Commits
6.0 ... master

17 changed files with 691 additions and 625 deletions

View File

@ -15,7 +15,7 @@ endif
build:
mkdir -p build
$(GO) build -ldflags "-w -X 'main.systemConfigDir=$(SYSCONFDIR)'" -o build/stormfetch stormfetch
cd src; $(GO) build -ldflags "-w -X 'main.systemConfigDir=$(SYSCONFDIR)'" -o ../build/stormfetch stormfetch
install: build/stormfetch config/
mkdir -p $(DESTDIR)$(BINDIR)
@ -23,14 +23,6 @@ install: build/stormfetch config/
cp build/stormfetch $(DESTDIR)$(BINDIR)/stormfetch
cp -r config/. $(DESTDIR)$(SYSCONFDIR)/stormfetch/
compress: build/stormfetch config/
mkdir -p stormfetch/$(BINDIR)
mkdir -p stormfetch/$(SYSCONFDIR)/stormfetch/
cp build/stormfetch stormfetch/$(BINDIR)/stormfetch
cp -r config/. stormfetch/$(SYSCONFDIR)/stormfetch/
tar --owner=root --group=root -czf stormfetch.tar.gz stormfetch
rm -r stormfetch
run: build/stormfetch
build/stormfetch

View File

@ -2,9 +2,6 @@
# Stormfetch
## A simple linux fetch program written in go and bash
### Developers:
- [EnumDev](https://gitlab.com/EnumDev)
### Project Information
Stormfetch is a program that can read your system's information and display it in the terminal along with the ASCII art of the Linux distribution you are running.
Stormfetch is still in beta, so distro compatibility is limited. If you would like to contribute ASCII art or add other compatibility features feel free to create a pull request or notify me through GitLab Issues.

View File

@ -2,6 +2,8 @@ distro_ascii: auto
fetch_script: auto
ansii_colors: []
force_config_ansii: false
dependency_warning: true
show_fs_type: true
hidden_partitions: []
# Hiding squashfs prevents snaps from showing up
hidden_filesystems: ["squashfs"]
hidden_gpus: []

View File

@ -1,9 +1,7 @@
source fetch_script_functions.sh
echo -e "${C3}Distribution: ${C4}${DISTRO_LONG_NAME} ($(uname -m))"
echo -e "${C3}Hostname: ${C4}$(cat /etc/hostname)"
echo -e "${C3}Kernel: ${C4}$(uname -s) $(uname -r)"
echo -e "${C3}Packages: ${C4}$(get_packages)"
echo -e "${C3}Packages: ${C4}${PACKAGES}"
echo -e "${C3}Shell: ${C4}${USER_SHELL}"
echo -e "${C3}Init: ${C4}${INIT_SYSTEM}"
echo -e "${C3}Libc: ${C4}${LIBC}"

View File

@ -1,40 +0,0 @@
command_exists() {
if [ -z "$1" ]; then
return 1
fi
if command -v "$1" &> /dev/null; then
return 0
else
return 1
fi
}
get_packages() {
ARRAY=()
if command_exists dpkg; then
ARRAY+=("$(dpkg-query -f '.\n' -W | wc -l) (dpkg)")
fi
if command_exists pacman; then
ARRAY+=("$(pacman -Q | wc -l) (pacman)")
fi
if command_exists rpm; then
ARRAY+=("$(rpm -qa | wc -l) (rpm)")
fi
if command_exists xbps-query; then
ARRAY+=("$(xbps-query -l | wc -l) (xbps)")
fi
if command_exists bpm; then
ARRAY+=("$(bpm list -c) (bpm)")
fi
if command_exists emerge; then
ARRAY+=("$(ls -l /var/db/pkg/* | wc -l) (emerge)")
fi
if command_exists flatpak; then
ARRAY+=("$(flatpak list | wc -l) (flatpak)")
fi
if command_exists snap; then
ARRAY+=("$(snap list | wc -l) (snap)")
fi
echo "${ARRAY[@]}"
unset ARRAY
}

View File

@ -3,19 +3,17 @@ module stormfetch
go 1.22
require (
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a
github.com/jackmordaunt/ghw v1.0.4
github.com/mitchellh/go-ps v1.0.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
github.com/go-ole/go-ole v1.2.6 // 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
github.com/moby/sys/mountinfo v0.7.1 // indirect
golang.org/x/sys v0.3.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)

View File

@ -1,5 +1,3 @@
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk=
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
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-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
@ -17,10 +15,7 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
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=
github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g=
github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

75
src/hardware.go Normal file
View File

@ -0,0 +1,75 @@
package main
import (
"fmt"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/jackmordaunt/ghw"
"os"
"os/exec"
"slices"
"strings"
)
func GetCPUModel() string {
cpu, err := ghw.CPU()
if err != nil {
return ""
}
if len(cpu.Processors) == 0 {
return ""
}
return cpu.Processors[0].Model
}
func GetCPUThreads() int {
cpu, err := ghw.CPU()
if err != nil {
return 0
}
return int(cpu.TotalThreads)
}
func GetGPUModels() (ret []string) {
cmd := exec.Command("sh", "-c", "lspci -v -m | grep 'VGA' -A6 | grep '^Device:'")
bytes, err := cmd.Output()
if err != nil {
return nil
}
for i, gpu := range strings.Split(string(bytes), "\n") {
if slices.Contains(config.HiddenGPUS, i+1) {
continue
}
if gpu == "" {
continue
}
gpu = strings.TrimPrefix(strings.TrimSpace(gpu), "Device:\t")
ret = append(ret, gpu)
}
return ret
}
func GetMotherboardModel() string {
bytes, err := os.ReadFile("/sys/devices/virtual/dmi/id/board_name")
if err != nil {
return ""
}
return strings.TrimSpace(string(bytes))
}
func GetMonitorResolution() []string {
var monitors []string
if GetDisplayProtocol() != "" {
err := glfw.Init()
if err != nil {
panic(err)
}
for _, monitor := range glfw.GetMonitors() {
mode := monitor.GetVideoMode()
monitors = append(monitors, fmt.Sprintf("%dx%d %dHz", mode.Width, mode.Height, mode.RefreshRate))
}
defer glfw.Terminate()
}
return monitors
}

View File

@ -22,30 +22,30 @@ var fetchScriptPath = ""
var TimeTaken = false
var config = StormfetchConfig{
Ascii: "auto",
FetchScript: "auto",
AnsiiColors: make([]int, 0),
ForceConfigAnsii: false,
DependencyWarning: true,
ShowFSType: false,
HiddenGPUS: make([]int, 0),
Ascii: "auto",
FetchScript: "auto",
AnsiiColors: make([]int, 0),
ForceConfigAnsii: false,
ShowFSType: false,
HiddenPartitions: make([]string, 0),
HiddenGPUS: make([]int, 0),
}
type StormfetchConfig struct {
Ascii string `yaml:"distro_ascii"`
DistroName string `yaml:"distro_name"`
FetchScript string `yaml:"fetch_script"`
AnsiiColors []int `yaml:"ansii_colors"`
ForceConfigAnsii bool `yaml:"force_config_ansii"`
DependencyWarning bool `yaml:"dependency_warning"`
ShowFSType bool `yaml:"show_fs_type"`
HiddenGPUS []int `yaml:"hidden_gpus"`
Ascii string `yaml:"distro_ascii"`
DistroName string `yaml:"distro_name"`
FetchScript string `yaml:"fetch_script"`
AnsiiColors []int `yaml:"ansii_colors"`
ForceConfigAnsii bool `yaml:"force_config_ansii"`
ShowFSType bool `yaml:"show_fs_type"`
HiddenPartitions []string `yaml:"hidden_partitions"`
HiddenFilesystems []string `yaml:"hidden_filesystems"`
HiddenGPUS []int `yaml:"hidden_gpus"`
}
func main() {
readConfig()
readFlags()
checkDependencies()
runStormfetch()
}
@ -95,26 +95,6 @@ func readFlags() {
flag.Parse()
}
func checkDependencies() {
// Show Dependency warning if enabled
if config.DependencyWarning {
var dependencies []string
var missing []string
for _, depend := range dependencies {
if _, err := os.Stat(path.Join("/usr/bin/", depend)); err != nil {
missing = append(missing, depend)
}
}
if len(missing) != 0 {
fmt.Println("[WARNING] Stormfetch functionality may be limited due to the following dependencies not being installed:")
for _, depend := range missing {
fmt.Println(depend)
}
fmt.Println("You can disable this warning through your stormfetch config")
}
}
}
func SetupFetchEnv(showTimeTaken bool) []string {
var env = make(map[string]string)
setVariable := func(key string, setter func() string) {
@ -125,11 +105,12 @@ func SetupFetchEnv(showTimeTaken bool) []string {
fmt.Println(fmt.Sprintf("Setting '%s' took %d milliseconds", key, end-start))
}
}
setVariable("DISTRO_LONG_NAME", func() string { return getDistroInfo().LongName })
setVariable("DISTRO_SHORT_NAME", func() string { return getDistroInfo().ShortName })
setVariable("CPU_MODEL", func() string { return getCPUName() })
setVariable("MOTHERBOARD", func() string { return getMotherboardModel() })
setVariable("CPU_THREADS", func() string { return strconv.Itoa(getCPUThreads()) })
setVariable("PACKAGES", func() string { return GetInstalledPackages() })
setVariable("DISTRO_LONG_NAME", func() string { return GetDistroInfo().LongName })
setVariable("DISTRO_SHORT_NAME", func() string { return GetDistroInfo().ShortName })
setVariable("CPU_MODEL", func() string { return GetCPUModel() })
setVariable("MOTHERBOARD", func() string { return GetMotherboardModel() })
setVariable("CPU_THREADS", func() string { return strconv.Itoa(GetCPUThreads()) })
start := time.Now().UnixMilli()
memory := GetMemoryInfo()
end := time.Now().UnixMilli()
@ -142,7 +123,7 @@ func SetupFetchEnv(showTimeTaken bool) []string {
env["MEM_FREE"] = strconv.Itoa(memory.MemAvailable)
}
start = time.Now().UnixMilli()
partitions := getMountedPartitions()
partitions := GetMountedPartitions(config.HiddenPartitions, config.HiddenFilesystems)
end = time.Now().UnixMilli()
if showTimeTaken {
fmt.Println(fmt.Sprintf("Setting '%s' took %d milliseconds", "PARTITION_*", end-start))
@ -155,8 +136,8 @@ func SetupFetchEnv(showTimeTaken bool) []string {
if part.Label != "" {
env["PARTITION"+strconv.Itoa(i+1)+"_LABEL"] = part.Label
}
if part.Type != "" && config.ShowFSType {
env["PARTITION"+strconv.Itoa(i+1)+"_TYPE"] = part.Type
if part.FileystemType != "" && config.ShowFSType {
env["PARTITION"+strconv.Itoa(i+1)+"_TYPE"] = part.FileystemType
}
env["PARTITION"+strconv.Itoa(i+1)+"_TOTAL_SIZE"] = FormatBytes(part.TotalSize)
env["PARTITION"+strconv.Itoa(i+1)+"_USED_SIZE"] = FormatBytes(part.UsedSize)
@ -170,7 +151,7 @@ func SetupFetchEnv(showTimeTaken bool) []string {
setVariable("INIT_SYSTEM", func() string { return GetInitSystem() })
setVariable("LOCAL_IPV4", func() string { return GetLocalIP() })
start = time.Now().UnixMilli()
monitors := getMonitorResolution()
monitors := GetMonitorResolution()
end = time.Now().UnixMilli()
if showTimeTaken {
fmt.Println(fmt.Sprintf("Setting '%s' took %d milliseconds", "MONITOR_*", end-start))
@ -182,7 +163,7 @@ func SetupFetchEnv(showTimeTaken bool) []string {
}
}
start = time.Now().UnixMilli()
gpus := getGPUNames()
gpus := GetGPUModels()
end = time.Now().UnixMilli()
if showTimeTaken {
fmt.Println(fmt.Sprintf("Setting '%s' took %d milliseconds", "GPU_*", end-start))
@ -220,7 +201,7 @@ func runStormfetch() {
}
}
setColorMap()
ascii := getDistroAsciiArt()
ascii := GetDistroAsciiArt()
if strings.HasPrefix(ascii, "#/") {
firstLine := strings.Split(ascii, "\n")[0]
if !config.ForceConfigAnsii {

57
src/memory.go Normal file
View File

@ -0,0 +1,57 @@
package main
import (
"bufio"
"os"
"strconv"
"strings"
)
type Memory struct {
MemTotal int
MemFree int
MemAvailable int
}
func GetMemoryInfo() *Memory {
toInt := func(raw string) int {
if raw == "" {
return 0
}
res, err := strconv.Atoi(raw)
if err != nil {
panic(err)
}
return res
}
parseLine := func(raw string) (key string, value int) {
text := strings.ReplaceAll(raw[:len(raw)-2], " ", "")
keyValue := strings.Split(text, ":")
return keyValue[0], toInt(keyValue[1])
}
if _, err := os.Stat("/proc/meminfo"); err != nil {
return nil
}
file, err := os.Open("/proc/meminfo")
if err != nil {
panic(err)
}
defer file.Close()
bufio.NewScanner(file)
scanner := bufio.NewScanner(file)
res := Memory{}
for scanner.Scan() {
key, value := parseLine(scanner.Text())
switch key {
case "MemTotal":
res.MemTotal = value / 1024
case "MemFree":
res.MemFree = value / 1024
case "MemAvailable":
res.MemAvailable = value / 1024
}
}
return &res
}

15
src/network.go Normal file
View File

@ -0,0 +1,15 @@
package main
import "net"
func GetLocalIP() string {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return ""
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP.String()
}

119
src/partitions.go Normal file
View File

@ -0,0 +1,119 @@
package main
import (
"os"
"path/filepath"
"slices"
"strings"
"syscall"
)
type partition struct {
Device string
MountPoint string
Label string
FileystemType string
TotalSize uint64
UsedSize uint64
FreeSize uint64
}
func GetMountedPartitions(hiddenPartitions, hiddenFilesystems []string) []partition {
// Get all filesystem and partition labels
fslabels, err := os.ReadDir("/dev/disk/by-label")
if err != nil && !os.IsNotExist(err) {
return nil
}
partlabels, err := os.ReadDir("/dev/disk/by-partlabel")
if err != nil && !os.IsNotExist(err) {
return nil
}
labels := make(map[string]string)
for _, entry := range partlabels {
link, err := filepath.EvalSymlinks(filepath.Join("/dev/disk/by-partlabel/", entry.Name()))
if err != nil {
continue
}
labels[link] = entry.Name()
}
for _, entry := range fslabels {
link, err := filepath.EvalSymlinks(filepath.Join("/dev/disk/by-label/", entry.Name()))
if err != nil {
continue
}
labels[link] = entry.Name()
}
// Get all mounted partitions
file, err := os.ReadFile("/proc/mounts")
if err != nil {
return nil
}
var partitions []partition
for _, entry := range strings.Split(string(file), "\n") {
fields := strings.Fields(entry)
if entry == "" {
continue
}
// Skip virtual partitions not under /dev
if !strings.HasPrefix(fields[0], "/dev") {
continue
}
// Skip partition if explicitly hidden
if slices.Contains(hiddenPartitions, fields[0]) {
continue
}
// Skip filesystem if explicitely hidden
if slices.Contains(hiddenFilesystems, fields[2]) {
continue
}
p := partition{
fields[0],
fields[1],
"",
fields[2],
0,
0,
0,
}
// Skip already added partitions
skip := false
for _, part := range partitions {
if part.Device == p.Device {
skip = true
}
}
if skip {
continue
}
// Set partition label if available
if value, ok := labels[p.Device]; ok {
p.Label = value
}
// Get partition total, used and free space
buf := new(syscall.Statfs_t)
err = syscall.Statfs(p.MountPoint, buf)
if err != nil {
continue
}
totalBlocks := buf.Blocks
freeBlocks := buf.Bfree
usedBlocks := totalBlocks - freeBlocks
blockSize := uint64(buf.Bsize)
p.TotalSize = totalBlocks * blockSize
p.FreeSize = freeBlocks * blockSize
p.UsedSize = usedBlocks * blockSize
partitions = append(partitions, p)
}
return partitions
}

53
src/pms.go Normal file
View File

@ -0,0 +1,53 @@
package main
import (
"fmt"
"os/exec"
"strings"
)
type PackageManager struct {
Name string
ExecutableName string
PackageListCommand string
}
var PackageManagers = []PackageManager{
{Name: "dpkg", ExecutableName: "dpkg", PackageListCommand: "dpkg-query -f '${Package}\\n' -W"},
{Name: "pacman", ExecutableName: "pacman", PackageListCommand: "pacman -Q"},
{Name: "rpm", ExecutableName: "rpm", PackageListCommand: "rpm -qa"},
{Name: "xbps", ExecutableName: "xbps-query", PackageListCommand: "xbps-query -l"},
{Name: "bpm", ExecutableName: "bpm", PackageListCommand: "bpm list -n"},
{Name: "portage", ExecutableName: "emerge", PackageListCommand: "find /var/db/pkg/*/ -mindepth 1 -maxdepth 1"},
{Name: "flatpak", ExecutableName: "flatpak", PackageListCommand: "flatpak list"},
{Name: "snap", ExecutableName: "snap", PackageListCommand: "snap list | tail +2"},
}
func (pm *PackageManager) CountPackages() int {
// Return 0 if package manager is not found
if _, err := exec.LookPath(pm.ExecutableName); err != nil {
return 0
}
output, err := exec.Command("/bin/sh", "-c", pm.PackageListCommand).Output()
if err != nil {
return 0
}
return strings.Count(string(output), "\n")
}
func GetInstalledPackages() (ret string) {
for _, pm := range PackageManagers {
count := pm.CountPackages()
if count > 0 {
if ret == "" {
ret += fmt.Sprintf("%d (%s)", count, pm.Name)
} else {
ret += fmt.Sprintf(" %d (%s)", count, pm.Name)
}
}
}
return ret
}

153
src/system.go Normal file
View File

@ -0,0 +1,153 @@
package main
import (
"github.com/mitchellh/go-ps"
"os"
"os/exec"
"path"
"strings"
)
type DistroInfo struct {
ID string
LongName string
ShortName string
}
func GetDistroInfo() DistroInfo {
info := DistroInfo{
ID: "unknown",
LongName: "Unknown",
ShortName: "Unknown",
}
if strings.TrimSpace(config.DistroName) != "" {
info.LongName = strings.TrimSpace(config.DistroName)
info.ShortName = strings.TrimSpace(config.DistroName)
}
var releaseMap = make(map[string]string)
if _, err := os.Stat("/etc/os-release"); err == nil {
releaseMap, err = ReadKeyValueFile("/etc/os-release")
if err != nil {
return info
}
}
if id, ok := releaseMap["ID"]; ok {
info.ID = id
}
if longName, ok := releaseMap["PRETTY_NAME"]; ok && info.LongName == "Unknown" {
info.LongName = longName
}
if shortName, ok := releaseMap["NAME"]; ok && info.ShortName == "Unknown" {
info.ShortName = shortName
}
return info
}
func GetDistroAsciiArt() string {
defaultAscii :=
` .--.
|o_o |
|:_/ |
// \ \
(| | )
/'\_ _/'\
\___)=(___/ `
var id string
if config.Ascii == "auto" {
id = GetDistroInfo().ID
} else {
id = config.Ascii
}
userConfDir, err := os.UserConfigDir()
if err != nil {
if _, err := os.Stat(path.Join(systemConfigDir, "stormfetch/ascii/", id)); err == nil {
bytes, err := os.ReadFile(path.Join(systemConfigDir, "stormfetch/ascii/", id))
if err != nil {
return defaultAscii
}
return string(bytes)
} else {
return defaultAscii
}
}
if _, err := os.Stat(path.Join(userConfDir, "stormfetch/ascii/", id)); err == nil {
bytes, err := os.ReadFile(path.Join(userConfDir, "stormfetch/ascii/", id))
if err != nil {
return defaultAscii
}
return string(bytes)
} else if _, err := os.Stat(path.Join(systemConfigDir, "stormfetch/ascii/", id)); err == nil {
bytes, err := os.ReadFile(path.Join(systemConfigDir, "stormfetch/ascii/", id))
if err != nil {
return defaultAscii
}
return strings.TrimRight(string(bytes), "\n\t ")
} else {
return defaultAscii
}
}
func GetInitSystem() string {
runCommand := func(command string) string {
cmd := exec.Command("/bin/bash", "-c", command)
workdir, err := os.Getwd()
if err != nil {
return ""
}
cmd.Dir = workdir
cmd.Env = os.Environ()
out, err := cmd.Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
process, err := ps.FindProcess(1)
if err != nil {
return ""
}
// Special cases
// OpenRC check
if _, err := os.Stat("/usr/sbin/openrc"); err == nil {
return "OpenRC " + runCommand("openrc --version | awk '{print $3}'")
}
// Default PID 1 process name checking
switch process.Executable() {
case "systemd":
return "Systemd " + runCommand("systemctl --version | head -n1 | awk '{print $2}'")
case "runit":
return "Runit"
case "dinit":
return "Dinit " + runCommand("dinit --version | head -n1 | awk '{print substr($3, 1, length($3)-1)}'")
case "enit":
return "Enit " + runCommand("enit --version | awk '{print $3}'")
default:
return process.Executable()
}
}
func GetLibc() string {
checkLibcOutput, err := exec.Command("ldd", "/usr/bin/ls").Output()
if err != nil {
return "Unknown"
}
if strings.Contains(string(checkLibcOutput), "ld-musl") {
// Using Musl Libc
output, _ := exec.Command("ldd").CombinedOutput()
return "Musl " + strings.TrimPrefix(strings.Split(strings.TrimSpace(string(output)), "\n")[1], "Version ")
} else {
// Using Glibc
cmd := exec.Command("ldd", "--version")
output, err := cmd.Output()
if err != nil {
return "Glibc"
}
outputSplit := strings.Split(strings.Split(strings.TrimSpace(string(output)), "\n")[0], " ")
ver := outputSplit[len(outputSplit)-1]
return "Glibc " + ver
}
}

127
src/user.go Normal file
View File

@ -0,0 +1,127 @@
package main
import (
"github.com/mitchellh/go-ps"
"log"
"os"
"os/exec"
"path/filepath"
"slices"
"strconv"
"strings"
)
func GetShell() string {
runCommand := func(command string) string {
cmd := exec.Command("/bin/bash", "-c", command)
workdir, err := os.Getwd()
if err != nil {
return ""
}
cmd.Dir = workdir
cmd.Env = os.Environ()
out, err := cmd.Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
file, err := os.ReadFile("/etc/passwd")
if err != nil {
return ""
}
str := string(file)
shell := ""
for _, line := range strings.Split(str, "\n") {
if strings.TrimSpace(line) == "" {
continue
}
userInfo := strings.Split(line, ":")
if userInfo[2] == strconv.Itoa(os.Getuid()) {
shell = userInfo[6]
}
}
shellName := filepath.Base(shell)
switch shellName {
case "dash":
return "Dash"
case "bash":
return "Bash " + runCommand("echo $BASH_VERSION")
case "zsh":
return "Zsh " + runCommand("$SHELL --version | awk '{print $2}'")
case "fish":
return "Fish " + runCommand("$SHELL --version | awk '{print $3}'")
case "nu":
return "Nushell " + runCommand("$SHELL --version")
default:
return "Unknown"
}
}
func GetDEWM() string {
processes, err := ps.Processes()
if err != nil {
log.Fatalf("Error: could not get processes: %s", err)
}
var executables []string
for _, process := range processes {
executables = append(executables, process.Executable())
}
processExists := func(process string) bool {
return slices.Contains(executables, process)
}
runCommand := func(command string) string {
cmd := exec.Command("/bin/bash", "-c", command)
workdir, err := os.Getwd()
if err != nil {
return ""
}
cmd.Dir = workdir
cmd.Env = os.Environ()
out, err := cmd.Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
if processExists("plasmashell") {
return "KDE Plasma " + runCommand("plasmashell --version | awk '{print $2}'")
} else if processExists("gnome-session") {
return "Gnome " + runCommand("gnome-shell --version | awk '{print $3}'")
} else if processExists("xfce4-session") {
return "XFCE " + runCommand("xfce4-session --version | head -n1 | awk '{print $2}'")
} else if processExists("cinnamon") {
return "Cinnamon " + runCommand("cinnamon --version | awk '{print $3}'")
} else if processExists("mate-panel") {
return "MATE " + runCommand("mate-about --version | awk '{print $4}'")
} else if processExists("lxsession") {
return "LXDE"
} else if processExists("i3") || processExists("i3-with-shmlog") {
return "i3 " + runCommand("i3 --version | awk '{print $3}'")
} else if processExists("sway") {
if runCommand("sway --version | awk '{print $1}'") == "swayfx" {
return "SwayFX " + runCommand("sway --version | awk '{print $3}'")
} else {
return "Sway " + runCommand("sway --version | awk '{print $3}'")
}
} else if processExists("bspwm") {
return "Bspwm " + runCommand("bspwm -v")
} else if processExists("Hyprland") {
return "Hyprland " + runCommand("hyprctl version | sed -n 3p | awk '{print $2}' | tr -d 'v,'")
} else if processExists("icewm-session") {
return "IceWM " + runCommand("icewm --version | awk '{print $2}'")
}
return ""
}
func GetDisplayProtocol() string {
protocol := os.Getenv("XDG_SESSION_TYPE")
if protocol == "x11" {
return "X11"
} else if protocol == "wayland" {
return "Wayland"
}
return ""
}

58
src/utils.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"fmt"
"math"
"os"
"regexp"
"strings"
)
func FormatBytes(bytes uint64) string {
var suffixes [6]string
suffixes[0] = "B"
suffixes[1] = "KiB"
suffixes[2] = "MiB"
suffixes[3] = "GiB"
suffixes[4] = "TiB"
suffixes[5] = "PiB"
bf := float64(bytes)
for _, unit := range suffixes {
if math.Abs(bf) < 1024.0 {
return fmt.Sprintf("%3.1f %s", bf, unit)
}
bf /= 1024.0
}
return fmt.Sprintf("%.1fYiB", bf)
}
func StripAnsii(str string) string {
const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
var re = regexp.MustCompile(ansi)
return re.ReplaceAllString(str, "")
}
func ReadKeyValueFile(filepath string) (map[string]string, error) {
ret := make(map[string]string)
if _, err := os.Stat(filepath); err != nil {
return nil, err
}
bytes, err := os.ReadFile(filepath)
if err != nil {
return nil, err
}
str := string(bytes)
lines := strings.Split(str, "\n")
for _, line := range lines {
if len(strings.Split(line, "=")) >= 2 {
key := strings.SplitN(line, "=", 2)[0]
value := strings.SplitN(line, "=", 2)[1]
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
value = value[1 : len(value)-1]
}
ret[key] = value
}
}
return ret, nil
}

514
utils.go
View File

@ -1,514 +0,0 @@
package main
import (
"bufio"
"fmt"
"github.com/go-gl/glfw/v3.3/glfw"
"github.com/jackmordaunt/ghw"
"github.com/mitchellh/go-ps"
"github.com/moby/sys/mountinfo"
"log"
"math"
"net"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"syscall"
)
type DistroInfo struct {
ID string
LongName string
ShortName string
}
func getDistroInfo() DistroInfo {
info := DistroInfo{
ID: "unknown",
LongName: "Unknown",
ShortName: "Unknown",
}
if strings.TrimSpace(config.DistroName) != "" {
info.LongName = strings.TrimSpace(config.DistroName)
info.ShortName = strings.TrimSpace(config.DistroName)
}
var releaseMap = make(map[string]string)
if _, err := os.Stat("/etc/os-release"); err == nil {
releaseMap, err = ReadKeyValueFile("/etc/os-release")
if err != nil {
return info
}
}
if id, ok := releaseMap["ID"]; ok {
info.ID = id
}
if longName, ok := releaseMap["PRETTY_NAME"]; ok && info.LongName == "Unknown" {
info.LongName = longName
}
if shortName, ok := releaseMap["NAME"]; ok && info.ShortName == "Unknown" {
info.ShortName = shortName
}
return info
}
func getDistroAsciiArt() string {
defaultAscii :=
` .--.
|o_o |
|:_/ |
// \ \
(| | )
/'\_ _/'\
\___)=(___/ `
var id string
if config.Ascii == "auto" {
id = getDistroInfo().ID
} else {
id = config.Ascii
}
userConfDir, err := os.UserConfigDir()
if err != nil {
if _, err := os.Stat(path.Join(systemConfigDir, "stormfetch/ascii/", id)); err == nil {
bytes, err := os.ReadFile(path.Join(systemConfigDir, "stormfetch/ascii/", id))
if err != nil {
return defaultAscii
}
return string(bytes)
} else {
return defaultAscii
}
}
if _, err := os.Stat(path.Join(userConfDir, "stormfetch/ascii/", id)); err == nil {
bytes, err := os.ReadFile(path.Join(userConfDir, "stormfetch/ascii/", id))
if err != nil {
return defaultAscii
}
return string(bytes)
} else if _, err := os.Stat(path.Join(systemConfigDir, "stormfetch/ascii/", id)); err == nil {
bytes, err := os.ReadFile(path.Join(systemConfigDir, "stormfetch/ascii/", id))
if err != nil {
return defaultAscii
}
return strings.TrimRight(string(bytes), "\n\t ")
} else {
return defaultAscii
}
}
func getMotherboardModel() string {
bytes, err := os.ReadFile("/sys/devices/virtual/dmi/id/board_name")
if err != nil {
return ""
}
return strings.TrimSpace(string(bytes))
}
func getCPUName() string {
cpu, err := ghw.CPU()
if err != nil {
return ""
}
if len(cpu.Processors) == 0 {
return ""
}
return cpu.Processors[0].Model
}
func getCPUThreads() int {
cpu, err := ghw.CPU()
if err != nil {
return 0
}
return int(cpu.TotalThreads)
}
func getGPUNames() []string {
var ret []string
cmd := exec.Command("/bin/bash", "-c", "lspci -v -m | grep 'VGA' -A6 | grep '^Device:' | sed 's/^Device://' | awk '{$1=$1};1'")
bytes, err := cmd.Output()
if err != nil {
return nil
}
for _, name := range strings.Split(string(bytes), "\n") {
name = strings.TrimSpace(name)
if name == "" {
continue
}
ret = append(ret, name)
}
return ret
}
type Memory struct {
MemTotal int
MemFree int
MemAvailable int
}
func GetMemoryInfo() *Memory {
toInt := func(raw string) int {
if raw == "" {
return 0
}
res, err := strconv.Atoi(raw)
if err != nil {
panic(err)
}
return res
}
parseLine := func(raw string) (key string, value int) {
text := strings.ReplaceAll(raw[:len(raw)-2], " ", "")
keyValue := strings.Split(text, ":")
return keyValue[0], toInt(keyValue[1])
}
if _, err := os.Stat("/proc/meminfo"); err != nil {
return nil
}
file, err := os.Open("/proc/meminfo")
if err != nil {
panic(err)
}
defer file.Close()
bufio.NewScanner(file)
scanner := bufio.NewScanner(file)
res := Memory{}
for scanner.Scan() {
key, value := parseLine(scanner.Text())
switch key {
case "MemTotal":
res.MemTotal = value / 1024
case "MemFree":
res.MemFree = value / 1024
case "MemAvailable":
res.MemAvailable = value / 1024
}
}
return &res
}
func GetShell() string {
runCommand := func(command string) string {
cmd := exec.Command("/bin/bash", "-c", command)
workdir, err := os.Getwd()
if err != nil {
return ""
}
cmd.Dir = workdir
cmd.Env = os.Environ()
out, err := cmd.Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
file, err := os.ReadFile("/etc/passwd")
if err != nil {
return ""
}
str := string(file)
shell := ""
for _, line := range strings.Split(str, "\n") {
if strings.TrimSpace(line) == "" {
continue
}
userInfo := strings.Split(line, ":")
if userInfo[2] == strconv.Itoa(os.Getuid()) {
shell = userInfo[6]
}
}
shellName := filepath.Base(shell)
switch shellName {
case "dash":
return "Dash"
case "bash":
return "Bash " + runCommand("echo $BASH_VERSION")
case "zsh":
return "Zsh " + runCommand("$SHELL --version | awk '{print $2}'")
case "fish":
return "Fish " + runCommand("$SHELL --version | awk '{print $3}'")
case "nu":
return "Nushell " + runCommand("$SHELL --version")
default:
return "Unknown"
}
}
func GetDEWM() string {
processes, err := ps.Processes()
if err != nil {
log.Fatalf("Error: could not get processes: %s", err)
}
var executables []string
for _, process := range processes {
executables = append(executables, process.Executable())
}
processExists := func(process string) bool {
return slices.Contains(executables, process)
}
runCommand := func(command string) string {
cmd := exec.Command("/bin/bash", "-c", command)
workdir, err := os.Getwd()
if err != nil {
return ""
}
cmd.Dir = workdir
cmd.Env = os.Environ()
out, err := cmd.Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
if processExists("plasmashell") {
return "KDE Plasma " + runCommand("plasmashell --version | awk '{print $2}'")
} else if processExists("gnome-session") {
return "Gnome " + runCommand("gnome-shell --version | awk '{print $3}'")
} else if processExists("xfce4-session") {
return "XFCE " + runCommand("xfce4-session --version | head -n1 | awk '{print $2}'")
} else if processExists("cinnamon") {
return "Cinnamon " + runCommand("cinnamon --version | awk '{print $3}'")
} else if processExists("mate-panel") {
return "MATE " + runCommand("mate-about --version | awk '{print $4}'")
} else if processExists("lxsession") {
return "LXDE"
} else if processExists("i3") {
return "i3 " + runCommand("i3 --version | awk '{print $3}'")
} else if processExists("sway") {
if runCommand("sway --version | awk '{print $1}'") == "swayfx" {
return "SwayFX " + runCommand("sway --version | awk '{print $3}'")
} else {
return "Sway " + runCommand("sway --version | awk '{print $3}'")
}
} else if processExists("bspwm") {
return "Bspwm " + runCommand("bspwm -v")
} else if processExists("Hyprland") {
return "Hyprland " + runCommand("hyprctl version | sed -n 3p | awk '{print $2}' | tr -d 'v,'")
} else if processExists("icewm-session") {
return "IceWM " + runCommand("icewm --version | awk '{print $2}'")
}
return ""
}
func GetDisplayProtocol() string {
protocol := os.Getenv("XDG_SESSION_TYPE")
if protocol == "x11" {
return "X11"
} else if protocol == "wayland" {
return "Wayland"
}
return ""
}
func getMonitorResolution() []string {
var monitors []string
if GetDisplayProtocol() != "" {
err := glfw.Init()
if err != nil {
panic(err)
}
for _, monitor := range glfw.GetMonitors() {
mode := monitor.GetVideoMode()
monitors = append(monitors, fmt.Sprintf("%dx%d %dHz", mode.Width, mode.Height, mode.RefreshRate))
}
defer glfw.Terminate()
}
return monitors
}
type partition struct {
Device string
MountPoint string
Label string
Type string
TotalSize uint64
UsedSize uint64
FreeSize uint64
}
func getMountedPartitions() []partition {
mounts, err := mountinfo.GetMounts(func(info *mountinfo.Info) (skip, stop bool) {
return !strings.HasPrefix(info.Source, "/dev/"), false
})
fslabels, err := os.ReadDir("/dev/disk/by-label")
if err != nil && !os.IsNotExist(err) {
return nil
}
partlabels, err := os.ReadDir("/dev/disk/by-partlabel")
if err != nil && !os.IsNotExist(err) {
return nil
}
labels := make(map[string]string)
for _, entry := range partlabels {
link, err := filepath.EvalSymlinks(filepath.Join("/dev/disk/by-partlabel/", entry.Name()))
if err != nil {
continue
}
labels[link] = entry.Name()
}
for _, entry := range fslabels {
link, err := filepath.EvalSymlinks(filepath.Join("/dev/disk/by-label/", entry.Name()))
if err != nil {
continue
}
labels[link] = entry.Name()
}
var partitions []partition
for _, entry := range mounts {
p := partition{
entry.Source,
entry.Mountpoint,
"",
entry.FSType,
0,
0,
0,
}
skip := false
for _, part := range partitions {
if part.Device == p.Device {
skip = true
}
}
if skip {
continue
}
if value, ok := labels[entry.Source]; ok {
p.Label = value
}
buf := new(syscall.Statfs_t)
err = syscall.Statfs(p.MountPoint, buf)
if err != nil {
continue
}
totalBlocks := buf.Blocks
freeBlocks := buf.Bfree
usedBlocks := totalBlocks - freeBlocks
blockSize := uint64(buf.Bsize)
p.TotalSize = totalBlocks * blockSize
p.FreeSize = freeBlocks * blockSize
p.UsedSize = usedBlocks * blockSize
partitions = append(partitions, p)
}
return partitions
}
func GetInitSystem() string {
runCommand := func(command string) string {
cmd := exec.Command("/bin/bash", "-c", command)
workdir, err := os.Getwd()
if err != nil {
return ""
}
cmd.Dir = workdir
cmd.Env = os.Environ()
out, err := cmd.Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
link, err := os.Readlink("/sbin/init")
if err != nil {
return "Unknown"
}
if path.Base(link) == "systemd" {
return "Systemd " + runCommand("systemctl --version | head -1 | awk '{print $2}'")
} else if path.Base(link) == "openrc-init" {
return "OpenRC " + runCommand("openrc --version | awk '{print $3}'")
} else if path.Base(link) == "runit-init" {
return "Runit"
} else {
return "Unknown"
}
}
func GetLibc() string {
cmd := exec.Command("/bin/bash", "-c", "find /usr/lib64/ -maxdepth 1 -name 'ld-*' | grep musl")
if err := cmd.Run(); err != nil {
cmd = exec.Command("/bin/bash", "-c", "ldd --version | head -1 | cut -d' ' -f4")
bytes, err := cmd.Output()
if err != nil {
return "Glibc"
}
return "Glibc " + strings.TrimSpace(string(bytes))
}
cmd = exec.Command("/bin/bash", "-c", "ldd 2>&1 | grep 'Version' | cut -d' ' -f2")
bytes, err := cmd.Output()
if err != nil {
return "Musl"
}
return "Musl " + strings.TrimSpace(string(bytes))
}
func GetLocalIP() string {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return ""
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP.String()
}
func FormatBytes(bytes uint64) string {
var suffixes [6]string
suffixes[0] = "B"
suffixes[1] = "KiB"
suffixes[2] = "MiB"
suffixes[3] = "GiB"
suffixes[4] = "TiB"
suffixes[5] = "PiB"
bf := float64(bytes)
for _, unit := range suffixes {
if math.Abs(bf) < 1024.0 {
return fmt.Sprintf("%3.1f %s", bf, unit)
}
bf /= 1024.0
}
return fmt.Sprintf("%.1fYiB", bf)
}
func StripAnsii(str string) string {
const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
var re = regexp.MustCompile(ansi)
return re.ReplaceAllString(str, "")
}
func ReadKeyValueFile(filepath string) (map[string]string, error) {
ret := make(map[string]string)
if _, err := os.Stat(filepath); err != nil {
return nil, err
}
bytes, err := os.ReadFile(filepath)
if err != nil {
return nil, err
}
str := string(bytes)
lines := strings.Split(str, "\n")
for _, line := range lines {
if len(strings.Split(line, "=")) >= 2 {
key := strings.SplitN(line, "=", 2)[0]
value := strings.SplitN(line, "=", 2)[1]
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
value = value[1 : len(value)-1]
}
ret[key] = value
}
}
return ret, nil
}