package main import ( "fmt" yaml "gopkg.in/yaml.v3" "log" "os" "os/exec" "path" "regexp" "strconv" "strings" ) var configPath = "" var fetchScriptPath = "" var config = StormfetchConfig{} type StormfetchConfig struct { Ascii string `yaml:"distro_ascii"` FetchScript string `yaml:"fetch_script"` AnsiiColors []int `yaml:"ansii_colors"` DependencyWarning bool `yaml:"dependency_warning"` } type DistroInfo struct { ID string LongName string ShortName string } func main() { readConfig() } func readConfig() { // Get home directory configDir, _ := os.UserConfigDir() // Find valid config directory if _, err := os.Stat(path.Join(configDir, "stormfetch/config.yaml")); err == nil { configPath = path.Join(configDir, "stormfetch/config.yaml") } else if _, err := os.Stat("/etc/stormfetch/config.yaml"); err == nil { configPath = "/etc/stormfetch/config.yaml" } else { log.Fatalf("Config file not found: %s", err.Error()) } // Parse config bytes, err := os.ReadFile(configPath) if err != nil { log.Fatal(err) } err = yaml.Unmarshal(bytes, &config) if err != nil { log.Fatal(err) } if config.FetchScript == "" { log.Fatalf("Fetch script path is empty") } else if config.FetchScript != "auto" { stat, err := os.Stat(config.FetchScript) if err != nil { log.Fatalf("Fetch script file not found: %s", err.Error()) } else if stat.IsDir() { log.Fatalf("Fetch script path points to a directory") } } if _, err := os.Stat(path.Join(configDir, "stormfetch/fetch_script.sh")); err == nil { fetchScriptPath = path.Join(configDir, "stormfetch/fetch_script.sh") } else if _, err := os.Stat("/etc/stormfetch/fetch_script.sh"); err == nil { fetchScriptPath = "/etc/stormfetch/fetch_script.sh" } else { log.Fatalf("Fetch script file not found: %s", err.Error()) } // Show Dependency warning if enabled if config.DependencyWarning { dependencies := []string{"lshw", "xhost", "xdpyinfo"} 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") } } //Execute fetch script cmd := exec.Command("/bin/bash", fetchScriptPath) cmd.Dir = path.Dir(fetchScriptPath) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, "C0=\033[0m") for i, color := range config.AnsiiColors { if i > 6 { break } cmd.Env = append(cmd.Env, fmt.Sprintf("C%d=\033[38;5;%dm", i+1, color)) } cmd.Env = append(cmd.Env, "DISTRO_LONG_NAME="+getDistroInfo().LongName) cmd.Env = append(cmd.Env, "DISTRO_SHORT_NAME="+getDistroInfo().ShortName) out, err := cmd.Output() if err != nil { log.Fatal(err) } // Fetch ascii art and apply colors colorMap := make(map[string]string) colorMap["C0"] = "C0=\033[0m" for i, color := range config.AnsiiColors { if i > 6 { break } colorMap["C"+strconv.Itoa(i+1)] = fmt.Sprintf("\033[38;5;%dm", color) } ascii := os.Expand(getDistroAsciiArt(), func(s string) string { return colorMap[s] }) asciiSplit := strings.Split(ascii, "\n") asciiNoColor := stripAnsii(ascii) // Print Distro Information maxWidth := 0 for _, line := range strings.Split(asciiNoColor, "\n") { if len(line) > maxWidth { maxWidth = len(line) } } final := "" for lineIndex, line := range asciiSplit { lineVisibleLength := len(strings.Split(asciiNoColor, "\n")[lineIndex]) lastAsciiColor := "" if lineIndex != 0 { r := regexp.MustCompile("\033[38;5;[0-9]+m") matches := r.FindAllString(asciiSplit[lineIndex-1], -1) if len(matches) != 0 { lastAsciiColor = r.FindAllString(asciiSplit[lineIndex-1], -1)[len(matches)-1] } } for i := lineVisibleLength; i < maxWidth+5; i++ { line = line + " " } asciiSplit[lineIndex] = lastAsciiColor + line str := string(out) if lineIndex < len(strings.Split(str, "\n")) { line = line + strings.Split(str, "\n")[lineIndex] } final += lastAsciiColor + line + "\n" } fmt.Println(final) } 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 } func getDistroInfo() DistroInfo { distroID := "" 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 DistroInfo{ ID: "unknown", LongName: "Unknown", ShortName: "Unknown", } } if value, ok := releaseMap["ID"]; ok { distroID = value } } switch distroID { default: if id, ok := releaseMap["ID"]; ok { if longName, ok := releaseMap["PRETTY_NAME"]; ok { if shortName, ok := releaseMap["NAME"]; ok { return DistroInfo{ ID: id, LongName: longName, ShortName: shortName, } } } } return DistroInfo{ ID: "unknown", LongName: "Unknown", ShortName: "Unknown", } } } 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("/etc/stormfetch/ascii/", id)); err == nil { bytes, err := os.ReadFile(path.Join("/etc/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("/etc/stormfetch/ascii/", id)); err == nil { bytes, err := os.ReadFile(path.Join("/etc/stormfetch/ascii/", id)) if err != nil { return defaultAscii } return string(bytes) } else { return defaultAscii } } 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, "") }