nikofetch 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nikohalfkino
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.
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: nikofetch
3
+ Version: 0.1.0
4
+ Summary: A tiny fastfetch-style system info banner
5
+ Author: niko
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/YOUR_USERNAME/nikofetch
8
+ Project-URL: Issues, https://github.com/YOUR_USERNAME/nikofetch/issues
9
+ Keywords: fetch,fastfetch,neofetch,system-info,cli,terminal
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Terminals
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: psutil>=5.9
21
+ Requires-Dist: rich>=13.0
22
+ Dynamic: license-file
23
+
24
+ # nikofetch
25
+
26
+ A tiny fastfetch-style system info banner, written in Python.
27
+
28
+ ```
29
+ you@host Sat Jan 01, 12:00
30
+
31
+ OS: Linux 6.x
32
+ Kernel: 6.x
33
+ Shell: zsh
34
+ Terminal: kitty
35
+ Uptime: 2d 4h 12m
36
+ CPU: 12% @ 3.60 GHz
37
+ Memory: 8.2 GiB / 32.0 GiB (26%)
38
+ Disk: 120 GiB / 512 GiB (23%)
39
+ ```
40
+
41
+ ## Features
42
+
43
+ - Minimal, configurable stat banner (OS, kernel, shell, terminal, uptime, CPU, memory, disk)
44
+ - Customizable titlebar with `{user}`, `{host}`, `{date}`, `{time}` placeholders
45
+ - Optional ASCII art or (in [kitty](https://sw.kovidgoyal.net/kitty/)) inline image art
46
+ - Interactive terminal config menu — no need to hand-edit JSON
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pipx install nikofetch
52
+ ```
53
+
54
+ or
55
+
56
+ ```bash
57
+ pip install nikofetch
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ```bash
63
+ nikofetch # show the banner
64
+ nikofetch --config # open the interactive config menu
65
+ ```
66
+
67
+ Config is stored at `~/.config/nikofetch/config.json`.
68
+
69
+ ## Development
70
+
71
+ ```bash
72
+ git clone https://github.com/YOUR_USERNAME/nikofetch
73
+ cd nikofetch
74
+ pip install -e .
75
+ ```
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,56 @@
1
+ # nikofetch
2
+
3
+ A tiny fastfetch-style system info banner, written in Python.
4
+
5
+ ```
6
+ you@host Sat Jan 01, 12:00
7
+
8
+ OS: Linux 6.x
9
+ Kernel: 6.x
10
+ Shell: zsh
11
+ Terminal: kitty
12
+ Uptime: 2d 4h 12m
13
+ CPU: 12% @ 3.60 GHz
14
+ Memory: 8.2 GiB / 32.0 GiB (26%)
15
+ Disk: 120 GiB / 512 GiB (23%)
16
+ ```
17
+
18
+ ## Features
19
+
20
+ - Minimal, configurable stat banner (OS, kernel, shell, terminal, uptime, CPU, memory, disk)
21
+ - Customizable titlebar with `{user}`, `{host}`, `{date}`, `{time}` placeholders
22
+ - Optional ASCII art or (in [kitty](https://sw.kovidgoyal.net/kitty/)) inline image art
23
+ - Interactive terminal config menu — no need to hand-edit JSON
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pipx install nikofetch
29
+ ```
30
+
31
+ or
32
+
33
+ ```bash
34
+ pip install nikofetch
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```bash
40
+ nikofetch # show the banner
41
+ nikofetch --config # open the interactive config menu
42
+ ```
43
+
44
+ Config is stored at `~/.config/nikofetch/config.json`.
45
+
46
+ ## Development
47
+
48
+ ```bash
49
+ git clone https://github.com/YOUR_USERNAME/nikofetch
50
+ cd nikofetch
51
+ pip install -e .
52
+ ```
53
+
54
+ ## License
55
+
56
+ MIT
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nikofetch"
7
+ version = "0.1.0"
8
+ description = "A tiny fastfetch-style system info banner"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "niko" } # <-- put your name / GitHub handle here
14
+ ]
15
+ keywords = ["fetch", "fastfetch", "neofetch", "system-info", "cli", "terminal"]
16
+ classifiers = [
17
+ "Environment :: Console",
18
+ "Intended Audience :: End Users/Desktop",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Operating System :: MacOS",
22
+ "Programming Language :: Python :: 3",
23
+ "Topic :: Terminals",
24
+ ]
25
+ dependencies = [
26
+ "psutil>=5.9",
27
+ "rich>=13.0",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/YOUR_USERNAME/nikofetch"
32
+ Issues = "https://github.com/YOUR_USERNAME/nikofetch/issues"
33
+
34
+ [project.scripts]
35
+ nikofetch = "nikofetch:main"
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ __all__ = ["main"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,531 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ nikofetch - a tiny fastfetch-style system info banner
4
+
5
+ Usage:
6
+ nikofetch # display current banner
7
+ nikofetch --config # config menu
8
+ """
9
+
10
+ import argparse
11
+ import contextlib
12
+ import copy
13
+ import curses
14
+ import getpass
15
+ import json
16
+ import os
17
+ import platform
18
+ import re
19
+ import shutil
20
+ import socket
21
+ import subprocess
22
+ import sys
23
+ import termios
24
+ import tty
25
+ from datetime import datetime
26
+ from pathlib import Path
27
+
28
+ import psutil
29
+ from rich.console import Console
30
+ from rich.table import Table
31
+ from rich.text import Text
32
+
33
+ console = Console()
34
+
35
+ CONFIG_DIR = Path.home() / ".config" / "nikofetch"
36
+ CONFIG_PATH = CONFIG_DIR / "config.json"
37
+
38
+ DEFAULT_CONFIG = {
39
+ "titlebar": {
40
+ "left": "{user}@{host}",
41
+ "center": "",
42
+ "right": "{date}, {time}",
43
+ },
44
+ "stats": {
45
+ "os": True,
46
+ "kernel": True,
47
+ "shell": True,
48
+ "terminal": True,
49
+ "uptime": True,
50
+ "cpu": True,
51
+ "memory": True,
52
+ "disk": True,
53
+ },
54
+ "art": {
55
+ "mode": "none", # none | ascii | image
56
+ "ascii_path": "",
57
+ "image_path": "",
58
+ },
59
+ }
60
+
61
+
62
+
63
+ # Config load/save
64
+
65
+
66
+ def load_config() -> dict:
67
+ if not CONFIG_PATH.exists():
68
+ return copy.deepcopy(DEFAULT_CONFIG)
69
+ try:
70
+ with open(CONFIG_PATH) as f:
71
+ loaded = json.load(f)
72
+ except (json.JSONDecodeError, OSError):
73
+ return copy.deepcopy(DEFAULT_CONFIG)
74
+
75
+ # merge with defaults so new keys added in future versions don't break old configs
76
+ merged = copy.deepcopy(DEFAULT_CONFIG)
77
+ for section, values in loaded.items():
78
+ if section in merged and isinstance(values, dict):
79
+ merged[section].update(values)
80
+ else:
81
+ merged[section] = values
82
+ return merged
83
+
84
+
85
+ def save_config(config: dict):
86
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
87
+ with open(CONFIG_PATH, "w") as f:
88
+ json.dump(config, f, indent=2)
89
+
90
+
91
+
92
+ # Stat getters
93
+
94
+
95
+ def get_uptime() -> str:
96
+ boot = datetime.fromtimestamp(psutil.boot_time())
97
+ delta = datetime.now() - boot
98
+ days, rem = divmod(int(delta.total_seconds()), 86400)
99
+ hours, rem = divmod(rem, 3600)
100
+ minutes, _ = divmod(rem, 60)
101
+ parts = []
102
+ if days:
103
+ parts.append(f"{days}d")
104
+ if hours:
105
+ parts.append(f"{hours}h")
106
+ parts.append(f"{minutes}m")
107
+ return " ".join(parts)
108
+
109
+
110
+ def get_shell() -> str:
111
+ shell_path = os.environ.get("SHELL", "unknown")
112
+ return shell_path.split("/")[-1]
113
+
114
+
115
+ def get_terminal() -> str:
116
+ return os.environ.get("TERM_PROGRAM") or os.environ.get("TERM", "unknown")
117
+
118
+
119
+ def get_memory() -> str:
120
+ vm = psutil.virtual_memory()
121
+ used_gb = vm.used / (1024 ** 3)
122
+ total_gb = vm.total / (1024 ** 3)
123
+ return f"{used_gb:.1f} GiB / {total_gb:.1f} GiB ({vm.percent}%)"
124
+
125
+
126
+ def get_cpu() -> str:
127
+ freq = psutil.cpu_freq()
128
+ # interval=None measures since the last call, so this reads 0% on a
129
+ # fresh process (e.g. a one-shot `nikofetch` run). Pass a small
130
+ # interval (e.g. 0.1) if accuracy matters more than startup latency.
131
+ load = psutil.cpu_percent(interval=None)
132
+ if freq:
133
+ return f"{load:.0f}% @ {freq.current / 1000:.2f} GHz"
134
+ return f"{load:.0f}%"
135
+
136
+
137
+ def get_disk() -> str:
138
+ du = psutil.disk_usage("/")
139
+ used_gb = du.used / (1024 ** 3)
140
+ total_gb = du.total / (1024 ** 3)
141
+ return f"{used_gb:.0f} GiB / {total_gb:.0f} GiB ({du.percent}%)"
142
+
143
+
144
+ STAT_DEFINITIONS = [
145
+ ("os", "OS", lambda: f"{platform.system()} {platform.release()}"),
146
+ ("kernel", "Kernel", lambda: platform.version().split()[0] if platform.version() else "n/a"),
147
+ ("shell", "Shell", get_shell),
148
+ ("terminal", "Terminal", get_terminal),
149
+ ("uptime", "Uptime", get_uptime),
150
+ ("cpu", "CPU", get_cpu),
151
+ ("memory", "Memory", get_memory),
152
+ ("disk", "Disk", get_disk),
153
+ ]
154
+
155
+
156
+
157
+ # Rendering
158
+
159
+
160
+ def format_placeholders(text: str) -> str:
161
+ now = datetime.now()
162
+ return text.format(
163
+ user=getpass.getuser(),
164
+ host=socket.gethostname(),
165
+ date=now.strftime("%a %b %d"),
166
+ time=now.strftime("%H:%M"),
167
+ )
168
+
169
+
170
+ def build_titlebar(config: dict, width: int) -> Text:
171
+ left = format_placeholders(config["titlebar"].get("left", "")).strip()
172
+ center = format_placeholders(config["titlebar"].get("center", "")).strip()
173
+ right = format_placeholders(config["titlebar"].get("right", "")).strip()
174
+
175
+ left = f" {left} " if left else ""
176
+ right = f" {right} " if right else ""
177
+ center = f" {center} " if center else ""
178
+
179
+ remaining = width - len(left) - len(right)
180
+
181
+ if center and remaining > len(center):
182
+ gap = remaining - len(center)
183
+ left_pad = gap // 2
184
+ right_pad = gap - left_pad
185
+ bar_text = f"{left}{' ' * left_pad}{center}{' ' * right_pad}{right}"
186
+ else:
187
+ pad = max(remaining, 0)
188
+ bar_text = f"{left}{' ' * pad}{right}"
189
+
190
+ bar_text = bar_text[:width] if width else bar_text
191
+ return Text(bar_text, style="black on white")
192
+
193
+
194
+ def build_stats_table(config: dict) -> Table:
195
+ table = Table.grid(padding=(0, 2))
196
+ table.add_column(justify="right", style="bold cyan")
197
+ table.add_column()
198
+
199
+ for key, label, getter in STAT_DEFINITIONS:
200
+ if config["stats"].get(key, True):
201
+ table.add_row(f"{label}:", getter())
202
+ return table
203
+
204
+
205
+ def load_ascii_art(config: dict) -> str | None:
206
+ art = config.get("art", {})
207
+ if art.get("mode") != "ascii" or not art.get("ascii_path"):
208
+ return None
209
+ resolved = Path(art["ascii_path"]).expanduser()
210
+ if not resolved.exists():
211
+ console.print(f"[red]ASCII art file not found: {art['ascii_path']}[/red]")
212
+ return None
213
+ try:
214
+ return resolved.read_text()
215
+ except OSError as e:
216
+ console.print(f"[red]Couldn't read ASCII art file: {e}[/red]")
217
+ return None
218
+
219
+
220
+ IMAGE_WIDTH_CELLS = 22 # how much horizontal space to reserve for the image
221
+
222
+
223
+ @contextlib.contextmanager
224
+ def raw_terminal():
225
+ # puts stdin into raw mode for the block, restoring old settings after
226
+ fd = sys.stdin.fileno()
227
+ old_settings = termios.tcgetattr(fd)
228
+ try:
229
+ tty.setraw(fd)
230
+ yield fd
231
+ finally:
232
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
233
+
234
+
235
+ def _query_cursor_row() -> int:
236
+ with raw_terminal():
237
+ sys.stdout.write("\x1b[6n")
238
+ sys.stdout.flush()
239
+ buf = ""
240
+ while "R" not in buf:
241
+ buf += sys.stdin.read(1)
242
+ match = re.search(r"\x1b\[(\d+);(\d+)R", buf)
243
+ return int(match.group(1)) if match else 1
244
+
245
+
246
+ def render_image_beside_stats(config: dict, stats_table: Table) -> bool:
247
+ """Place the configured image at the cursor's current row using kitty's
248
+ --place (absolute screen coordinates), then manually cursor-position each
249
+ stats line to sit beside it. Returns True if it handled the image (even
250
+ if it fell back to printing an error), False if there's nothing to do."""
251
+ art = config.get("art", {})
252
+ if art.get("mode") != "image" or not art.get("image_path"):
253
+ return False
254
+
255
+ image_path = art["image_path"]
256
+ if "KITTY_WINDOW_ID" not in os.environ:
257
+ console.print(
258
+ "[yellow]Image art configured, but this isn't a kitty terminal — skipping.[/yellow]"
259
+ )
260
+ return False
261
+ resolved = Path(image_path).expanduser()
262
+ if not resolved.exists():
263
+ console.print(f"[red]Image not found: {image_path}[/red]")
264
+ return False
265
+ if not shutil.which("kitten"):
266
+ console.print("[yellow]'kitten' command not found — can't display image.[/yellow]")
267
+ return False
268
+
269
+ with console.capture() as capture:
270
+ console.print(stats_table)
271
+ lines = capture.get().rstrip("\n").split("\n")
272
+
273
+ image_height = max(len(lines), 6)
274
+ row = _query_cursor_row()
275
+ top = row - 1 # --place uses 0-indexed screen coordinates
276
+
277
+ subprocess.run(
278
+ [
279
+ "kitten", "icat",
280
+ f"--place={IMAGE_WIDTH_CELLS}x{image_height}@0x{top}",
281
+ "--align=left",
282
+ "--scale-up",
283
+ str(resolved),
284
+ ]
285
+ )
286
+
287
+ gap = 2
288
+ text_col = IMAGE_WIDTH_CELLS + gap
289
+ for i, line in enumerate(lines):
290
+ sys.stdout.write(f"\x1b[{top + i + 1};{text_col + 1}H{line}")
291
+ sys.stdout.flush()
292
+
293
+ final_row = top + max(image_height, len(lines)) + 1
294
+ sys.stdout.write(f"\x1b[{final_row};1H\n")
295
+ sys.stdout.flush()
296
+ return True
297
+
298
+
299
+ def render_once(config: dict | None = None):
300
+ if config is None:
301
+ config = load_config()
302
+ width = console.size.width
303
+
304
+ console.print(build_titlebar(config, width))
305
+ console.print()
306
+
307
+ ascii_text = load_ascii_art(config)
308
+ stats_table = build_stats_table(config)
309
+
310
+ if render_image_beside_stats(config, stats_table):
311
+ pass # image handled its own placement, including the stats table
312
+ elif ascii_text:
313
+ combined = Table.grid(padding=(0, 4))
314
+ combined.add_column()
315
+ combined.add_column()
316
+ combined.add_row(Text(ascii_text, style="bold"), stats_table)
317
+ console.print(combined)
318
+ else:
319
+ console.print(stats_table)
320
+
321
+ console.print()
322
+
323
+
324
+
325
+ # config menu
326
+
327
+ HELP_SELECT = "Arrows: move Z: select X: back"
328
+ HELP_CHECKBOX = "Arrows: move Z: toggle X: back/done"
329
+ HELP_TEXT = "Type to edit Enter: confirm Backspace: delete"
330
+
331
+
332
+ def _addstr_safe(stdscr, y, x, text, attr=curses.A_NORMAL):
333
+ height, width = stdscr.getmaxyx()
334
+ if 0 <= y < height and x < width:
335
+ try:
336
+ stdscr.addstr(y, x, text[: max(width - x - 1, 0)], attr)
337
+ except curses.error:
338
+ pass
339
+
340
+
341
+ def _curses_menu_loop(stdscr, title: str, n: int, render_row, on_select, help_text: str):
342
+ """Shared arrow-key menu loop used by curses_select and curses_checkbox.
343
+ render_row(i, selected) -> str for row i.
344
+ on_select(i) runs on Z; return True to exit the loop, False to keep it open.
345
+ Returns the selected index, or None if X was pressed."""
346
+ idx = 0
347
+ while True:
348
+ stdscr.clear()
349
+ _addstr_safe(stdscr, 0, 0, title, curses.A_BOLD)
350
+ for i in range(n):
351
+ attr = curses.A_REVERSE if i == idx else curses.A_NORMAL
352
+ _addstr_safe(stdscr, 2 + i, 0, render_row(i, i == idx), attr)
353
+ _addstr_safe(stdscr, 3 + n, 0, help_text, curses.A_DIM)
354
+ stdscr.refresh()
355
+
356
+ key = stdscr.getch()
357
+ if key == curses.KEY_UP:
358
+ idx = (idx - 1) % n
359
+ elif key == curses.KEY_DOWN:
360
+ idx = (idx + 1) % n
361
+ elif key in (ord("z"), ord("Z")):
362
+ if on_select(idx):
363
+ return idx
364
+ elif key in (ord("x"), ord("X")):
365
+ return None
366
+
367
+
368
+ def curses_select(stdscr, title: str, choices: list) -> str | None:
369
+ """choices: list of (label, value)"""
370
+
371
+ def render_row(i, selected):
372
+ prefix = "> " if selected else " "
373
+ return f"{prefix}{choices[i][0]}"
374
+
375
+ idx = _curses_menu_loop(stdscr, title, len(choices), render_row, lambda i: True, HELP_SELECT)
376
+ return choices[idx][1] if idx is not None else None
377
+
378
+
379
+ def curses_checkbox(stdscr, title: str, config: dict):
380
+ def render_row(i, selected):
381
+ key, label, _getter = STAT_DEFINITIONS[i]
382
+ checked = config["stats"].get(key, True)
383
+ box = "[X]" if checked else "[ ]"
384
+ prefix = "> " if selected else " "
385
+ return f"{prefix}{box} {label}"
386
+
387
+ def toggle(i):
388
+ stat_key = STAT_DEFINITIONS[i][0]
389
+ config["stats"][stat_key] = not config["stats"].get(stat_key, True)
390
+ return False # X exits this menu, not Z
391
+
392
+ _curses_menu_loop(stdscr, title, len(STAT_DEFINITIONS), render_row, toggle, HELP_CHECKBOX)
393
+
394
+
395
+ def curses_text_input(stdscr, title: str, default: str = "", footer: str | None = None) -> str:
396
+ curses.curs_set(1)
397
+ buf = list(default)
398
+ pos = len(buf)
399
+ try:
400
+ while True:
401
+ stdscr.clear()
402
+ _addstr_safe(stdscr, 0, 0, title, curses.A_BOLD)
403
+ _addstr_safe(stdscr, 2, 0, "".join(buf))
404
+ if footer:
405
+ _addstr_safe(stdscr, 4, 0, footer, curses.A_DIM)
406
+ _addstr_safe(stdscr, 5, 0, HELP_TEXT, curses.A_DIM)
407
+ else:
408
+ _addstr_safe(stdscr, 4, 0, HELP_TEXT, curses.A_DIM)
409
+ stdscr.move(2, min(pos, stdscr.getmaxyx()[1] - 1))
410
+ stdscr.refresh()
411
+
412
+ key = stdscr.getch()
413
+ if key in (curses.KEY_ENTER, 10, 13):
414
+ return "".join(buf)
415
+ elif key in (curses.KEY_BACKSPACE, 127, 8):
416
+ if pos > 0:
417
+ buf.pop(pos - 1)
418
+ pos -= 1
419
+ elif key == curses.KEY_LEFT:
420
+ pos = max(0, pos - 1)
421
+ elif key == curses.KEY_RIGHT:
422
+ pos = min(len(buf), pos + 1)
423
+ elif 32 <= key <= 126:
424
+ buf.insert(pos, chr(key))
425
+ pos += 1
426
+ finally:
427
+ curses.curs_set(0)
428
+
429
+
430
+ def curses_preview(stdscr, config: dict):
431
+ curses.def_prog_mode()
432
+ curses.endwin()
433
+ console.clear()
434
+ render_once(config)
435
+ console.print("[dim]Press Enter or Space to return...[/dim]")
436
+
437
+ with raw_terminal():
438
+ while True:
439
+ ch = sys.stdin.read(1)
440
+ if ch in ("\r", "\n", " "):
441
+ break
442
+
443
+ console.clear()
444
+ curses.reset_prog_mode()
445
+ stdscr.clear()
446
+ stdscr.refresh()
447
+
448
+
449
+ def _config_menu_titlebar(stdscr, config: dict):
450
+ placeholders_note = "Placeholders: {user} {host} {date} {time}"
451
+ config["titlebar"]["left"] = curses_text_input(
452
+ stdscr, "Left text:", config["titlebar"].get("left", ""), footer=placeholders_note
453
+ )
454
+ config["titlebar"]["center"] = curses_text_input(
455
+ stdscr, "Center text (blank for none):", config["titlebar"].get("center", ""), footer=placeholders_note
456
+ )
457
+ config["titlebar"]["right"] = curses_text_input(
458
+ stdscr, "Right text:", config["titlebar"].get("right", ""), footer=placeholders_note
459
+ )
460
+
461
+
462
+ def _config_menu_art(stdscr, config: dict):
463
+ mode = curses_select(
464
+ stdscr,
465
+ "Banner art:",
466
+ [("None", "none"), ("ASCII art (from a .txt file)", "ascii"), ("Image (kitty terminal only)", "image")],
467
+ )
468
+ if mode is None:
469
+ return
470
+ config["art"]["mode"] = mode
471
+
472
+ if mode == "ascii":
473
+ path = curses_text_input(
474
+ stdscr, "Path to .txt file with ASCII art:", config["art"].get("ascii_path", "")
475
+ )
476
+ config["art"]["ascii_path"] = path
477
+ elif mode == "image":
478
+ config["art"]["image_path"] = curses_text_input(
479
+ stdscr, "Path to image file:", config["art"].get("image_path", "")
480
+ )
481
+
482
+
483
+ def _run_config_menu(stdscr, config: dict):
484
+ curses.curs_set(0)
485
+ while True:
486
+ action = curses_select(
487
+ stdscr,
488
+ "nikofetch config",
489
+ [
490
+ ("Toggle stats", "stats"),
491
+ ("Edit titlebar text", "titlebar"),
492
+ ("Set banner art (ascii / image / none)", "art"),
493
+ ("Preview", "preview"),
494
+ ("Save and exit", "save"),
495
+ ("Exit without saving", "exit"),
496
+ ],
497
+ )
498
+ if action is None or action == "exit":
499
+ return
500
+ elif action == "stats":
501
+ curses_checkbox(stdscr, "Toggle stats", config)
502
+ elif action == "titlebar":
503
+ _config_menu_titlebar(stdscr, config)
504
+ elif action == "art":
505
+ _config_menu_art(stdscr, config)
506
+ elif action == "preview":
507
+ curses_preview(stdscr, config)
508
+ elif action == "save":
509
+ save_config(config)
510
+ return
511
+
512
+
513
+ def run_config_menu():
514
+ config = load_config()
515
+ curses.wrapper(_run_config_menu, config)
516
+
517
+ # Entry point
518
+
519
+ def main():
520
+ parser = argparse.ArgumentParser(description="nikofetch - minimal system info banner")
521
+ parser.add_argument("--config", action="store_true", help="open the interactive config menu")
522
+ args = parser.parse_args()
523
+
524
+ if args.config:
525
+ run_config_menu()
526
+ else:
527
+ render_once()
528
+
529
+
530
+ if __name__ == "__main__":
531
+ main()
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: nikofetch
3
+ Version: 0.1.0
4
+ Summary: A tiny fastfetch-style system info banner
5
+ Author: niko
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/YOUR_USERNAME/nikofetch
8
+ Project-URL: Issues, https://github.com/YOUR_USERNAME/nikofetch/issues
9
+ Keywords: fetch,fastfetch,neofetch,system-info,cli,terminal
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Terminals
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: psutil>=5.9
21
+ Requires-Dist: rich>=13.0
22
+ Dynamic: license-file
23
+
24
+ # nikofetch
25
+
26
+ A tiny fastfetch-style system info banner, written in Python.
27
+
28
+ ```
29
+ you@host Sat Jan 01, 12:00
30
+
31
+ OS: Linux 6.x
32
+ Kernel: 6.x
33
+ Shell: zsh
34
+ Terminal: kitty
35
+ Uptime: 2d 4h 12m
36
+ CPU: 12% @ 3.60 GHz
37
+ Memory: 8.2 GiB / 32.0 GiB (26%)
38
+ Disk: 120 GiB / 512 GiB (23%)
39
+ ```
40
+
41
+ ## Features
42
+
43
+ - Minimal, configurable stat banner (OS, kernel, shell, terminal, uptime, CPU, memory, disk)
44
+ - Customizable titlebar with `{user}`, `{host}`, `{date}`, `{time}` placeholders
45
+ - Optional ASCII art or (in [kitty](https://sw.kovidgoyal.net/kitty/)) inline image art
46
+ - Interactive terminal config menu — no need to hand-edit JSON
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pipx install nikofetch
52
+ ```
53
+
54
+ or
55
+
56
+ ```bash
57
+ pip install nikofetch
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ```bash
63
+ nikofetch # show the banner
64
+ nikofetch --config # open the interactive config menu
65
+ ```
66
+
67
+ Config is stored at `~/.config/nikofetch/config.json`.
68
+
69
+ ## Development
70
+
71
+ ```bash
72
+ git clone https://github.com/YOUR_USERNAME/nikofetch
73
+ cd nikofetch
74
+ pip install -e .
75
+ ```
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/nikofetch/__init__.py
5
+ src/nikofetch/cli.py
6
+ src/nikofetch.egg-info/PKG-INFO
7
+ src/nikofetch.egg-info/SOURCES.txt
8
+ src/nikofetch.egg-info/dependency_links.txt
9
+ src/nikofetch.egg-info/entry_points.txt
10
+ src/nikofetch.egg-info/requires.txt
11
+ src/nikofetch.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nikofetch = nikofetch:main
@@ -0,0 +1,2 @@
1
+ psutil>=5.9
2
+ rich>=13.0
@@ -0,0 +1 @@
1
+ nikofetch