nandi/nimfetchpublic Fork 0
e6e2cdc
Commits
Clone
git clone https://git.rickub.com/nandi/nimfetch.git
git clone ssh://git@rickub.com/nandi/nimfetch.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Initial nimfetch implementation

nandithebull committed 2026-09-20T23:10:52-07:00 Browse files
e6e2cdc
added .gitignore +4 -0
new file mode 100644
@@ -0,0 +1,4 @@
1+nimfetch
2+bin/
3+nimcache/
4+tests/test_nimfetch
new file mode 100644
@@ -0,0 +1,4 @@
1+nimfetch
2+bin/
3+nimcache/
4+tests/test_nimfetch
added LICENSE +21 -0
new file mode 100644
@@ -0,0 +1,21 @@
1+MIT License
2+
3+Copyright (c) 2026 nimfetch contributors
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.
new file mode 100644
@@ -0,0 +1,21 @@
1+MIT License
2+
3+Copyright (c) 2026 nimfetch contributors
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.
added README.md +34 -0
new file mode 100644
@@ -0,0 +1,34 @@
1+# nimfetch
2+
3+A small, fastfetch-inspired system information tool written in Nim. It has no
4+runtime dependencies and uses the normal system utilities available on the host.
5+
6+## Build
7+
8+```sh
9+nimble build -d:release
10+./nimfetch
11+```
12+
13+Or compile directly:
14+
15+```sh
16+nim c -d:release -o:nimfetch src/nimfetch.nim
17+```
18+
19+## Usage
20+
21+```text
22+nimfetch [--no-logo] [--logo arch|ubuntu|debian|fedora|linux]
23+ [--no-color] [--json] [--version] [--help]
24+```
25+
26+The implementation targets Linux first, with basic macOS and Windows fallbacks.
27+Missing optional commands such as `lspci`, `ip`, or a package manager are shown
28+as `Unknown` instead of causing the program to fail.
29+
30+## Test
31+
32+```sh
33+nimble test
34+```
new file mode 100644
@@ -0,0 +1,34 @@
1+# nimfetch
2+
3+A small, fastfetch-inspired system information tool written in Nim. It has no
4+runtime dependencies and uses the normal system utilities available on the host.
5+
6+## Build
7+
8+```sh
9+nimble build -d:release
10+./nimfetch
11+```
12+
13+Or compile directly:
14+
15+```sh
16+nim c -d:release -o:nimfetch src/nimfetch.nim
17+```
18+
19+## Usage
20+
21+```text
22+nimfetch [--no-logo] [--logo arch|ubuntu|debian|fedora|linux]
23+ [--no-color] [--json] [--version] [--help]
24+```
25+
26+The implementation targets Linux first, with basic macOS and Windows fallbacks.
27+Missing optional commands such as `lspci`, `ip`, or a package manager are shown
28+as `Unknown` instead of causing the program to fail.
29+
30+## Test
31+
32+```sh
33+nimble test
34+```
added nimfetch.nimble +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+version = "0.1.0"
2+author = "nimfetch contributors"
3+description = "A fast, dependency-free system information tool written in Nim"
4+license = "MIT"
5+srcDir = "src"
6+bin = @["nimfetch"]
7+
8+requires "nim >= 2.0.0"
9+
10+task test, "Run the test suite":
11+ exec "nim c -r --nimcache:/tmp/nimfetch-tests --path:src tests/test_nimfetch.nim"
12+
13+task release, "Build an optimized binary":
14+ exec "nim c -d:release --opt:speed --nimcache:/tmp/nimfetch-release -o:bin/nimfetch src/nimfetch.nim"
new file mode 100644
@@ -0,0 +1,14 @@
1+version = "0.1.0"
2+author = "nimfetch contributors"
3+description = "A fast, dependency-free system information tool written in Nim"
4+license = "MIT"
5+srcDir = "src"
6+bin = @["nimfetch"]
7+
8+requires "nim >= 2.0.0"
9+
10+task test, "Run the test suite":
11+ exec "nim c -r --nimcache:/tmp/nimfetch-tests --path:src tests/test_nimfetch.nim"
12+
13+task release, "Build an optimized binary":
14+ exec "nim c -d:release --opt:speed --nimcache:/tmp/nimfetch-release -o:bin/nimfetch src/nimfetch.nim"
added src/nimfetch.nim +105 -0
new file mode 100644
@@ -0,0 +1,105 @@
1+import std/[json, os, sequtils, strutils, terminal]
2+import sysinfo
3+
4+const Version = "0.1.0"
5+
6+type Options = object
7+ logo, color, json: bool
8+
9+type DisplayRow = object
10+ key, value: string
11+ isTitle, isSeparator, isPalette: bool
12+
13+proc logoFor(name: string): seq[string] =
14+ case name
15+ of "arch": @[r" /\", r" / \", r" /\ \", r" / \",
16+ r" / ,, \", r" / | | -\", r" /_-'' ''-_\"]
17+ of "ubuntu": @[" _", " ---(@)", r" _/ --- \", "(@) | |",
18+ r" \ --- _/", " ---(@)"]
19+ of "debian": @[" _____", r" / __ \", "| / |", r"| \___-", "-_", " --_"]
20+ of "fedora": @[" _____", r" / __)\", r" | / \", " ___| |__/",
21+ " / (_ _)", "/ / | |", r"\ \__/ |", r" \_____/"]
22+ else: @[" .--.", " |o_o |", " |:_/ |", r" // \ \", " (| | )",
23+ r"/'\_ _/'\", r"\___)=(___/"]
24+
25+proc help() =
26+ echo "nimfetch ", Version, " - fast system information in Nim"
27+ echo ""
28+ echo "Usage: nimfetch [options]"
29+ echo " --no-logo Hide the ASCII logo"
30+ echo " --logo <name> Use arch, ubuntu, debian, fedora, or linux"
31+ echo " --no-color Disable ANSI colors"
32+ echo " --json Print machine-readable JSON"
33+ echo " -v, --version Show version"
34+ echo " -h, --help Show this help"
35+
36+proc parseOptions(): tuple[options: Options, logoName: string] =
37+ result.options = Options(logo: true, color: true)
38+ var i = 1
39+ while i <= paramCount():
40+ case paramStr(i)
41+ of "--no-logo": result.options.logo = false
42+ of "--no-color": result.options.color = false
43+ of "--json": result.options.json = true; result.options.logo = false
44+ of "--logo":
45+ inc i
46+ if i > paramCount():
47+ stderr.writeLine "nimfetch: --logo requires a name"
48+ quit 2
49+ result.logoName = paramStr(i).toLowerAscii
50+ of "-v", "--version": echo Version; quit 0
51+ of "-h", "--help": help(); quit 0
52+ else:
53+ stderr.writeLine "nimfetch: unknown option: " & paramStr(i)
54+ stderr.writeLine "Try 'nimfetch --help'."
55+ quit 2
56+ inc i
57+
58+proc colored(text, code: string, enabled: bool): string =
59+ if enabled: "\e[" & code & "m" & text & "\e[0m" else: text
60+
61+proc main() =
62+ var (options, logoName) = parseOptions()
63+ if not isatty(stdout): options.color = false
64+ let info = collectInfo()
65+
66+ if options.json:
67+ var output = newJObject()
68+ for item in info:
69+ output[item.key.toLowerAscii.replace(" ", "_").replace("(/)", "root")] = %item.value
70+ echo pretty(output)
71+ return
72+
73+ if logoName.len == 0: logoName = osRelease().id
74+ if logoName notin ["arch", "ubuntu", "debian", "fedora", "linux"]: logoName = "linux"
75+ let logo = if options.logo: logoFor(logoName) else: @[]
76+ var displayRows: seq[DisplayRow]
77+ for item in info:
78+ if item.key == "Title":
79+ displayRows.add DisplayRow(value: item.value, isTitle: true)
80+ displayRows.add DisplayRow(value: "-".repeat(item.value.len), isSeparator: true)
81+ else:
82+ displayRows.add DisplayRow(key: item.key, value: item.value)
83+ displayRows.add DisplayRow(value: "● ● ● ● ● ● ● ●", isPalette: true)
84+
85+ let rows = max(logo.len, displayRows.len)
86+ let width = if logo.len > 0: logo.mapIt(it.len).max + 4 else: 0
87+
88+ for i in 0 ..< rows:
89+ let left = if i < logo.len: colored(logo[i].alignLeft(width), "36;1", options.color)
90+ else: " ".repeat(width)
91+ if i < displayRows.len:
92+ let row = displayRows[i]
93+ if row.isTitle:
94+ echo left, colored(row.value, "32;1", options.color)
95+ elif row.isSeparator:
96+ echo left, colored(row.value, "32", options.color)
97+ elif row.isPalette:
98+ echo left, colored(row.value, "36;1", options.color)
99+ else:
100+ echo left, colored(row.key & ":", "34;1", options.color), " ", row.value
101+ else:
102+ echo left
103+
104+when isMainModule:
105+ main()
new file mode 100644
@@ -0,0 +1,105 @@
1+import std/[json, os, sequtils, strutils, terminal]
2+import sysinfo
3+
4+const Version = "0.1.0"
5+
6+type Options = object
7+ logo, color, json: bool
8+
9+type DisplayRow = object
10+ key, value: string
11+ isTitle, isSeparator, isPalette: bool
12+
13+proc logoFor(name: string): seq[string] =
14+ case name
15+ of "arch": @[r" /\", r" / \", r" /\ \", r" / \",
16+ r" / ,, \", r" / | | -\", r" /_-'' ''-_\"]
17+ of "ubuntu": @[" _", " ---(@)", r" _/ --- \", "(@) | |",
18+ r" \ --- _/", " ---(@)"]
19+ of "debian": @[" _____", r" / __ \", "| / |", r"| \___-", "-_", " --_"]
20+ of "fedora": @[" _____", r" / __)\", r" | / \", " ___| |__/",
21+ " / (_ _)", "/ / | |", r"\ \__/ |", r" \_____/"]
22+ else: @[" .--.", " |o_o |", " |:_/ |", r" // \ \", " (| | )",
23+ r"/'\_ _/'\", r"\___)=(___/"]
24+
25+proc help() =
26+ echo "nimfetch ", Version, " - fast system information in Nim"
27+ echo ""
28+ echo "Usage: nimfetch [options]"
29+ echo " --no-logo Hide the ASCII logo"
30+ echo " --logo <name> Use arch, ubuntu, debian, fedora, or linux"
31+ echo " --no-color Disable ANSI colors"
32+ echo " --json Print machine-readable JSON"
33+ echo " -v, --version Show version"
34+ echo " -h, --help Show this help"
35+
36+proc parseOptions(): tuple[options: Options, logoName: string] =
37+ result.options = Options(logo: true, color: true)
38+ var i = 1
39+ while i <= paramCount():
40+ case paramStr(i)
41+ of "--no-logo": result.options.logo = false
42+ of "--no-color": result.options.color = false
43+ of "--json": result.options.json = true; result.options.logo = false
44+ of "--logo":
45+ inc i
46+ if i > paramCount():
47+ stderr.writeLine "nimfetch: --logo requires a name"
48+ quit 2
49+ result.logoName = paramStr(i).toLowerAscii
50+ of "-v", "--version": echo Version; quit 0
51+ of "-h", "--help": help(); quit 0
52+ else:
53+ stderr.writeLine "nimfetch: unknown option: " & paramStr(i)
54+ stderr.writeLine "Try 'nimfetch --help'."
55+ quit 2
56+ inc i
57+
58+proc colored(text, code: string, enabled: bool): string =
59+ if enabled: "\e[" & code & "m" & text & "\e[0m" else: text
60+
61+proc main() =
62+ var (options, logoName) = parseOptions()
63+ if not isatty(stdout): options.color = false
64+ let info = collectInfo()
65+
66+ if options.json:
67+ var output = newJObject()
68+ for item in info:
69+ output[item.key.toLowerAscii.replace(" ", "_").replace("(/)", "root")] = %item.value
70+ echo pretty(output)
71+ return
72+
73+ if logoName.len == 0: logoName = osRelease().id
74+ if logoName notin ["arch", "ubuntu", "debian", "fedora", "linux"]: logoName = "linux"
75+ let logo = if options.logo: logoFor(logoName) else: @[]
76+ var displayRows: seq[DisplayRow]
77+ for item in info:
78+ if item.key == "Title":
79+ displayRows.add DisplayRow(value: item.value, isTitle: true)
80+ displayRows.add DisplayRow(value: "-".repeat(item.value.len), isSeparator: true)
81+ else:
82+ displayRows.add DisplayRow(key: item.key, value: item.value)
83+ displayRows.add DisplayRow(value: "● ● ● ● ● ● ● ●", isPalette: true)
84+
85+ let rows = max(logo.len, displayRows.len)
86+ let width = if logo.len > 0: logo.mapIt(it.len).max + 4 else: 0
87+
88+ for i in 0 ..< rows:
89+ let left = if i < logo.len: colored(logo[i].alignLeft(width), "36;1", options.color)
90+ else: " ".repeat(width)
91+ if i < displayRows.len:
92+ let row = displayRows[i]
93+ if row.isTitle:
94+ echo left, colored(row.value, "32;1", options.color)
95+ elif row.isSeparator:
96+ echo left, colored(row.value, "32", options.color)
97+ elif row.isPalette:
98+ echo left, colored(row.value, "36;1", options.color)
99+ else:
100+ echo left, colored(row.key & ":", "34;1", options.color), " ", row.value
101+ else:
102+ echo left
103+
104+when isMainModule:
105+ main()
added src/sysinfo.nim +173 -0
new file mode 100644
@@ -0,0 +1,173 @@
1+import std/[os, osproc, strformat, strutils]
2+
3+type
4+ InfoItem* = object
5+ key*, value*: string
6+
7+proc readFirst(path: string): string =
8+ try:
9+ readFile(path).strip
10+ except CatchableError:
11+ ""
12+
13+proc command(cmd: string): string =
14+ try:
15+ let execution = execCmdEx(cmd, options = {poUsePath, poStdErrToStdOut})
16+ if execution.exitCode == 0: execution.output.strip else: ""
17+ except CatchableError:
18+ ""
19+
20+proc osRelease*(): tuple[name, id: string] =
21+ result = (hostOS.capitalizeAscii, hostOS)
22+ when defined(linux):
23+ for line in readFirst("/etc/os-release").splitLines:
24+ let pos = line.find('=')
25+ if pos < 1: continue
26+ let key = line[0 ..< pos]
27+ let value = line[pos + 1 .. ^1].strip(chars = {'"', '\''})
28+ case key
29+ of "PRETTY_NAME": result.name = value
30+ of "ID": result.id = value.toLowerAscii
31+ else: discard
32+
33+proc formatBytes*(bytes: int64): string =
34+ const units = ["B", "KiB", "MiB", "GiB", "TiB"]
35+ var value = bytes.float
36+ var unit = 0
37+ while value >= 1024 and unit < units.high:
38+ value /= 1024
39+ inc unit
40+ if unit == 0: &"{value.int} {units[unit]}" else: &"{value:.2f} {units[unit]}"
41+
42+proc formatDuration*(seconds: int64): string =
43+ let days = seconds div 86400
44+ let hours = (seconds mod 86400) div 3600
45+ let minutes = (seconds mod 3600) div 60
46+ var parts: seq[string]
47+ if days > 0: parts.add &"{days}d"
48+ if hours > 0: parts.add &"{hours}h"
49+ parts.add &"{minutes}m"
50+ parts.join(" ")
51+
52+proc kernel(): string =
53+ when defined(windows):
54+ result = command("ver")
55+ else:
56+ result = command("uname -sr")
57+
58+proc uptime(): string =
59+ when defined(linux):
60+ let raw = readFirst("/proc/uptime").splitWhitespace
61+ if raw.len > 0:
62+ try: return formatDuration(int64(parseFloat(raw[0])))
63+ except ValueError: discard
64+ elif defined(macosx):
65+ import std/[parseutils, times]
66+ let boot = command("sysctl -n kern.boottime")
67+ let marker = "sec = "
68+ let start = boot.find(marker)
69+ if start >= 0:
70+ var epoch: int
71+ discard parseInt(boot, epoch, start + marker.len)
72+ return formatDuration(getTime().toUnix - epoch)
73+ "Unknown"
74+
75+proc cpu(): string =
76+ when defined(linux):
77+ for line in readFirst("/proc/cpuinfo").splitLines:
78+ if line.startsWith("model name") or line.startsWith("Hardware"):
79+ let pos = line.find(':')
80+ if pos >= 0: return line[pos + 1 .. ^1].strip
81+ elif defined(macosx):
82+ result = command("sysctl -n machdep.cpu.brand_string")
83+ elif defined(windows):
84+ result = getEnv("PROCESSOR_IDENTIFIER")
85+ if result.len == 0: result = "Unknown"
86+
87+proc gpu(): string =
88+ when defined(linux):
89+ result = command("lspci 2>/dev/null | sed -n '/VGA\\|3D controller/{s/.*: //;p;q;}'")
90+ elif defined(macosx):
91+ result = command("system_profiler SPDisplaysDataType | awk -F': ' '/Chipset Model/{print $2; exit}'")
92+ if result.len == 0: result = "Unknown"
93+
94+proc memory(): string =
95+ when defined(linux):
96+ var total, available: int64
97+ for line in readFirst("/proc/meminfo").splitLines:
98+ let fields = line.splitWhitespace
99+ if fields.len < 2: continue
100+ try:
101+ if fields[0] == "MemTotal:": total = parseInt(fields[1]).int64 * 1024
102+ elif fields[0] == "MemAvailable:": available = parseInt(fields[1]).int64 * 1024
103+ except ValueError: discard
104+ if total > 0: return &"{formatBytes(total - available)} / {formatBytes(total)}"
105+ elif defined(macosx):
106+ let total = command("sysctl -n hw.memsize")
107+ if total.len > 0: return formatBytes(parseBiggestInt(total))
108+ "Unknown"
109+
110+proc disk(): string =
111+ when defined(posix):
112+ let line = command("df -Pk / | tail -1").splitWhitespace
113+ if line.len >= 5:
114+ try:
115+ let total = parseBiggestInt(line[1]) * 1024
116+ let used = parseBiggestInt(line[2]) * 1024
117+ return &"{formatBytes(used)} / {formatBytes(total)} ({line[4]})"
118+ except ValueError: discard
119+ "Unknown"
120+
121+proc packageCount(): string =
122+ var counts: seq[string]
123+ when defined(linux):
124+ let managers = [
125+ ("dpkg", "dpkg-query -f '${binary:Package}\\n' -W 2>/dev/null | wc -l"),
126+ ("rpm", "rpm -qa 2>/dev/null | wc -l"),
127+ ("pacman", "pacman -Qq 2>/dev/null | wc -l"),
128+ ("apk", "apk info 2>/dev/null | wc -l"),
129+ ("flatpak", "flatpak list --app --columns=application 2>/dev/null | wc -l")]
130+ for (manager, cmd) in managers.items:
131+ if findExe(manager).len > 0:
132+ let count = command(cmd)
133+ if count.len > 0 and count != "0": counts.add &"{count} ({manager})"
134+ if counts.len == 0: "Unknown" else: counts.join(", ")
135+
136+proc desktop(): string =
137+ result = getEnv("XDG_CURRENT_DESKTOP")
138+ if result.len == 0: result = getEnv("DESKTOP_SESSION")
139+ if result.len == 0: result = "TTY"
140+
141+proc localIp(): string =
142+ when defined(linux):
143+ let route = command("ip route get 1.1.1.1 2>/dev/null")
144+ let words = route.splitWhitespace
145+ let pos = words.find("src")
146+ if pos >= 0 and pos + 1 < words.len: return words[pos + 1]
147+ let addresses = command("hostname -I 2>/dev/null").splitWhitespace
148+ if addresses.len > 0: return addresses[0]
149+ result = "Unknown"
150+
151+proc collectInfo*(): seq[InfoItem] =
152+ let distro = osRelease()
153+ let user = getEnv("USER", getEnv("USERNAME", "user"))
154+ let host = getEnv("HOSTNAME", command("hostname"))
155+ let shellPath = getEnv("SHELL", getEnv("COMSPEC", "Unknown"))
156+ let terminal = getEnv("TERM_PROGRAM", getEnv("TERM", "Unknown"))
157+ result = @[
158+ InfoItem(key: "Title", value: user & "@" & host),
159+ InfoItem(key: "OS", value: distro.name),
160+ InfoItem(key: "Host", value: host),
161+ InfoItem(key: "Kernel", value: kernel()),
162+ InfoItem(key: "Uptime", value: uptime()),
163+ InfoItem(key: "Packages", value: packageCount()),
164+ InfoItem(key: "Shell", value: extractFilename(shellPath)),
165+ InfoItem(key: "Display", value: getEnv("WAYLAND_DISPLAY", getEnv("DISPLAY", "TTY"))),
166+ InfoItem(key: "DE/WM", value: desktop()),
167+ InfoItem(key: "Terminal", value: terminal),
168+ InfoItem(key: "CPU", value: cpu()),
169+ InfoItem(key: "GPU", value: gpu()),
170+ InfoItem(key: "Memory", value: memory()),
171+ InfoItem(key: "Disk (/)" , value: disk()),
172+ InfoItem(key: "Local IP", value: localIp()),
173+ InfoItem(key: "Locale", value: getEnv("LANG", "Unknown"))]
new file mode 100644
@@ -0,0 +1,173 @@
1+import std/[os, osproc, strformat, strutils]
2+
3+type
4+ InfoItem* = object
5+ key*, value*: string
6+
7+proc readFirst(path: string): string =
8+ try:
9+ readFile(path).strip
10+ except CatchableError:
11+ ""
12+
13+proc command(cmd: string): string =
14+ try:
15+ let execution = execCmdEx(cmd, options = {poUsePath, poStdErrToStdOut})
16+ if execution.exitCode == 0: execution.output.strip else: ""
17+ except CatchableError:
18+ ""
19+
20+proc osRelease*(): tuple[name, id: string] =
21+ result = (hostOS.capitalizeAscii, hostOS)
22+ when defined(linux):
23+ for line in readFirst("/etc/os-release").splitLines:
24+ let pos = line.find('=')
25+ if pos < 1: continue
26+ let key = line[0 ..< pos]
27+ let value = line[pos + 1 .. ^1].strip(chars = {'"', '\''})
28+ case key
29+ of "PRETTY_NAME": result.name = value
30+ of "ID": result.id = value.toLowerAscii
31+ else: discard
32+
33+proc formatBytes*(bytes: int64): string =
34+ const units = ["B", "KiB", "MiB", "GiB", "TiB"]
35+ var value = bytes.float
36+ var unit = 0
37+ while value >= 1024 and unit < units.high:
38+ value /= 1024
39+ inc unit
40+ if unit == 0: &"{value.int} {units[unit]}" else: &"{value:.2f} {units[unit]}"
41+
42+proc formatDuration*(seconds: int64): string =
43+ let days = seconds div 86400
44+ let hours = (seconds mod 86400) div 3600
45+ let minutes = (seconds mod 3600) div 60
46+ var parts: seq[string]
47+ if days > 0: parts.add &"{days}d"
48+ if hours > 0: parts.add &"{hours}h"
49+ parts.add &"{minutes}m"
50+ parts.join(" ")
51+
52+proc kernel(): string =
53+ when defined(windows):
54+ result = command("ver")
55+ else:
56+ result = command("uname -sr")
57+
58+proc uptime(): string =
59+ when defined(linux):
60+ let raw = readFirst("/proc/uptime").splitWhitespace
61+ if raw.len > 0:
62+ try: return formatDuration(int64(parseFloat(raw[0])))
63+ except ValueError: discard
64+ elif defined(macosx):
65+ import std/[parseutils, times]
66+ let boot = command("sysctl -n kern.boottime")
67+ let marker = "sec = "
68+ let start = boot.find(marker)
69+ if start >= 0:
70+ var epoch: int
71+ discard parseInt(boot, epoch, start + marker.len)
72+ return formatDuration(getTime().toUnix - epoch)
73+ "Unknown"
74+
75+proc cpu(): string =
76+ when defined(linux):
77+ for line in readFirst("/proc/cpuinfo").splitLines:
78+ if line.startsWith("model name") or line.startsWith("Hardware"):
79+ let pos = line.find(':')
80+ if pos >= 0: return line[pos + 1 .. ^1].strip
81+ elif defined(macosx):
82+ result = command("sysctl -n machdep.cpu.brand_string")
83+ elif defined(windows):
84+ result = getEnv("PROCESSOR_IDENTIFIER")
85+ if result.len == 0: result = "Unknown"
86+
87+proc gpu(): string =
88+ when defined(linux):
89+ result = command("lspci 2>/dev/null | sed -n '/VGA\\|3D controller/{s/.*: //;p;q;}'")
90+ elif defined(macosx):
91+ result = command("system_profiler SPDisplaysDataType | awk -F': ' '/Chipset Model/{print $2; exit}'")
92+ if result.len == 0: result = "Unknown"
93+
94+proc memory(): string =
95+ when defined(linux):
96+ var total, available: int64
97+ for line in readFirst("/proc/meminfo").splitLines:
98+ let fields = line.splitWhitespace
99+ if fields.len < 2: continue
100+ try:
101+ if fields[0] == "MemTotal:": total = parseInt(fields[1]).int64 * 1024
102+ elif fields[0] == "MemAvailable:": available = parseInt(fields[1]).int64 * 1024
103+ except ValueError: discard
104+ if total > 0: return &"{formatBytes(total - available)} / {formatBytes(total)}"
105+ elif defined(macosx):
106+ let total = command("sysctl -n hw.memsize")
107+ if total.len > 0: return formatBytes(parseBiggestInt(total))
108+ "Unknown"
109+
110+proc disk(): string =
111+ when defined(posix):
112+ let line = command("df -Pk / | tail -1").splitWhitespace
113+ if line.len >= 5:
114+ try:
115+ let total = parseBiggestInt(line[1]) * 1024
116+ let used = parseBiggestInt(line[2]) * 1024
117+ return &"{formatBytes(used)} / {formatBytes(total)} ({line[4]})"
118+ except ValueError: discard
119+ "Unknown"
120+
121+proc packageCount(): string =
122+ var counts: seq[string]
123+ when defined(linux):
124+ let managers = [
125+ ("dpkg", "dpkg-query -f '${binary:Package}\\n' -W 2>/dev/null | wc -l"),
126+ ("rpm", "rpm -qa 2>/dev/null | wc -l"),
127+ ("pacman", "pacman -Qq 2>/dev/null | wc -l"),
128+ ("apk", "apk info 2>/dev/null | wc -l"),
129+ ("flatpak", "flatpak list --app --columns=application 2>/dev/null | wc -l")]
130+ for (manager, cmd) in managers.items:
131+ if findExe(manager).len > 0:
132+ let count = command(cmd)
133+ if count.len > 0 and count != "0": counts.add &"{count} ({manager})"
134+ if counts.len == 0: "Unknown" else: counts.join(", ")
135+
136+proc desktop(): string =
137+ result = getEnv("XDG_CURRENT_DESKTOP")
138+ if result.len == 0: result = getEnv("DESKTOP_SESSION")
139+ if result.len == 0: result = "TTY"
140+
141+proc localIp(): string =
142+ when defined(linux):
143+ let route = command("ip route get 1.1.1.1 2>/dev/null")
144+ let words = route.splitWhitespace
145+ let pos = words.find("src")
146+ if pos >= 0 and pos + 1 < words.len: return words[pos + 1]
147+ let addresses = command("hostname -I 2>/dev/null").splitWhitespace
148+ if addresses.len > 0: return addresses[0]
149+ result = "Unknown"
150+
151+proc collectInfo*(): seq[InfoItem] =
152+ let distro = osRelease()
153+ let user = getEnv("USER", getEnv("USERNAME", "user"))
154+ let host = getEnv("HOSTNAME", command("hostname"))
155+ let shellPath = getEnv("SHELL", getEnv("COMSPEC", "Unknown"))
156+ let terminal = getEnv("TERM_PROGRAM", getEnv("TERM", "Unknown"))
157+ result = @[
158+ InfoItem(key: "Title", value: user & "@" & host),
159+ InfoItem(key: "OS", value: distro.name),
160+ InfoItem(key: "Host", value: host),
161+ InfoItem(key: "Kernel", value: kernel()),
162+ InfoItem(key: "Uptime", value: uptime()),
163+ InfoItem(key: "Packages", value: packageCount()),
164+ InfoItem(key: "Shell", value: extractFilename(shellPath)),
165+ InfoItem(key: "Display", value: getEnv("WAYLAND_DISPLAY", getEnv("DISPLAY", "TTY"))),
166+ InfoItem(key: "DE/WM", value: desktop()),
167+ InfoItem(key: "Terminal", value: terminal),
168+ InfoItem(key: "CPU", value: cpu()),
169+ InfoItem(key: "GPU", value: gpu()),
170+ InfoItem(key: "Memory", value: memory()),
171+ InfoItem(key: "Disk (/)" , value: disk()),
172+ InfoItem(key: "Local IP", value: localIp()),
173+ InfoItem(key: "Locale", value: getEnv("LANG", "Unknown"))]
added tests/test_nimfetch.nim +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+import std/[unittest]
2+import sysinfo
3+
4+suite "nimfetch helpers":
5+ test "formats byte quantities":
6+ check formatBytes(0) == "0 B"
7+ check formatBytes(1024) == "1.00 KiB"
8+ check formatBytes(1_073_741_824) == "1.00 GiB"
9+
10+ test "formats durations":
11+ check formatDuration(60) == "1m"
12+ check formatDuration(3660) == "1h 1m"
13+ check formatDuration(90060) == "1d 1h 1m"
14+
15+ test "collects core fields":
16+ let info = collectInfo()
17+ check info.len >= 10
18+ check info[0].key == "Title"
19+ check info[1].key == "OS"
new file mode 100644
@@ -0,0 +1,19 @@
1+import std/[unittest]
2+import sysinfo
3+
4+suite "nimfetch helpers":
5+ test "formats byte quantities":
6+ check formatBytes(0) == "0 B"
7+ check formatBytes(1024) == "1.00 KiB"
8+ check formatBytes(1_073_741_824) == "1.00 GiB"
9+
10+ test "formats durations":
11+ check formatDuration(60) == "1m"
12+ check formatDuration(3660) == "1h 1m"
13+ check formatDuration(90060) == "1d 1h 1m"
14+
15+ test "collects core fields":
16+ let info = collectInfo()
17+ check info.len >= 10
18+ check info[0].key == "Title"
19+ check info[1].key == "OS"