From aa142afa78620acaf15eca6a0f361cd48ced5d46 Mon Sep 17 00:00:00 2001 From: nekohepott Date: Wed, 17 Jun 2026 04:16:26 +0300 Subject: [PATCH] refactor (this build is broken) --- default.nix | 2 +- flake.nix | 2 +- logos/artix.png | 3 + logos/cachyos.png | 3 + logos/endeavouros.png | 3 + logos/fedora.png | 3 + logos/gentoo.png | 3 + logos/kali.png | 3 + logos/manjaro.png | 3 + logos/mint.png | 3 + logos/opensuse.png | 3 + logos/ubuntu.png | 3 + logos/void.png | 3 + main.go | 617 ----------------------------------- shell.nix | 2 +- src/main.go | 92 ++++++ src/providers/config.go | 140 ++++++++ src/providers/logo.go | 156 +++++++++ src/providers/packages.go | 50 +++ src/providers/systemstats.go | 215 ++++++++++++ 20 files changed, 689 insertions(+), 620 deletions(-) create mode 100644 logos/artix.png create mode 100644 logos/cachyos.png create mode 100644 logos/endeavouros.png create mode 100644 logos/fedora.png create mode 100644 logos/gentoo.png create mode 100644 logos/kali.png create mode 100644 logos/manjaro.png create mode 100644 logos/mint.png create mode 100644 logos/opensuse.png create mode 100644 logos/ubuntu.png create mode 100644 logos/void.png delete mode 100644 main.go create mode 100644 src/main.go create mode 100644 src/providers/config.go create mode 100644 src/providers/logo.go create mode 100644 src/providers/packages.go create mode 100644 src/providers/systemstats.go diff --git a/default.nix b/default.nix index 69ae640..8277b78 100644 --- a/default.nix +++ b/default.nix @@ -17,7 +17,7 @@ buildGoApplication { pname = "goGoFetch"; version = "1.0"; pwd = ./.; - src = ./.; + src = ./src; modules = ./gomod2nix.toml; nativeBuildInputs = [ pkgs.makeWrapper ]; diff --git a/flake.nix b/flake.nix index 45bc282..ae2747c 100644 --- a/flake.nix +++ b/flake.nix @@ -24,7 +24,7 @@ go-lint = pkgs.stdenvNoCC.mkDerivation { name = "go-lint"; dontBuild = true; - src = ./.; + src = ./src; doCheck = true; nativeBuildInputs = with pkgs; [ golangci-lint diff --git a/logos/artix.png b/logos/artix.png new file mode 100644 index 0000000..29c3a7f --- /dev/null +++ b/logos/artix.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b55b01699eb67b112913295a371379ca84d095b9ef6948a910135efff3f8ff4 +size 472609 diff --git a/logos/cachyos.png b/logos/cachyos.png new file mode 100644 index 0000000..2b3c027 --- /dev/null +++ b/logos/cachyos.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e79ba42abde4807af1d099e45f2f25cb027c2ed2a12fc685e98b9b796e82474a +size 107375 diff --git a/logos/endeavouros.png b/logos/endeavouros.png new file mode 100644 index 0000000..abc2e7e --- /dev/null +++ b/logos/endeavouros.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7066dd16b1585be5a3319c2f7cb1c6e0d2949583430f54868427f15f17cf8620 +size 8468 diff --git a/logos/fedora.png b/logos/fedora.png new file mode 100644 index 0000000..f013c1f --- /dev/null +++ b/logos/fedora.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd5d0dd63c59997cbfea0988b2f5f6d8eb77591fb78941ef7e1f9f3b847503cb +size 50087 diff --git a/logos/gentoo.png b/logos/gentoo.png new file mode 100644 index 0000000..daa987c --- /dev/null +++ b/logos/gentoo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a39fa535e97430cd982d571e25744a3414e4493169580e20f700fdd41eb1fae +size 127361 diff --git a/logos/kali.png b/logos/kali.png new file mode 100644 index 0000000..8c792c4 --- /dev/null +++ b/logos/kali.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fe40eed6cdf5e368910da6624bde5f0b041fcdeb35c297fc8ca2721b381150d +size 77902 diff --git a/logos/manjaro.png b/logos/manjaro.png new file mode 100644 index 0000000..45d4dd4 --- /dev/null +++ b/logos/manjaro.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d2965487d2a89b92ea48a546b822c78dc6338a8bc31616190316046e07533cf +size 24590 diff --git a/logos/mint.png b/logos/mint.png new file mode 100644 index 0000000..5535793 --- /dev/null +++ b/logos/mint.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7eb209cfd5c196c612c2d17d862d1660e31feebe56300bc924159c2ae8e6c54 +size 46466 diff --git a/logos/opensuse.png b/logos/opensuse.png new file mode 100644 index 0000000..57245fc --- /dev/null +++ b/logos/opensuse.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42164500ec6a4403dcb46dfb88afa8e8c5974a9d72af374c56c09e561b8994d4 +size 19348 diff --git a/logos/ubuntu.png b/logos/ubuntu.png new file mode 100644 index 0000000..83b4a74 --- /dev/null +++ b/logos/ubuntu.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d23b69c85ad351f43a30097faa7be2377998e7d53c183d4997ea679eeb5fe9ea +size 50745 diff --git a/logos/void.png b/logos/void.png new file mode 100644 index 0000000..f59700f --- /dev/null +++ b/logos/void.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e148190550c02b1a91e9e6f10403e0e21c837b7f5cd1d53a5b4934cf0e4d8bf +size 231512 diff --git a/main.go b/main.go deleted file mode 100644 index a5313e6..0000000 --- a/main.go +++ /dev/null @@ -1,617 +0,0 @@ -package main - -import ( - "bufio" - "flag" - "fmt" - _ "image/png" - "os" - "os/exec" - "path" - "path/filepath" - "regexp" - "strconv" - "strings" - "time" - "unicode/utf8" - - "github.com/BurntSushi/toml" -) - -const ( - Reset = "\033[0m" - Blue = "\033[1;34m" -) - -var dist string - -var ansiEscapeRe = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`) - -type Config struct { - Ascii bool `toml:"ascii"` - Layout []string `toml:"layout"` - CustomLogo string `toml:"custom_logo"` - LogoPosition string `toml:"logo_position"` -} - -func appendMissingConfigKeys(pathConf string, md toml.MetaData) error { - var missing []string - - if !md.IsDefined("ascii") { - missing = append(missing, "# Set this to \"true\" if you like ascii more than images\nascii = false") - } - - if !md.IsDefined("layout") { - missing = append(missing, `# Stats layout: reorder or remove items to customize your fetch -# Available: "dist", "host", "cpu", "kernel", "ram", "gpu", "de/wm", "pkgs", "shell", "terminal", "uptime" -layout = [ - "host", - "dist", - "cpu", - "kernel", - "ram", - "gpu", - "de/wm", - "pkgs", - "shell", - "terminal", - "uptime", -]`) - } - - if !md.IsDefined("custom_logo") { - missing = append(missing, "# Absolute path to a custom image or .txt file. Leave empty to use auto-detection.\ncustom_logo = \"\"") - } - - if !md.IsDefined("logo_position") { - missing = append(missing, "# Position of the logo block relative to the stats block: \"left\" or \"right\".\nlogo_position = \"left\"") - } - - if len(missing) == 0 { - return nil - } - - f, err := os.OpenFile(pathConf, os.O_APPEND|os.O_WRONLY, 0644) - if err != nil { - return err - } - defer func(f *os.File) { - _ = f.Close() - }(f) - - block := "\n# Added automatically after update\n\n" + strings.Join(missing, "\n\n") + "\n" - _, err = f.WriteString(block) - return err -} - -func getConfigPath() (string, error) { - configDir, err := os.UserConfigDir() - if err != nil { - return "", err - } - directoryName := "gogofetch" - fullPath := filepath.Join(configDir, directoryName, "config.toml") - - return fullPath, nil -} - -func initConfig() (Config, string) { - var conf Config - pathConf, err := getConfigPath() - if err != nil { - return conf, "" - } - - err = os.MkdirAll(filepath.Dir(pathConf), 0755) - if err != nil { - return Config{}, "" - } - - if _, err := os.Stat(pathConf); os.IsNotExist(err) { - defaultConfig := []byte(` -# Set this to "true" if you like ascii more than images -ascii = false - -# Stats layout: reorder or remove items to customize your fetch -# Available: "dist", "host", "cpu", "kernel", "ram", "gpu", "de/wm", "pkgs", "shell", "terminal", "uptime" -layout = [ - "host", - "dist", - "cpu", - "kernel", - "ram", - "gpu", - "de/wm", - "pkgs", - "shell", - "terminal", - "uptime", -] - -# Absolute path to a custom image or .txt file. Leave empty to use auto-detection. -custom_logo = "" - -# Position of the logo block relative to the stats block: "left" or "right". -logo_position = "left" -`) - err := os.WriteFile(pathConf, defaultConfig, 0644) - if err != nil { - return Config{}, "" - } - } - - md, err := toml.DecodeFile(pathConf, &conf) - if err != nil { - fmt.Printf("Error loading config at %s: %v\n", pathConf, err) - return conf, pathConf - } - - if err := appendMissingConfigKeys(pathConf, md); err != nil { - fmt.Printf("Warning: Could not update config at %s: %v\n", pathConf, err) - } else { - if _, err := toml.DecodeFile(pathConf, &conf); err != nil { - fmt.Printf("Error reloading updated config at %s: %v\n", pathConf, err) - } - } - - return conf, pathConf -} - -func getDist() string { - f, _ := os.Open("/etc/os-release") - defer func(f *os.File) { - err := f.Close() - if err != nil { - fmt.Println("Error closing file:", err) - } - }(f) - s := bufio.NewScanner(f) - for s.Scan() { - t := s.Text() - if strings.HasPrefix(t, "PRETTY_NAME") { - distro := strings.TrimPrefix(t, "PRETTY_NAME=") - dist = strings.Trim(distro, "\"") - dist = strings.TrimSpace(dist) - return dist - } - } - return "no /etc/os-release file found" -} - -func getRam() string { - f, err := os.Open("/proc/meminfo") - if err != nil { - return "error" - } - - var total, available int - s := bufio.NewScanner(f) - for s.Scan() { - line := s.Text() - if strings.HasPrefix(line, "MemTotal:") { - _, err2 := fmt.Sscanf(line, "MemTotal: %d kB", &total) - if err2 != nil { - return "" - } - } - if strings.HasPrefix(line, "MemAvailable:") { - _, err := fmt.Sscanf(line, "MemAvailable: %d kB", &available) - if err != nil { - return "" - } - } - } - totalMB := total / 1024 - availableMB := available / 1024 - usedMB := totalMB - availableMB - - ram := fmt.Sprintf("%d / %d MiB (%d MiB available)", usedMB, totalMB, availableMB) - ram = strings.TrimSpace(ram) - return ram - -} - -func getCpu() string { - f, err := os.Open("/proc/cpuinfo") - if err != nil { - return "error" - } - - var cpu string - s := bufio.NewScanner(f) - for s.Scan() { - line := s.Text() - if strings.HasPrefix(line, "model name") { - parts := strings.SplitN(line, ":", 2) - if len(parts) > 1 { - cpu := strings.TrimSpace(parts[1]) - return cpu - } - } - } - - return strings.TrimSpace(cpu) -} - -func getGpu() string { - out, err := exec.Command("sh", "-c", `lspci -mm | awk -F'"' '$2=="VGA compatible controller" || $2=="3D controller" || $2=="Display controller"'`).Output() - if err != nil || len(out) == 0 { - return "Unknown GPU" - } - - lines := strings.Split(strings.TrimSpace(string(out)), "\n") - var gpus []string - - for _, line := range lines { - re := regexp.MustCompile(`"([^"]+)"`) - matches := re.FindAllStringSubmatch(line, -1) - - if len(matches) >= 3 { - vendor := matches[1][1] - device := matches[2][1] - - cleaned := cleanGpuString(vendor + " " + device) - gpus = append(gpus, cleaned) - } - } - - if len(gpus) > 0 { - return strings.Join(gpus, " / ") - } - return "GPU not found" -} - -func cleanGpuString(raw string) string { - prettyNameRe := regexp.MustCompile(`\[(.*?)]`) - match := prettyNameRe.FindStringSubmatch(raw) - - res := raw - if len(match) > 1 { - vendor := "" - if strings.Contains(strings.ToLower(raw), "nvidia") { - vendor = "NVIDIA " - } - if strings.Contains(strings.ToLower(raw), "intel") { - vendor = "Intel " - } - if strings.Contains(strings.ToLower(raw), "amd") { - vendor = "AMD " - } - - res = vendor + match[1] - } - - re := regexp.MustCompile(`\(.*?\)|\[.*?]`) - res = re.ReplaceAllString(res, "") - - fluff := []string{"Corporation", "Controller", "VGA compatible", "3D", "Graphics", "Device"} - for _, word := range fluff { - res = strings.ReplaceAll(res, word, "") - } - - return strings.Join(strings.Fields(res), " ") -} - -func getKernel() string { - out, err := exec.Command("sh", "-c", "uname -r").Output() - if err != nil { - return "Error getting kernel, how did we get there?" - } - return strings.TrimSpace(string(out)) -} - -func getHostname() string { - hostname, err := exec.Command("sh", "-c", "uname -n").Output() - if err != nil { - return "Error getting hostname, awful" - } - username, errUser := exec.Command("sh", "-c", "whoami").Output() - if errUser != nil { - return "Error getting username, nobody here but us chickens!" - } - fullHostname := fmt.Sprintf("%s@%s", strings.TrimSpace(string(username)), strings.TrimSpace(string(hostname))) - return fullHostname -} - -func checkNix() bool { - _, err := exec.Command("sh", "-c", "command -v nix-env >/dev/null 2>&1").Output() - if err != nil { - return false - } - return true -} - -func addNix() string { - out, _ := exec.Command("sh", "-c", "nix-env -q | wc -l").Output() - nixPkgs := strings.TrimSpace(string(out)) + " (nix)" - return nixPkgs -} - -func getPkgs() string { - var pkgs string - haveNix := checkNix() - if strings.HasPrefix(dist, "Arch") { - out, _ := exec.Command("sh", "-c", "pacman -Qq | wc -l").Output() - pkgs = strings.TrimSpace(string(out)) + " (pacman)" - if haveNix { - pkgs = strings.TrimSpace(pkgs + ", " + addNix()) - } - return pkgs - } - if strings.HasPrefix(dist, "Debian") { - out, _ := exec.Command("sh", "-c", "dpkg -l | grep ^ii | wc -l").Output() - pkgs = strings.TrimSpace(string(out)) + " (apt)" - if haveNix { - pkgs = strings.TrimSpace(pkgs + ", " + addNix()) - } - return pkgs - } - if strings.HasPrefix(dist, "NixOS") { - entries, err := os.ReadDir("/run/current-system/sw/bin") - if err != nil { - return "can't get nix packages" - } - return strconv.Itoa(len(entries)) + " (nix)" - } - return "unknown" -} - -func getLogo(dist string) string { - switch { - case strings.HasPrefix(dist, "Arch"): - return "logos/arch.png" - case strings.HasPrefix(dist, "NixOS"): - return "logos/nixos.png" - case strings.HasPrefix(dist, "Debian"): - return "logos/debian.png" - default: - return "logos/linux.png" - } -} - -func getAscii(dist string) string { - switch { - case strings.HasPrefix(dist, "Arch"): - return "logos/arch.txt" - case strings.HasPrefix(dist, "NixOS"): - return "logos/nixos.txt" - case strings.HasPrefix(dist, "Debian"): - return "logos/debian.txt" - default: - return "logos/linux.txt" - } -} - -func printAsciiLogo(path string) string { - content, err := os.ReadFile(path) - if err != nil { - return fmt.Sprintf("Error: Could not read ASCII file at %s\n", path) - } - return string(content) -} - -func renderLogoChafa(path string, width uint) []string { - cols := width / 8 - if cols == 0 { - cols = 1 - } - sizeArg := fmt.Sprintf("--size=%dx%d", cols, cols/2) - cmd := exec.Command("chafa", path, "--format", "symbols", sizeArg, "--symbols=half") - out, err := cmd.Output() - if err != nil { - return []string{fmt.Sprintf("Chafa error: %v", err)} - } - return splitLines(string(out)) -} - -func normalizeLogoPosition(pos string) string { - value := strings.ToLower(strings.TrimSpace(pos)) - if value == "left" || value == "right" { - return value - } - return "right" -} - -func splitLines(content string) []string { - normalized := strings.ReplaceAll(content, "\r\n", "\n") - normalized = strings.TrimRight(normalized, "\n") - if normalized == "" { - return []string{} - } - return strings.Split(normalized, "\n") -} - -func maxLineLen(lines []string) int { - maxLen := 0 - for _, line := range lines { - lineLen := visibleWidth(line) - if lineLen > maxLen { - maxLen = lineLen - } - } - return maxLen -} - -func visibleWidth(line string) int { - stripped := ansiEscapeRe.ReplaceAllString(line, "") - return utf8.RuneCountInString(stripped) -} - -func padRightVisible(line string, targetWidth int) string { - pad := targetWidth - visibleWidth(line) - if pad <= 0 { - return line - } - return line + strings.Repeat(" ", pad) -} - -func renderSideBySide(statsLines []string, logoLines []string, position string) { - const gap = " " - statsWidth := maxLineLen(statsLines) - logoWidth := maxLineLen(logoLines) - - lineCount := len(statsLines) - if len(logoLines) > lineCount { - lineCount = len(logoLines) - } - - for i := 0; i < lineCount; i++ { - stats := "" - logo := "" - - if i < len(statsLines) { - stats = statsLines[i] - } - if i < len(logoLines) { - logo = logoLines[i] - } - - if position == "left" { - fmt.Printf("%s%s%s\n", padRightVisible(logo, logoWidth), gap, stats) - continue - } - - fmt.Printf("%s%s%s\n", padRightVisible(stats, statsWidth), gap, logo) - } -} - -func getAbsLogoPath(relativePath string) string { - if _, err := os.Stat(relativePath); err == nil { - return relativePath - } - - exePath, err := os.Executable() - if err == nil { - exeDir := filepath.Dir(exePath) - - nixPath := filepath.Join(exeDir, "..", "share", "gogofetch", relativePath) - - if _, err := os.Stat(nixPath); err == nil { - return nixPath - } - } - - systemPath := filepath.Join("/usr/share/gogofetch", relativePath) - if _, err := os.Stat(systemPath); err == nil { - return systemPath - } - - return relativePath -} - -func getShell() string { - out, err := exec.Command("ps", "-p", strconv.Itoa(os.Getppid()), "-o", "comm=").Output() - if err != nil { - return path.Base(os.Getenv("SHELL")) - } - return strings.TrimSpace(string(out)) -} - -func getTerminal() string { - return os.Getenv("TERM") -} - -func getDE() string { - de := os.Getenv("XDG_CURRENT_DESKTOP") - if de == "" { - return "Unknown or w/o GUI" - } - return strings.TrimSpace(de) -} - -func getUptime() string { - data, err := os.ReadFile("/proc/uptime") - if err != nil { - return "0" - } - uptimeStr := strings.Fields(string(data))[0] - seconds, err := strconv.ParseFloat(uptimeStr, 64) - if err != nil { - _, err := fmt.Fprintf(os.Stderr, "Error: %v\n", err) - if err != nil { - return "error printing uptime" - } - return "" - } - d := time.Duration(seconds) * time.Second - formatted := d.Round(time.Second).String() - spaced := strings.NewReplacer("h", "h ", "m", "m ", "s", "s ").Replace(formatted) - return spaced -} - -func main() { - conf, _ := initConfig() - - if len(conf.Layout) == 0 { - conf.Layout = []string{"host", "dist", "cpu", "ram", "shell", "uptime"} - fmt.Println("Warning: No layout specified in config, using default") - } - - dist := getDist() - var defaultLogo string - - if conf.CustomLogo != "" { - defaultLogo = conf.CustomLogo - } else if conf.Ascii { - defaultLogo = getAscii(dist) - } else { - defaultLogo = getLogo(dist) - } - - width := flag.Int("w", 190, "Logo width in lines") - logoPath := flag.String("l", getAbsLogoPath(defaultLogo), "Path to the logo") - logoPos := flag.String("p", normalizeLogoPosition(conf.LogoPosition), "Logo position: left or right") - flag.Parse() - position := normalizeLogoPosition(*logoPos) - - type Info struct { - Label string - Value string - } - var stats []Info - - for _, item := range conf.Layout { - switch strings.ToLower(item) { - case "host": - stats = append(stats, Info{"host", getHostname()}) - case "dist": - stats = append(stats, Info{"OS", dist}) - case "cpu": - stats = append(stats, Info{"cpu", getCpu()}) - case "kernel", "krnl": - stats = append(stats, Info{"krnl", getKernel()}) - case "ram": - stats = append(stats, Info{"ram", getRam()}) - case "gpu": - stats = append(stats, Info{"gpu", getGpu()}) - case "de/wm", "de", "wm": - stats = append(stats, Info{"de/wm", getDE()}) - case "pkgs": - stats = append(stats, Info{"pkgs", getPkgs()}) - case "shell": - stats = append(stats, Info{"shell", getShell()}) - case "terminal", "term": - stats = append(stats, Info{"term", getTerminal()}) - case "uptime": - stats = append(stats, Info{"uptime", getUptime()}) - } - } - - var statsLines []string - for _, info := range stats { - statsLines = append(statsLines, fmt.Sprintf("-> %s%s%s: %s", Blue, info.Label, Reset, info.Value)) - } - - var logoLines []string - if _, err := os.Stat(*logoPath); err == nil { - if conf.Ascii { - logoLines = splitLines(printAsciiLogo(*logoPath)) - } else { - logoLines = renderLogoChafa(*logoPath, uint(*width)) - } - } else { - logoLines = []string{fmt.Sprintf("Logo not found at: %s", *logoPath)} - } - - renderSideBySide(statsLines, logoLines, position) -} diff --git a/shell.nix b/shell.nix index dae85fc..7f191d9 100644 --- a/shell.nix +++ b/shell.nix @@ -18,7 +18,7 @@ let goApp = pkgs.buildGoModule { pname = "goGoFetch"; version = "1.0"; - src = ./.; + src = ./src; vendorHash = null; }; in diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..01d73a2 --- /dev/null +++ b/src/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "flag" + "fmt" + "goGoFetch/src/providers" + _ "image/png" + "os" + "strings" +) + +const ( + Reset = "\033[0m" + Blue = "\033[1;34m" +) + +func main() { + conf, _ := providers.InitConfig() + + if len(conf.Layout) == 0 { + conf.Layout = []string{"host", "dist", "cpu", "ram", "shell", "uptime"} + fmt.Println("Warning: No layout specified in config, using default") + } + + distID, prettyName := providers.GetDist() + var defaultLogo string + + if conf.CustomLogo != "" { + defaultLogo = conf.CustomLogo + } else if conf.Ascii { + defaultLogo = providers.GetAscii(distID) + } else { + defaultLogo = providers.GetLogo(distID) + } + + width := flag.Int("w", 190, "Logo width in lines") + logoPath := flag.String("l", providers.GetAbsLogoPath(defaultLogo), "Path to the logo") + logoPos := flag.String("p", providers.NormalizeLogoPosition(conf.LogoPosition), "Logo position: left or right") + flag.Parse() + position := providers.NormalizeLogoPosition(*logoPos) + + type Info struct { + Label string + Value string + } + var stats []Info + + for _, item := range conf.Layout { + switch strings.ToLower(item) { + case "host": + stats = append(stats, Info{"host", providers.GetHostname()}) + case "dist": + stats = append(stats, Info{"OS", prettyName}) + case "cpu": + stats = append(stats, Info{"cpu", providers.GetCpu()}) + case "kernel", "krnl": + stats = append(stats, Info{"krnl", providers.GetKernel()}) + case "ram": + stats = append(stats, Info{"ram", providers.GetRam()}) + case "gpu": + stats = append(stats, Info{"gpu", providers.GetGpu()}) + case "de/wm", "de", "wm": + stats = append(stats, Info{"de/wm", providers.GetDE()}) + case "pkgs": + stats = append(stats, Info{"pkgs", providers.GetPkgs()}) + case "shell": + stats = append(stats, Info{"shell", providers.GetShell()}) + case "terminal", "term": + stats = append(stats, Info{"term", providers.GetTerminal()}) + case "uptime": + stats = append(stats, Info{"uptime", providers.GetUptime()}) + } + } + + var statsLines []string + for _, info := range stats { + statsLines = append(statsLines, fmt.Sprintf("-> %s%s%s: %s", Blue, info.Label, Reset, info.Value)) + } + + var logoLines []string + if _, err := os.Stat(*logoPath); err == nil { + if conf.Ascii { + logoLines = providers.SplitLines(providers.PrintAsciiLogo(*logoPath)) + } else { + logoLines = providers.RenderLogoChafa(*logoPath, uint(*width)) + } + } else { + logoLines = []string{fmt.Sprintf("Logo not found at: %s", *logoPath)} + } + + providers.RenderSideBySide(statsLines, logoLines, position) +} diff --git a/src/providers/config.go b/src/providers/config.go new file mode 100644 index 0000000..80a0612 --- /dev/null +++ b/src/providers/config.go @@ -0,0 +1,140 @@ +package providers + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/BurntSushi/toml" +) + +type Config struct { + Ascii bool `toml:"ascii"` + Layout []string `toml:"layout"` + CustomLogo string `toml:"custom_logo"` + LogoPosition string `toml:"logo_position"` +} + +func appendMissingConfigKeys(pathConf string, md toml.MetaData) error { + var missing []string + + if !md.IsDefined("ascii") { + missing = append(missing, "# Set this to \"true\" if you like ascii more than images\nascii = false") + } + + if !md.IsDefined("layout") { + missing = append(missing, `# Stats layout: reorder or remove items to customize your fetch +# Available: "dist", "host", "cpu", "kernel", "ram", "gpu", "de/wm", "pkgs", "shell", "terminal", "uptime" +layout = [ + "host", + "dist", + "cpu", + "kernel", + "ram", + "gpu", + "de/wm", + "pkgs", + "shell", + "terminal", + "uptime", +]`) + } + + if !md.IsDefined("custom_logo") { + missing = append(missing, "# Absolute path to a custom image or .txt file. Leave empty to use auto-detection.\ncustom_logo = \"\"") + } + + if !md.IsDefined("logo_position") { + missing = append(missing, "# Position of the logo block relative to the stats block: \"left\" or \"right\".\nlogo_position = \"left\"") + } + + if len(missing) == 0 { + return nil + } + + f, err := os.OpenFile(pathConf, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer func(f *os.File) { + _ = f.Close() + }(f) + + block := "\n# Added automatically after update\n\n" + strings.Join(missing, "\n\n") + "\n" + _, err = f.WriteString(block) + return err +} + +func getConfigPath() (string, error) { + configDir, err := os.UserConfigDir() + if err != nil { + return "", err + } + directoryName := "gogofetch" + fullPath := filepath.Join(configDir, directoryName, "config.toml") + + return fullPath, nil +} + +func InitConfig() (Config, string) { + var conf Config + pathConf, err := getConfigPath() + if err != nil { + return conf, "" + } + + err = os.MkdirAll(filepath.Dir(pathConf), 0755) + if err != nil { + return Config{}, "" + } + + if _, err := os.Stat(pathConf); os.IsNotExist(err) { + defaultConfig := []byte(` +# Set this to "true" if you like ascii more than images +ascii = false + +# Stats layout: reorder or remove items to customize your fetch +# Available: "dist", "host", "cpu", "kernel", "ram", "gpu", "de/wm", "pkgs", "shell", "terminal", "uptime" +layout = [ + "host", + "dist", + "cpu", + "kernel", + "ram", + "gpu", + "de/wm", + "pkgs", + "shell", + "terminal", + "uptime", +] + +# Absolute path to a custom image or .txt file. Leave empty to use auto-detection. +custom_logo = "" + +# Position of the logo block relative to the stats block: "left" or "right". +logo_position = "left" +`) + err := os.WriteFile(pathConf, defaultConfig, 0644) + if err != nil { + return Config{}, "" + } + } + + md, err := toml.DecodeFile(pathConf, &conf) + if err != nil { + fmt.Printf("Error loading config at %s: %v\n", pathConf, err) + return conf, pathConf + } + + if err := appendMissingConfigKeys(pathConf, md); err != nil { + fmt.Printf("Warning: Could not update config at %s: %v\n", pathConf, err) + } else { + if _, err := toml.DecodeFile(pathConf, &conf); err != nil { + fmt.Printf("Error reloading updated config at %s: %v\n", pathConf, err) + } + } + + return conf, pathConf +} diff --git a/src/providers/logo.go b/src/providers/logo.go new file mode 100644 index 0000000..7b3cd88 --- /dev/null +++ b/src/providers/logo.go @@ -0,0 +1,156 @@ +package providers + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "unicode/utf8" +) + +var ansiEscapeRe = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`) + +func GetLogo(id string) string { + switch { + case strings.HasPrefix(id, "arch"): + return "logos/arch.png" + case strings.HasPrefix(id, "nixos"): + return "logos/nixos.png" + case strings.HasPrefix(id, "debian"): + return "logos/debian.png" + default: + return "logos/linux.png" + } +} + +func GetAscii(id string) string { + switch { + case strings.HasPrefix(id, "arch"): + return "logos/arch.txt" + case strings.HasPrefix(id, "nixos"): + return "logos/nixos.txt" + case strings.HasPrefix(id, "debian"): + return "logos/debian.txt" + default: + return "logos/linux.txt" + } +} + +func PrintAsciiLogo(path string) string { + content, err := os.ReadFile(path) + if err != nil { + return fmt.Sprintf("Error: Could not read ASCII file at %s\n", path) + } + return string(content) +} + +func RenderLogoChafa(path string, width uint) []string { + cols := width / 8 + if cols == 0 { + cols = 1 + } + sizeArg := fmt.Sprintf("--size=%dx%d", cols, cols/2) + cmd := exec.Command("chafa", path, "--format", "symbols", sizeArg, "--symbols=half") + out, err := cmd.Output() + if err != nil { + return []string{fmt.Sprintf("Chafa error: %v", err)} + } + return SplitLines(string(out)) +} + +func NormalizeLogoPosition(pos string) string { + value := strings.ToLower(strings.TrimSpace(pos)) + if value == "left" || value == "right" { + return value + } + return "right" +} + +func SplitLines(content string) []string { + normalized := strings.ReplaceAll(content, "\r\n", "\n") + normalized = strings.TrimRight(normalized, "\n") + if normalized == "" { + return []string{} + } + return strings.Split(normalized, "\n") +} + +func maxLineLen(lines []string) int { + maxLen := 0 + for _, line := range lines { + lineLen := visibleWidth(line) + if lineLen > maxLen { + maxLen = lineLen + } + } + return maxLen +} + +func visibleWidth(line string) int { + stripped := ansiEscapeRe.ReplaceAllString(line, "") + return utf8.RuneCountInString(stripped) +} + +func padRightVisible(line string, targetWidth int) string { + pad := targetWidth - visibleWidth(line) + if pad <= 0 { + return line + } + return line + strings.Repeat(" ", pad) +} + +func RenderSideBySide(statsLines []string, logoLines []string, position string) { + const gap = " " + statsWidth := maxLineLen(statsLines) + logoWidth := maxLineLen(logoLines) + + lineCount := len(statsLines) + if len(logoLines) > lineCount { + lineCount = len(logoLines) + } + + for i := 0; i < lineCount; i++ { + stats := "" + logo := "" + + if i < len(statsLines) { + stats = statsLines[i] + } + if i < len(logoLines) { + logo = logoLines[i] + } + + if position == "left" { + fmt.Printf("%s%s%s\n", padRightVisible(logo, logoWidth), gap, stats) + continue + } + + fmt.Printf("%s%s%s\n", padRightVisible(stats, statsWidth), gap, logo) + } +} + +func GetAbsLogoPath(relativePath string) string { + if _, err := os.Stat(relativePath); err == nil { + return relativePath + } + + exePath, err := os.Executable() + if err == nil { + exeDir := filepath.Dir(exePath) + + nixPath := filepath.Join(exeDir, "..", "share", "gogofetch", relativePath) + + if _, err := os.Stat(nixPath); err == nil { + return nixPath + } + } + + systemPath := filepath.Join("/usr/share/gogofetch", relativePath) + if _, err := os.Stat(systemPath); err == nil { + return systemPath + } + + return relativePath +} diff --git a/src/providers/packages.go b/src/providers/packages.go new file mode 100644 index 0000000..96de6fd --- /dev/null +++ b/src/providers/packages.go @@ -0,0 +1,50 @@ +package providers + +import ( + "os" + "os/exec" + "strconv" + "strings" +) + +func checkNix() bool { + _, err := os.Stat("/nix") + if err != nil { + return false + } + return true +} + +func addNix() string { + out, _ := exec.Command("sh", "-c", "nix-env -q | wc -l").Output() + nixPkgs := strings.TrimSpace(string(out)) + " (nix)" + return nixPkgs +} + +func GetPkgs() string { + var pkgs string + haveNix := checkNix() + if strings.HasPrefix(id, "arch") { + entries, err := os.ReadDir("/run/current-system/sw/bin") + if err != nil { + return "can't get nix packages" + } + return strconv.Itoa(len(entries)) + " (nix)" + } + if strings.HasPrefix(id, "debian") { + out, _ := exec.Command("sh", "-c", "dpkg -l | grep ^ii | wc -l").Output() + pkgs = strings.TrimSpace(string(out)) + " (apt)" + if haveNix { + pkgs = strings.TrimSpace(pkgs + ", " + addNix()) + } + return pkgs + } + if strings.HasPrefix(id, "nixos") { + entries, err := os.ReadDir("/run/current-system/sw/bin") + if err != nil { + return "can't get nix packages" + } + return strconv.Itoa(len(entries)) + " (nix)" + } + return "unknown" +} diff --git a/src/providers/systemstats.go b/src/providers/systemstats.go new file mode 100644 index 0000000..62007bb --- /dev/null +++ b/src/providers/systemstats.go @@ -0,0 +1,215 @@ +package providers + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "path" + "regexp" + "strconv" + "strings" + "time" +) + +var id, prettyName string + +func GetDist() (string, string) { + f, _ := os.Open("/etc/os-release") + defer func(f *os.File) { + err := f.Close() + if err != nil { + fmt.Println("Error closing file:", err) + } + }(f) + s := bufio.NewScanner(f) + for s.Scan() { + t := s.Text() + if strings.HasPrefix(t, "ID=") { + id = strings.TrimPrefix(t, "ID=") + } + if strings.HasPrefix(t, "PRETTY_NAME=") { + prettyName = strings.TrimPrefix(t, "PRETTY_NAME=") + prettyName = strings.Trim(prettyName, "\"") + } + } + if id == "" && prettyName == "" { + return "unknown", "unknown" + } + return id, prettyName +} + +func GetRam() string { + f, err := os.Open("/proc/meminfo") + if err != nil { + return "error" + } + + var total, available int + s := bufio.NewScanner(f) + for s.Scan() { + line := s.Text() + if strings.HasPrefix(line, "MemTotal:") { + _, err2 := fmt.Sscanf(line, "MemTotal: %d kB", &total) + if err2 != nil { + return "" + } + } + if strings.HasPrefix(line, "MemAvailable:") { + _, err := fmt.Sscanf(line, "MemAvailable: %d kB", &available) + if err != nil { + return "" + } + } + } + totalMB := total / 1024 + availableMB := available / 1024 + usedMB := totalMB - availableMB + + ram := fmt.Sprintf("%d / %d MiB (%d MiB available)", usedMB, totalMB, availableMB) + ram = strings.TrimSpace(ram) + return ram + +} + +func GetCpu() string { + f, err := os.Open("/proc/cpuinfo") + if err != nil { + return "error" + } + + var cpu string + s := bufio.NewScanner(f) + for s.Scan() { + line := s.Text() + if strings.HasPrefix(line, "model name") { + parts := strings.SplitN(line, ":", 2) + if len(parts) > 1 { + cpu := strings.TrimSpace(parts[1]) + return cpu + } + } + } + + return strings.TrimSpace(cpu) +} + +func GetGpu() string { + out, err := exec.Command("sh", "-c", `lspci -mm | awk -F'"' '$2=="VGA compatible controller" || $2=="3D controller" || $2=="Display controller"'`).Output() + if err != nil || len(out) == 0 { + return "Unknown GPU" + } + + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + var gpus []string + + for _, line := range lines { + re := regexp.MustCompile(`"([^"]+)"`) + matches := re.FindAllStringSubmatch(line, -1) + + if len(matches) >= 3 { + vendor := matches[1][1] + device := matches[2][1] + + cleaned := cleanGpuString(vendor + " " + device) + gpus = append(gpus, cleaned) + } + } + + if len(gpus) > 0 { + return strings.Join(gpus, " / ") + } + return "GPU not found" +} + +func cleanGpuString(raw string) string { + prettyNameRe := regexp.MustCompile(`\[(.*?)]`) + match := prettyNameRe.FindStringSubmatch(raw) + + res := raw + if len(match) > 1 { + vendor := "" + if strings.Contains(strings.ToLower(raw), "nvidia") { + vendor = "NVIDIA " + } + if strings.Contains(strings.ToLower(raw), "intel") { + vendor = "Intel " + } + if strings.Contains(strings.ToLower(raw), "amd") { + vendor = "AMD " + } + + res = vendor + match[1] + } + + re := regexp.MustCompile(`\(.*?\)|\[.*?]`) + res = re.ReplaceAllString(res, "") + + fluff := []string{"Corporation", "Controller", "VGA compatible", "3D", "Graphics", "Device"} + for _, word := range fluff { + res = strings.ReplaceAll(res, word, "") + } + + return strings.Join(strings.Fields(res), " ") +} + +func GetKernel() string { + out, err := exec.Command("sh", "-c", "uname -r").Output() + if err != nil { + return "Error getting kernel, how did we get there?" + } + return strings.TrimSpace(string(out)) +} + +func GetHostname() string { + hostname, err := exec.Command("sh", "-c", "uname -n").Output() + if err != nil { + return "Error getting hostname, awful" + } + username, errUser := exec.Command("sh", "-c", "whoami").Output() + if errUser != nil { + return "Error getting username, nobody here but us chickens!" + } + fullHostname := fmt.Sprintf("%s@%s", strings.TrimSpace(string(username)), strings.TrimSpace(string(hostname))) + return fullHostname +} + +func GetTerminal() string { + return os.Getenv("TERM") +} + +func GetDE() string { + de := os.Getenv("XDG_CURRENT_DESKTOP") + if de == "" { + return "Unknown or w/o GUI" + } + return strings.TrimSpace(de) +} + +func GetUptime() string { + data, err := os.ReadFile("/proc/uptime") + if err != nil { + return "0" + } + uptimeStr := strings.Fields(string(data))[0] + seconds, err := strconv.ParseFloat(uptimeStr, 64) + if err != nil { + _, err := fmt.Fprintf(os.Stderr, "Error: %v\n", err) + if err != nil { + return "error printing uptime" + } + return "" + } + d := time.Duration(seconds) * time.Second + formatted := d.Round(time.Second).String() + spaced := strings.NewReplacer("h", "h ", "m", "m ", "s", "s ").Replace(formatted) + return spaced +} + +func GetShell() string { + out, err := exec.Command("ps", "-p", strconv.Itoa(os.Getppid()), "-o", "comm=").Output() + if err != nil { + return path.Base(os.Getenv("SHELL")) + } + return strings.TrimSpace(string(out)) +}