diff --git a/.gitignore b/.gitignore index 8970f30..4a61744 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ result/ /.idea/go.imports.xml /.idea/vcs.xml /.idea/workspace.xml +/goGoFetch diff --git a/README.md b/README.md index 6974cd3..c0c7f8a 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,18 @@ to show custom image and set custom width: ```bash gogofetch -l path -w 250 ``` +to place logo on the left (default) or right: +```bash +gogofetch -p left +gogofetch -p right +``` to show help message: ```bash gogofetch -h ``` +config file is created at `~/.config/gogofetch/config.toml`, all parameters are described in config file. + # NixOS credits to melvi for flake diff --git a/logos/arch.png b/logos/arch.png index d3373c4..2069cfe 100644 --- a/logos/arch.png +++ b/logos/arch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da3e518a7984b9dbace1c3de8c2ccdda0b7750558e4650ffbc45a50ebbb33072 -size 96910 +oid sha256:3fceb72a39b217ddd9aec33440cb43a84cb71cd17ef1e76c29352c138deb3a08 +size 51016 diff --git a/logos/debian.png b/logos/debian.png index 1887729..2a87e91 100644 --- a/logos/debian.png +++ b/logos/debian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3abbf6d15a7cfe184bf8a2bb85e82bce205251ee2ae41db02a65e35a199f16f -size 20619 +oid sha256:ac0d961e17dd3c90f4980e04db3aabaee1ea4998fbba8528a2652ed3dc4efce2 +size 69548 diff --git a/main.go b/main.go index de08553..6643521 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" "time" + "unicode/utf8" "github.com/BurntSushi/toml" ) @@ -24,10 +25,63 @@ const ( 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"` + 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 = [ + "dist", + "host", + "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) { @@ -76,6 +130,9 @@ layout = [ # 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 { @@ -83,8 +140,18 @@ custom_logo = "" } } - if _, err := toml.DecodeFile(pathConf, &conf); err != nil { + 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 @@ -325,13 +392,88 @@ func printAsciiLogo(path string) string { return string(content) } -func printLogoChafa(path string, width uint) { +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, sizeArg, "--symbols=half") - cmd.Stdout = os.Stdout - if err := cmd.Run(); err != nil { - fmt.Println("Chafa error:", err) + 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) } } @@ -414,9 +556,11 @@ func main() { defaultLogo = getLogo(dist) } - width := flag.Int("w", 200, "Logo width in lines") + 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 @@ -451,18 +595,21 @@ func main() { } } - if _, err := os.Stat(*logoPath); err == nil { - if conf.Ascii { - fmt.Print(printAsciiLogo(*logoPath)) - fmt.Println() - } else { - printLogoChafa(*logoPath, uint(*width)) - } - } else { - fmt.Printf("Logo not found at: %s\n", *logoPath) + var statsLines []string + for _, info := range stats { + statsLines = append(statsLines, fmt.Sprintf("~ %s%s%s: %s", Blue, info.Label, Reset, info.Value)) } - for _, info := range stats { - fmt.Printf("~ %s%s%s: %s\n", 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) }