diff --git a/main.go b/main.go new file mode 100644 index 0000000..1c43895 --- /dev/null +++ b/main.go @@ -0,0 +1,448 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "runtime" + "strconv" + "strings" + "syscall" +) + +var cube = []string{ + " +------+", + " / /|", + " +------+ |", + " | | +", + " | |/", + " +------+", +} + +func main() { + + info := collectAll() + + values := make([]string, 0, len(info)) + keys := make([]string, 0, len(info)) + for _, kv := range info { + values = append(values, strings.ToLower(kv.v)) + keys = append(keys, strings.ToLower(kv.k)) + } + + printAligned(values, keys, cube) +} + +type pair struct{ k, v string } + +func collectAll() []pair { + var out []pair + + distro := readPrettyOSRelease("/etc/os-release") + if distro == "" { + distro = runtime.GOOS + } + out = append(out, pair{"os", distro}) + + out = append(out, pair{"arch", runtime.GOARCH}) + + out = append(out, pair{"kernel", getUname()}) + + cpuModel, cpuHz := detectCPU() + if cpuHz > 0 { + + if cpuHz >= 1e9 { + ghz := float64(cpuHz) / 1e9 + out = append(out, pair{"cpu", fmt.Sprintf("%s-%.2f ghz", cpuModel, ghz)}) + } else { + out = append(out, pair{"cpu", fmt.Sprintf("%s-%d mhz", cpuModel, cpuHz/1e6)}) + } + } else { + out = append(out, pair{"cpu", cpuModel}) + } + + memTotal, memAvail, swapTotal, swapFree := parseMemInfo() + if memTotal > 0 { + memUsed := memTotal - memAvail + out = append(out, pair{"memory", fmt.Sprintf("%s / %s", hr(memUsed), hr(memTotal))}) + } + if swapTotal > 0 { + swapUsed := swapTotal - swapFree + out = append(out, pair{"swap", fmt.Sprintf("%s / %s", hr(swapUsed), hr(swapTotal))}) + } + + out = append(out, pair{"desktop", detectDesktop()}) + + used, total, fstype := getStorageAndType("/") + out = append(out, pair{"storage", fmt.Sprintf("%s / %s %s", hr(used), hr(total), fstype)}) + + return out +} + +func readPrettyOSRelease(path string) string { + if b, err := os.ReadFile(path); err == nil { + for _, ln := range bytes.Split(b, []byte{'\n'}) { + if bytes.HasPrefix(ln, []byte("PRETTY_NAME=")) { + val := strings.Trim(string(bytes.TrimPrefix(ln, []byte("PRETTY_NAME="))), `"`) + return strings.TrimSpace(val) + } + } + + name := parseKey(bytesToLower(b), "name=") + version := parseKey(bytesToLower(b), "version=") + if name != "" && version != "" { + return strings.TrimSpace(name + " " + version) + } + if name != "" { + return name + } + } + return "" +} + +func parseKey(b []byte, key string) string { + for _, ln := range bytes.Split(b, []byte{'\n'}) { + if bytes.HasPrefix(ln, []byte(key)) { + val := bytes.TrimPrefix(ln, []byte(key)) + val = bytes.Trim(val, `"' `) + return string(val) + } + } + return "" +} + +func bytesToLower(b []byte) []byte { return bytes.ToLower(b) } + +func getUname() string { + var u syscall.Utsname + if err := syscall.Uname(&u); err == nil { + return fromUts(u.Release[:]) + } + return "unknown" +} + +func fromUts(a []int8) string { + var b []byte + for _, c := range a { + if c == 0 { + break + } + b = append(b, byte(c)) + } + return string(b) +} + +func detectCPU() (model string, hz uint64) { + + kPaths := []string{ + "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq", + "/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq", + } + for _, p := range kPaths { + if b, err := os.ReadFile(p); err == nil { + + if n, err := strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64); err == nil && n > 0 { + hz = n * 1000 + + model = readCPUModelFromProc() + if model == "" { + model = "unknown" + } + return model, hz + } + } + } + + if b, err := os.ReadFile("/proc/cpuinfo"); err == nil { + lower := bytes.ToLower(b) + + if m := findFirstField(lower, "model name"); m != "" { + model = trimField(m) + } else if m := findFirstField(lower, "hardware"); m != "" { + model = trimField(m) + } else if m := findFirstField(lower, "processor"); m != "" { + model = trimField(m) + } else if m := findFirstField(lower, "model"); m != "" { + + model = trimField(m) + } else { + model = "unknown" + } + + if mhz := findFirstField(lower, "cpu mhz"); mhz != "" { + if f, err := strconv.ParseFloat(strings.TrimSpace(mhz), 64); err == nil && f > 0 { + hz = uint64(f * 1e6) + } + } + + return model, hz + } + + return "unknown", 0 +} + +func readCPUModelFromProc() string { + if b, err := os.ReadFile("/proc/cpuinfo"); err == nil { + lower := bytes.ToLower(b) + if m := findFirstField(lower, "model name"); m != "" { + return trimField(m) + } + if m := findFirstField(lower, "hardware"); m != "" { + return trimField(m) + } + } + return "" +} + +func findFirstField(b []byte, key string) string { + + t := []byte(key + ":") + if i := bytes.Index(b, t); i != -1 { + + if j := bytes.IndexByte(b[i:], '\n'); j != -1 { + line := b[i : i+j] + parts := bytes.SplitN(line, []byte{':'}, 2) + if len(parts) == 2 { + return string(parts[1]) + } + } else { + parts := bytes.SplitN(b[i:], []byte{':'}, 2) + if len(parts) == 2 { + return string(parts[1]) + } + } + } + + t2 := []byte(key + "\t:") + if i := bytes.Index(b, t2); i != -1 { + if j := bytes.IndexByte(b[i:], '\n'); j != -1 { + line := b[i : i+j] + parts := bytes.SplitN(line, []byte{':'}, 2) + if len(parts) == 2 { + return string(parts[1]) + } + } + } + + t3 := []byte(key + "=") + if i := bytes.Index(b, t3); i != -1 { + if j := bytes.IndexByte(b[i:], '\n'); j != -1 { + line := b[i : i+j] + parts := bytes.SplitN(line, []byte{'='}, 2) + if len(parts) == 2 { + return string(parts[1]) + } + } + } + return "" +} + +func trimField(s string) string { + return strings.TrimSpace(strings.Trim(s, `"`)) +} + +func parseMemInfo() (memTotal, memAvailable, swapTotal, swapFree uint64) { + if b, err := os.ReadFile("/proc/meminfo"); err == nil { + for _, ln := range bytes.Split(b, []byte{'\n'}) { + if len(ln) == 0 { + continue + } + l := bytes.ToLower(ln) + fields := bytes.Fields(l) + if len(fields) < 2 { + continue + } + + key := string(fields[0]) + valStr := string(fields[1]) + n, err := strconv.ParseUint(valStr, 10, 64) + if err != nil { + continue + } + + switch key { + case "memtotal:": + memTotal = n * 1024 + case "memavailable:": + memAvailable = n * 1024 + case "swaptotal:": + swapTotal = n * 1024 + case "swapfree:": + swapFree = n * 1024 + } + } + } + return +} + +func getStorageAndType(path string) (used, total uint64, fstype string) { + var st syscall.Statfs_t + if syscall.Statfs(path, &st) == nil { + total = st.Blocks * uint64(st.Bsize) + avail := st.Bavail * uint64(st.Bsize) + used = total - avail + } + + if b, err := os.ReadFile("/proc/mounts"); err == nil { + lines := bytes.Split(b, []byte{'\n'}) + + bestLen := -1 + for _, ln := range lines { + if len(ln) == 0 { + continue + } + parts := bytes.Fields(ln) + if len(parts) < 3 { + continue + } + mp := string(parts[1]) + if strings.HasPrefix(path, mp) && len(mp) > bestLen { + bestLen = len(mp) + fstype = string(parts[2]) + } + } + } + if fstype == "" { + fstype = "fs" + } + return used, total, strings.ToLower(fstype) +} + +func detectDesktop() string { + + envs := []string{ + "XDG_CURRENT_DESKTOP", + "DESKTOP_SESSION", + "XDG_SESSION_DESKTOP", + "GDMSESSION", + "SWAYSOCK", + } + for _, e := range envs { + if v := os.Getenv(e); v != "" { + + return strings.ToLower(strings.Split(v, ":")[0]) + } + } + + if os.Getenv("WAYLAND_DISPLAY") != "" { + return "wayland" + } + if os.Getenv("DISPLAY") != "" { + return "x11" + } + + return "tty" +} + +func printAligned(values, keys []string, cube []string) { + n := len(values) + cubeH := len(cube) + + valW := 0 + keyW := 0 + for _, v := range values { + if visibleLen(v) > valW { + valW = visibleLen(v) + } + } + for _, k := range keys { + if visibleLen(k) > keyW { + keyW = visibleLen(k) + } + } + + cubeCol := valW + 2 + 3 + keyW + 2 + + lines := n + if cubeH > lines { + lines = cubeH + } + cubeStart := 0 + if n > cubeH { + cubeStart = (n - cubeH) / 2 + } + + for i := 0; i < lines; i++ { + left := "" + mid := "" + if i < n { + left = values[i] + mid = "<- " + bold(keys[i]) + } + line := "" + if left != "" || mid != "" { + + if left != "" { + line += padRight(left, valW) + } else { + line += strings.Repeat(" ", valW) + } + line += " " + + if mid != "" { + + key := keys[i] + paddedKey := padRight(key, keyW) + mid = "<- " + bold(paddedKey) + line += mid + } + } + + vis := visibleLen(line) + if pad := cubeCol - vis; pad > 0 { + line += strings.Repeat(" ", pad) + } + + cIdx := i - cubeStart + if cIdx >= 0 && cIdx < cubeH { + line += cube[cIdx] + } + fmt.Println(line) + } +} + +func bold(s string) string { return "\033[1m" + s + "\033[0m" } + +func padRight(s string, w int) string { + if visibleLen(s) >= w { + return s + } + return s + strings.Repeat(" ", w-visibleLen(s)) +} + +func visibleLen(s string) int { + n := 0 + esc := false + for i := 0; i < len(s); i++ { + c := s[i] + if c == '\033' { + esc = true + continue + } + if esc { + if c == 'm' { + esc = false + } + continue + } + n++ + } + return n +} + +func hr(b uint64) string { + const ( + KB = 1024 + MB = KB * 1024 + GB = MB * 1024 + ) + if b >= GB { + f := float64(b) / float64(GB) + + return fmt.Sprintf("%.2f gb", f) + } + if b >= MB { + return fmt.Sprintf("%.0f mb", float64(b)/float64(MB)) + } + return fmt.Sprintf("%d kb", b/KB) +} \ No newline at end of file