larpfetch 0.1.0__py3-none-any.whl
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.
- larpfetch/__init__.py +3 -0
- larpfetch/__main__.py +6 -0
- larpfetch/cli.py +172 -0
- larpfetch/collectors/__init__.py +1 -0
- larpfetch/collectors/common.py +327 -0
- larpfetch/collectors/linux.py +5 -0
- larpfetch/collectors/macos.py +5 -0
- larpfetch/collectors/windows.py +5 -0
- larpfetch/config.py +74 -0
- larpfetch/easter_eggs.py +92 -0
- larpfetch/logos.py +141 -0
- larpfetch/models.py +99 -0
- larpfetch/renderer.py +144 -0
- larpfetch/resolver.py +45 -0
- larpfetch-0.1.0.dist-info/METADATA +202 -0
- larpfetch-0.1.0.dist-info/RECORD +19 -0
- larpfetch-0.1.0.dist-info/WHEEL +4 -0
- larpfetch-0.1.0.dist-info/entry_points.txt +2 -0
- larpfetch-0.1.0.dist-info/licenses/LICENSE +21 -0
larpfetch/__init__.py
ADDED
larpfetch/__main__.py
ADDED
larpfetch/cli.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""CLI entry point for larpfetch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Sequence
|
|
8
|
+
|
|
9
|
+
from larpfetch import __version__
|
|
10
|
+
from larpfetch.collectors.common import collect_all
|
|
11
|
+
from larpfetch.config import (
|
|
12
|
+
get_appearance,
|
|
13
|
+
get_default_profile,
|
|
14
|
+
get_named_profiles,
|
|
15
|
+
load_config,
|
|
16
|
+
)
|
|
17
|
+
from larpfetch.renderer import render
|
|
18
|
+
from larpfetch.resolver import resolve
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_sets(sets: list[str]) -> dict[str, str]:
|
|
22
|
+
"""Parse repeated --set key=value arguments."""
|
|
23
|
+
result: dict[str, str] = {}
|
|
24
|
+
for s in sets:
|
|
25
|
+
if "=" not in s:
|
|
26
|
+
print(f"Error: --set argument must be key=value, got: {s}", file=sys.stderr)
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
key, _, value = s.partition("=")
|
|
29
|
+
key = key.strip()
|
|
30
|
+
if not key:
|
|
31
|
+
print(f"Error: --set key cannot be empty in: {s}", file=sys.stderr)
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
result[key] = value
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _list_profiles(config: dict) -> None:
|
|
38
|
+
"""Print available profiles."""
|
|
39
|
+
profiles = get_named_profiles(config)
|
|
40
|
+
if not profiles:
|
|
41
|
+
print("No profiles configured.")
|
|
42
|
+
return
|
|
43
|
+
print("Available profiles:")
|
|
44
|
+
for name in sorted(profiles.keys()):
|
|
45
|
+
fields = profiles[name]
|
|
46
|
+
desc = fields.get("os", fields.get("distro", "Custom identity"))
|
|
47
|
+
print(f" {name:20s} {desc}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _show_config(config: dict) -> None:
|
|
51
|
+
"""Print the resolved configuration."""
|
|
52
|
+
default = get_default_profile(config)
|
|
53
|
+
profiles = get_named_profiles(config)
|
|
54
|
+
appearance = get_appearance(config)
|
|
55
|
+
|
|
56
|
+
print("Default profile:")
|
|
57
|
+
if default:
|
|
58
|
+
for k, v in sorted(default.items()):
|
|
59
|
+
print(f" {k} = {v}")
|
|
60
|
+
else:
|
|
61
|
+
print(" (empty)")
|
|
62
|
+
|
|
63
|
+
print("\nProfiles:")
|
|
64
|
+
if profiles:
|
|
65
|
+
for name in sorted(profiles.keys()):
|
|
66
|
+
print(f"\n [{name}]")
|
|
67
|
+
for k, v in sorted(profiles[name].items()):
|
|
68
|
+
print(f" {k} = {v}")
|
|
69
|
+
else:
|
|
70
|
+
print(" (none)")
|
|
71
|
+
|
|
72
|
+
print("\nAppearance:")
|
|
73
|
+
if appearance:
|
|
74
|
+
for k, v in sorted(appearance.items()):
|
|
75
|
+
print(f" {k} = {v}")
|
|
76
|
+
else:
|
|
77
|
+
print(" (defaults)")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
81
|
+
parser = argparse.ArgumentParser(
|
|
82
|
+
prog="larpfetch",
|
|
83
|
+
description=(
|
|
84
|
+
"LARP as any distro, hardware, or machine you want."
|
|
85
|
+
" Because reality is optional until you pass --real-shit."
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
parser.add_argument("-p", "--profile", help="Select a named profile")
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--real-shit",
|
|
91
|
+
action="store_true",
|
|
92
|
+
help="Show real detected system information only",
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--list-profiles", action="store_true", help="List available profiles and exit"
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--show-config", action="store_true", help="Show current configuration and exit"
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument("--config", help="Path to a custom config file")
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"--set",
|
|
103
|
+
action="append",
|
|
104
|
+
default=[],
|
|
105
|
+
metavar="KEY=VALUE",
|
|
106
|
+
help="Override a field (repeatable)",
|
|
107
|
+
)
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"--version", action="version", version=f"larpfetch {__version__}"
|
|
110
|
+
)
|
|
111
|
+
return parser
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
115
|
+
parser = build_parser()
|
|
116
|
+
args = parser.parse_args(argv)
|
|
117
|
+
|
|
118
|
+
# Load config
|
|
119
|
+
try:
|
|
120
|
+
config = load_config(args.config)
|
|
121
|
+
except FileNotFoundError as e:
|
|
122
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"Error loading config: {e}", file=sys.stderr)
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
|
|
128
|
+
# --list-profiles
|
|
129
|
+
if args.list_profiles:
|
|
130
|
+
_list_profiles(config)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# --show-config
|
|
134
|
+
if args.show_config:
|
|
135
|
+
_show_config(config)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Collect real system info
|
|
139
|
+
real = collect_all()
|
|
140
|
+
|
|
141
|
+
# Resolve display identity
|
|
142
|
+
default_profile = get_default_profile(config)
|
|
143
|
+
selected_profile = None
|
|
144
|
+
if args.profile:
|
|
145
|
+
profiles = get_named_profiles(config)
|
|
146
|
+
if args.profile not in profiles:
|
|
147
|
+
avail = ", ".join(sorted(profiles.keys()))
|
|
148
|
+
print(
|
|
149
|
+
f"Error: profile '{args.profile}' not found. Available: {avail}",
|
|
150
|
+
file=sys.stderr,
|
|
151
|
+
)
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
selected_profile = profiles[args.profile]
|
|
154
|
+
|
|
155
|
+
cli_overrides = _parse_sets(args.set)
|
|
156
|
+
appearance = get_appearance(config)
|
|
157
|
+
|
|
158
|
+
resolved = resolve(
|
|
159
|
+
real=real,
|
|
160
|
+
default_profile=default_profile,
|
|
161
|
+
selected_profile=selected_profile,
|
|
162
|
+
cli_overrides=cli_overrides,
|
|
163
|
+
real_shit=args.real_shit,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Render
|
|
167
|
+
output = render(resolved, real, args.real_shit, appearance)
|
|
168
|
+
print(output)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Collectors package."""
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Cross-platform collectors using standard library and psutil."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import getpass
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import socket
|
|
9
|
+
import time
|
|
10
|
+
from collections import OrderedDict
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import psutil
|
|
14
|
+
except ImportError:
|
|
15
|
+
psutil = None # type: ignore[assignment]
|
|
16
|
+
|
|
17
|
+
from larpfetch.models import SystemInfo
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _fmt_bytes(n: int) -> str:
|
|
21
|
+
"""Format bytes to human-readable string."""
|
|
22
|
+
for unit in ("B", "KiB", "MiB", "GiB", "TiB", "PiB"):
|
|
23
|
+
if abs(n) < 1024:
|
|
24
|
+
return f"{n:.1f} {unit}"
|
|
25
|
+
n = int(n / 1024)
|
|
26
|
+
return f"{n} EiB"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _fmt_uptime(seconds: float) -> str:
|
|
30
|
+
"""Format seconds to a human-readable uptime string."""
|
|
31
|
+
days = int(seconds // 86400)
|
|
32
|
+
hours = int((seconds % 86400) // 3600)
|
|
33
|
+
minutes = int((seconds % 3600) // 60)
|
|
34
|
+
parts: list[str] = []
|
|
35
|
+
if days:
|
|
36
|
+
parts.append(f"{days}d")
|
|
37
|
+
if hours:
|
|
38
|
+
parts.append(f"{hours}h")
|
|
39
|
+
parts.append(f"{minutes}m")
|
|
40
|
+
return " ".join(parts)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def collect_common() -> SystemInfo:
|
|
44
|
+
"""Collect information common across all platforms."""
|
|
45
|
+
info = SystemInfo(fields=OrderedDict())
|
|
46
|
+
|
|
47
|
+
# Username
|
|
48
|
+
try:
|
|
49
|
+
info.set("username", getpass.getuser())
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
# Hostname
|
|
54
|
+
try:
|
|
55
|
+
info.set("hostname", socket.gethostname())
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
# Architecture
|
|
60
|
+
try:
|
|
61
|
+
info.set("architecture", platform.machine())
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
# Uptime
|
|
66
|
+
try:
|
|
67
|
+
if psutil is not None:
|
|
68
|
+
boot = psutil.boot_time()
|
|
69
|
+
info.set("uptime", _fmt_uptime(time.time() - boot))
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
# Memory
|
|
74
|
+
try:
|
|
75
|
+
if psutil is not None:
|
|
76
|
+
vm = psutil.virtual_memory()
|
|
77
|
+
info.set("memory", f"{_fmt_bytes(vm.used)} / {_fmt_bytes(vm.total)}")
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
# Disk
|
|
82
|
+
try:
|
|
83
|
+
if psutil is not None:
|
|
84
|
+
du = psutil.disk_usage("/")
|
|
85
|
+
info.set("disk", f"{_fmt_bytes(du.used)} / {_fmt_bytes(du.total)}")
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
# Battery
|
|
90
|
+
try:
|
|
91
|
+
if psutil is not None:
|
|
92
|
+
bat = psutil.sensors_battery()
|
|
93
|
+
if bat is not None:
|
|
94
|
+
info.set("battery", f"{bat.percent}%")
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
# CPU model
|
|
99
|
+
try:
|
|
100
|
+
info.set("cpu", _detect_cpu())
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
return info
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _detect_cpu() -> str:
|
|
108
|
+
"""Best-effort CPU name detection."""
|
|
109
|
+
# Linux: /proc/cpuinfo
|
|
110
|
+
try:
|
|
111
|
+
with open("/proc/cpuinfo") as f:
|
|
112
|
+
for line in f:
|
|
113
|
+
if line.startswith("model name"):
|
|
114
|
+
return line.split(":", 1)[1].strip()
|
|
115
|
+
except OSError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# macOS: sysctl
|
|
119
|
+
try:
|
|
120
|
+
import subprocess
|
|
121
|
+
|
|
122
|
+
result = subprocess.run(
|
|
123
|
+
["sysctl", "-n", "machdep.cpu.brand_string"],
|
|
124
|
+
capture_output=True,
|
|
125
|
+
text=True,
|
|
126
|
+
timeout=3,
|
|
127
|
+
)
|
|
128
|
+
if result.returncode == 0:
|
|
129
|
+
return result.stdout.strip()
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
# Windows: platform.processor()
|
|
134
|
+
proc = platform.processor()
|
|
135
|
+
if proc:
|
|
136
|
+
return proc
|
|
137
|
+
|
|
138
|
+
return "Unknown CPU"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def collect_platform() -> SystemInfo:
|
|
142
|
+
"""Collect platform-specific information."""
|
|
143
|
+
system = platform.system()
|
|
144
|
+
if system == "Linux":
|
|
145
|
+
return _collect_linux()
|
|
146
|
+
elif system == "Darwin":
|
|
147
|
+
return _collect_macos()
|
|
148
|
+
elif system == "Windows":
|
|
149
|
+
return _collect_windows()
|
|
150
|
+
return SystemInfo(fields=OrderedDict())
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _collect_linux() -> SystemInfo:
|
|
154
|
+
info = SystemInfo(fields=OrderedDict())
|
|
155
|
+
|
|
156
|
+
# OS / distro from /etc/os-release
|
|
157
|
+
os_release: dict[str, str] = {}
|
|
158
|
+
try:
|
|
159
|
+
with open("/etc/os-release") as f:
|
|
160
|
+
for line in f:
|
|
161
|
+
line = line.strip()
|
|
162
|
+
if "=" in line:
|
|
163
|
+
key, _, value = line.partition("=")
|
|
164
|
+
os_release[key] = value.strip('"')
|
|
165
|
+
except OSError:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
if os_release.get("PRETTY_NAME"):
|
|
169
|
+
info.set("distro", os_release["PRETTY_NAME"])
|
|
170
|
+
info.set("os", os_release["PRETTY_NAME"])
|
|
171
|
+
elif os_release.get("NAME"):
|
|
172
|
+
info.set("distro", os_release["NAME"])
|
|
173
|
+
info.set("os", os_release["NAME"])
|
|
174
|
+
|
|
175
|
+
if os_release.get("VERSION"):
|
|
176
|
+
info.set("os_version", os_release["VERSION"])
|
|
177
|
+
|
|
178
|
+
# Kernel
|
|
179
|
+
try:
|
|
180
|
+
info.set("kernel", platform.release())
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
# Shell
|
|
185
|
+
shell = os.environ.get("SHELL", "")
|
|
186
|
+
if shell:
|
|
187
|
+
info.set("shell", os.path.basename(shell))
|
|
188
|
+
|
|
189
|
+
# Desktop environment
|
|
190
|
+
de = os.environ.get("XDG_CURRENT_DESKTOP", "") or os.environ.get(
|
|
191
|
+
"DESKTOP_SESSION", ""
|
|
192
|
+
)
|
|
193
|
+
if de:
|
|
194
|
+
info.set("de", de)
|
|
195
|
+
|
|
196
|
+
# GPU via lspci (best-effort)
|
|
197
|
+
try:
|
|
198
|
+
import subprocess
|
|
199
|
+
|
|
200
|
+
result = subprocess.run(
|
|
201
|
+
["lspci"],
|
|
202
|
+
capture_output=True,
|
|
203
|
+
text=True,
|
|
204
|
+
timeout=3,
|
|
205
|
+
)
|
|
206
|
+
if result.returncode == 0:
|
|
207
|
+
for line in result.stdout.splitlines():
|
|
208
|
+
lower = line.lower()
|
|
209
|
+
if "vga" in lower or "3d" in lower or "display" in lower:
|
|
210
|
+
# Format: "XX:XX.X Class: vendor device"
|
|
211
|
+
parts = line.split(": ", 2)
|
|
212
|
+
if len(parts) >= 3:
|
|
213
|
+
gpu_desc = parts[2]
|
|
214
|
+
# Trim the trailing (rev xx) if present
|
|
215
|
+
if " (" in gpu_desc:
|
|
216
|
+
gpu_desc = gpu_desc[: gpu_desc.rfind(" (")]
|
|
217
|
+
info.set("gpu", gpu_desc)
|
|
218
|
+
break
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
return info
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _collect_macos() -> SystemInfo:
|
|
226
|
+
info = SystemInfo(fields=OrderedDict())
|
|
227
|
+
|
|
228
|
+
# macOS version
|
|
229
|
+
try:
|
|
230
|
+
mac_ver = platform.mac_ver()
|
|
231
|
+
if mac_ver and mac_ver[0]:
|
|
232
|
+
info.set("os", f"macOS {mac_ver[0]}")
|
|
233
|
+
info.set("distro", f"macOS {mac_ver[0]}")
|
|
234
|
+
info.set("os_version", mac_ver[0])
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
# Kernel
|
|
239
|
+
try:
|
|
240
|
+
info.set("kernel", f"Darwin {platform.release()}")
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
# Shell
|
|
245
|
+
shell = os.environ.get("SHELL", "")
|
|
246
|
+
if shell:
|
|
247
|
+
info.set("shell", os.path.basename(shell))
|
|
248
|
+
|
|
249
|
+
# Desktop environment (macOS = Aqua)
|
|
250
|
+
info.set("de", "Aqua")
|
|
251
|
+
|
|
252
|
+
# GPU via system_profiler (best-effort)
|
|
253
|
+
try:
|
|
254
|
+
import subprocess
|
|
255
|
+
|
|
256
|
+
result = subprocess.run(
|
|
257
|
+
["system_profiler", "SPDisplaysDataType"],
|
|
258
|
+
capture_output=True,
|
|
259
|
+
text=True,
|
|
260
|
+
timeout=5,
|
|
261
|
+
)
|
|
262
|
+
if result.returncode == 0:
|
|
263
|
+
for line in result.stdout.splitlines():
|
|
264
|
+
if "Chipset Model:" in line or "Chip:" in line:
|
|
265
|
+
gpu = line.split(":", 1)[1].strip()
|
|
266
|
+
info.set("gpu", gpu)
|
|
267
|
+
break
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
return info
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _collect_windows() -> SystemInfo:
|
|
275
|
+
info = SystemInfo(fields=OrderedDict())
|
|
276
|
+
|
|
277
|
+
# Windows version
|
|
278
|
+
try:
|
|
279
|
+
ver = platform.version()
|
|
280
|
+
release = platform.release()
|
|
281
|
+
info.set("os", f"Windows {release} {ver}")
|
|
282
|
+
info.set("os_version", f"{release} {ver}")
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
# Kernel
|
|
287
|
+
try:
|
|
288
|
+
info.set("kernel", platform.platform())
|
|
289
|
+
except Exception:
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
# Shell
|
|
293
|
+
info.set("shell", "powershell.exe")
|
|
294
|
+
|
|
295
|
+
# Desktop environment
|
|
296
|
+
info.set("de", "Desktop Window Manager")
|
|
297
|
+
|
|
298
|
+
# GPU via WMIC (best-effort)
|
|
299
|
+
try:
|
|
300
|
+
import subprocess
|
|
301
|
+
|
|
302
|
+
result = subprocess.run(
|
|
303
|
+
["wmic", "path", "win32_videocontroller", "get", "name"],
|
|
304
|
+
capture_output=True,
|
|
305
|
+
text=True,
|
|
306
|
+
timeout=5,
|
|
307
|
+
)
|
|
308
|
+
if result.returncode == 0:
|
|
309
|
+
lines = [
|
|
310
|
+
line.strip()
|
|
311
|
+
for line in result.stdout.splitlines()
|
|
312
|
+
if line.strip() and line.strip() != "Name"
|
|
313
|
+
]
|
|
314
|
+
if lines:
|
|
315
|
+
info.set("gpu", lines[0])
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
return info
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def collect_all() -> SystemInfo:
|
|
323
|
+
"""Collect all available system information."""
|
|
324
|
+
info = collect_common()
|
|
325
|
+
platform_info = collect_platform()
|
|
326
|
+
info.update_from(platform_info.to_dict())
|
|
327
|
+
return info
|
larpfetch/config.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""TOML configuration loading and management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tomllib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
# Platform default config paths
|
|
12
|
+
if sys.platform == "darwin":
|
|
13
|
+
_DEFAULT_CONFIG_DIR = Path.home() / "Library" / "Application Support" / "larpfetch"
|
|
14
|
+
elif sys.platform == "win32":
|
|
15
|
+
_DEFAULT_CONFIG_DIR = Path(os.environ.get("APPDATA", "~")) / "larpfetch"
|
|
16
|
+
else:
|
|
17
|
+
_xdg = Path(os.environ.get("XDG_CONFIG_HOME", ""))
|
|
18
|
+
if _xdg.is_absolute():
|
|
19
|
+
_DEFAULT_CONFIG_DIR = _xdg / "larpfetch"
|
|
20
|
+
else:
|
|
21
|
+
_DEFAULT_CONFIG_DIR = Path.home() / ".config" / "larpfetch"
|
|
22
|
+
|
|
23
|
+
DEFAULT_CONFIG_PATH = _DEFAULT_CONFIG_DIR / "config.toml"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resolve_config_path(explicit: str | None = None) -> Path | None:
|
|
27
|
+
"""Return the config path to use, or None if no config exists."""
|
|
28
|
+
if explicit:
|
|
29
|
+
p = Path(explicit)
|
|
30
|
+
if not p.is_file():
|
|
31
|
+
raise FileNotFoundError(f"Config file not found: {explicit}")
|
|
32
|
+
return p
|
|
33
|
+
if DEFAULT_CONFIG_PATH.is_file():
|
|
34
|
+
return DEFAULT_CONFIG_PATH
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_config(path: str | None = None) -> dict[str, Any]:
|
|
39
|
+
"""Load and parse a TOML config file.
|
|
40
|
+
|
|
41
|
+
Returns a dict with keys: default, profiles, appearance.
|
|
42
|
+
Missing sections default to empty dicts.
|
|
43
|
+
Raises FileNotFoundError or tomllib.TOMLDecodeError on failure.
|
|
44
|
+
"""
|
|
45
|
+
config_path = _resolve_config_path(path)
|
|
46
|
+
if config_path is None:
|
|
47
|
+
return {"default": {}, "profiles": {}, "appearance": {}}
|
|
48
|
+
|
|
49
|
+
with open(config_path, "rb") as f:
|
|
50
|
+
data = tomllib.load(f)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
"default": data.get("default", {}),
|
|
54
|
+
"profiles": data.get("profiles", {}),
|
|
55
|
+
"appearance": data.get("appearance", {}),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_default_profile(config: dict[str, Any]) -> dict[str, str]:
|
|
60
|
+
"""Extract the default profile as a flat string dict."""
|
|
61
|
+
return {k: str(v) for k, v in config.get("default", {}).items()}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_named_profiles(config: dict[str, Any]) -> dict[str, dict[str, str]]:
|
|
65
|
+
"""Extract all named profiles as flat string dicts."""
|
|
66
|
+
profiles: dict[str, dict[str, str]] = {}
|
|
67
|
+
for name, values in config.get("profiles", {}).items():
|
|
68
|
+
profiles[name] = {k: str(v) for k, v in values.items()}
|
|
69
|
+
return profiles
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_appearance(config: dict[str, Any]) -> dict[str, Any]:
|
|
73
|
+
"""Extract appearance settings."""
|
|
74
|
+
return config.get("appearance", {})
|
larpfetch/easter_eggs.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Easter eggs and humor for larpfetch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from larpfetch.models import SystemInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _is_implausible_memory(mem: str) -> bool:
|
|
12
|
+
"""Check if a memory string is absurdly large (> 1 TiB)."""
|
|
13
|
+
match = re.search(r"([\d.]+)\s*(GiB|TiB|PiB|EiB)", mem, re.IGNORECASE)
|
|
14
|
+
if not match:
|
|
15
|
+
return False
|
|
16
|
+
value = float(match.group(1))
|
|
17
|
+
unit = match.group(2).lower()
|
|
18
|
+
if unit == "pib":
|
|
19
|
+
return True
|
|
20
|
+
if unit == "eib":
|
|
21
|
+
return True
|
|
22
|
+
if unit == "tib" and value >= 1:
|
|
23
|
+
return True
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _compute_authenticity(real: SystemInfo, resolved: SystemInfo, real_shit: bool) -> int:
|
|
28
|
+
"""Compute an authenticity percentage (0-100)."""
|
|
29
|
+
if real_shit:
|
|
30
|
+
return 100
|
|
31
|
+
|
|
32
|
+
real_d = real.to_dict()
|
|
33
|
+
resolved_d = resolved.to_dict()
|
|
34
|
+
|
|
35
|
+
if not real_d:
|
|
36
|
+
return 50
|
|
37
|
+
|
|
38
|
+
matches = sum(1 for k in real_d if k in resolved_d and real_d[k] == resolved_d[k])
|
|
39
|
+
total = max(len(real_d), 1)
|
|
40
|
+
pct = int((matches / total) * 100)
|
|
41
|
+
# In LARP mode, authenticity should be low if user is faking a lot
|
|
42
|
+
return max(0, min(100, pct))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_authenticity_line(
|
|
46
|
+
real: SystemInfo, resolved: SystemInfo, real_shit: bool, easter_eggs: bool
|
|
47
|
+
) -> str | None:
|
|
48
|
+
"""Return the authenticity line, or None if disabled."""
|
|
49
|
+
if not easter_eggs:
|
|
50
|
+
return None
|
|
51
|
+
pct = _compute_authenticity(real, resolved, real_shit)
|
|
52
|
+
return f"Authenticity: {pct}%"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_extra_lines(
|
|
56
|
+
resolved: SystemInfo, real: SystemInfo, real_shit: bool, easter_eggs: bool
|
|
57
|
+
) -> list[str]:
|
|
58
|
+
"""Return additional humorous or informational lines."""
|
|
59
|
+
if not easter_eggs:
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
lines: list[str] = []
|
|
63
|
+
|
|
64
|
+
if real_shit:
|
|
65
|
+
lines.append("Source: reality (unfortunately)")
|
|
66
|
+
return lines
|
|
67
|
+
|
|
68
|
+
# Check for implausible memory
|
|
69
|
+
mem = resolved.get("memory", "")
|
|
70
|
+
if _is_implausible_memory(mem):
|
|
71
|
+
lines.append("Source: trust me bro")
|
|
72
|
+
|
|
73
|
+
# Check for absurdly high package count
|
|
74
|
+
pc = resolved.get("package_count", "")
|
|
75
|
+
if pc.isdigit() and int(pc) > 99999:
|
|
76
|
+
lines.append("Reality Leakage: 100.00%")
|
|
77
|
+
|
|
78
|
+
# Check if real hardware is better than LARP (out-LARP detection)
|
|
79
|
+
if not real_shit and real.fields:
|
|
80
|
+
# Conservative thresholds for "impressive" real hardware
|
|
81
|
+
real_mem = real.get("memory", "")
|
|
82
|
+
if _is_implausible_memory(real_mem):
|
|
83
|
+
lines.append("Reality has out-LARPed the LARP.")
|
|
84
|
+
|
|
85
|
+
# The allegations were true - deterministic based on username
|
|
86
|
+
username = resolved.get("username", "")
|
|
87
|
+
if username:
|
|
88
|
+
h = int(hashlib.md5(username.encode()).hexdigest()[:8], 16)
|
|
89
|
+
if h % 100 == 0: # 1% chance
|
|
90
|
+
lines.append("The allegations were true.")
|
|
91
|
+
|
|
92
|
+
return lines
|