Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions fetch_cord/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CommandProvider,
FastfetchProvider,
Fetch,
FieldProvider,
NativeProvider,
get_infos,
)
Expand Down Expand Up @@ -164,17 +165,29 @@ def main(
pause = PauseWatcher(pause_when)

os_type = platform.system()
# Only the commands defined for this OS are run. The structured fastfetch
# provider fills the common fields first; command/native providers only
# fill the gaps (e.g. motherboard/resolution/system_type, or everything on
# Windows where fastfetch may not be installed).
# Only the commands defined for this OS are run.
command_map = {
component_type: value[os_type]
for component_type, value in config["commands"].items()
if os_type in value
}

fetch = Fetch([FastfetchProvider(), CommandProvider(command_map), NativeProvider()])
# Native fetchers first: they need no tool installed and cost a few
# milliseconds. fastfetch then covers everything native does not implement
# yet, and the commands cover what neither reaches.
#
# This is only safe because a native fetcher returns None when it is not
# confident rather than guessing - an unreadable registry key, or firmware
# boilerplate like "To Be Filled By O.E.M." - so Fetch.snapshot hands the
# field straight to fastfetch. Native coverage can grow one field at a
# time without any platform ever losing detail it has today.
providers: list[FieldProvider] = [
NativeProvider(),
FastfetchProvider(),
CommandProvider(command_map),
]

fetch = Fetch(providers)

if getattr(args, "dry_run", False):
snapshot = fetch.snapshot()
Expand Down
15 changes: 12 additions & 3 deletions fetch_cord/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,20 @@ def fetch(self, skip: set[str] | None = None) -> dict[str, str]:


class NativeProvider:
"""Uses the bundled native (Windows) fetchers for fields not otherwise
covered, e.g. the installed-packages count on Windows."""
"""Uses the bundled native (Windows) fetchers, which read the registry and
Win32 directly. Adds no tool dependency and no subprocess, and returns {}
for anything it cannot read so another provider fills the gap."""

name = "native"
NATIVE_COMPONENTS = ("packages",)
NATIVE_COMPONENTS = (
"cpu",
"host",
"kernel",
"mem",
"motherboard",
"os",
"packages",
)

def __init__(self) -> None:
self._native = native_module
Expand Down
25 changes: 20 additions & 5 deletions fetch_cord/native/Windows/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
import os
from types import ModuleType

if os.name == "nt":
from . import packages
from . import cpu, host, kernel, mem, motherboard, os_info, packages

def fetch(component_class: str) -> str | None:
fetch_map = {"packages": packages}
fetch_map: dict[str, ModuleType] = {
"cpu": cpu,
"host": host,
"kernel": kernel,
"mem": mem,
"motherboard": motherboard,
"os": os_info,
"packages": packages,
}

if component_class not in fetch_map:
def fetch(component_class: str) -> str | None:
module = fetch_map.get(component_class)
if module is None:
return None

return str(fetch_map[component_class].fetch())
# A fetcher returns None when the value is not readable on this
# machine; pass that through rather than stringifying it, so the next
# provider fills the gap instead of showing "None".
value = module.fetch()

return str(value) if value else None

else:

Expand Down
12 changes: 12 additions & 0 deletions fetch_cord/native/Windows/cpu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Processor name, matching Win32_Processor.Name.

e.g. "Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz"
"""

from fetch_cord.native.Windows import registry


def fetch() -> str | None:
name = registry.read(registry.CPU0, "ProcessorNameString")

return name.strip() if name else None
10 changes: 10 additions & 0 deletions fetch_cord/native/Windows/host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Motherboard product name, matching Win32_BaseBoard.Product.

e.g. "PRIME B760-PLUS D4"
"""

from fetch_cord.native.Windows import motherboard, registry


def fetch() -> str | None:
return motherboard.usable(registry.read(registry.BIOS, "BaseBoardProduct"))
23 changes: 23 additions & 0 deletions fetch_cord/native/Windows/kernel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Windows version string, matching Win32_OperatingSystem.Version.

e.g. "10.0.26100"
"""

from fetch_cord.native.Windows import registry


def fetch() -> str | None:
build = registry.read(registry.CURRENT_VERSION, "CurrentBuildNumber")
if build is None:
return None

major = registry.read_int(registry.CURRENT_VERSION, "CurrentMajorVersionNumber")
minor = registry.read_int(registry.CURRENT_VERSION, "CurrentMinorVersionNumber")

# Present since Windows 10; fall back to the values every release carries.
if major is None:
major = 10
if minor is None:
minor = 0

return f"{major}.{minor}.{build}"
25 changes: 25 additions & 0 deletions fetch_cord/native/Windows/mem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Memory use, matching the format the PowerShell command produces.

e.g. "12.40 GB / 31.92 GB"

Used is total minus available, which is how the Linux and macOS commands
define it too, so the value stays comparable across platforms.
"""

import psutil

GIB = 1024**3


def fetch() -> str | None:
try:
memory = psutil.virtual_memory()
except Exception:
return None

if not memory.total:
return None

used = memory.total - memory.available

return f"{used / GIB:.2f} GB / {memory.total / GIB:.2f} GB"
42 changes: 42 additions & 0 deletions fetch_cord/native/Windows/motherboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Motherboard vendor and product, matching the Windows PowerShell command.

Joins BaseBoardManufacturer and BaseBoardProduct the same way Linux reports
board_vendor + board_name, so the host cycle's app_id/top_line can match
board-specific entries in motherboards.json (e.g. TUF) rather than only the
generic vendor.

e.g. "ASUSTeK COMPUTER INC. PRIME B760-PLUS D4"
"""

from fetch_cord.native.Windows import registry

# Strings firmware ships when the vendor never filled the field in. Returning
# None for these lets another provider supply something better.
PLACEHOLDERS = {
"",
"to be filled by o.e.m.",
"default string",
"system manufacturer",
"not applicable",
"not specified",
"none",
"o.e.m.",
"unknown",
}


def usable(value: str | None) -> str | None:
"""The value, unless it is firmware boilerplate."""
if value is None:
return None

value = value.strip()

return None if value.lower() in PLACEHOLDERS else value


def fetch() -> str | None:
manufacturer = usable(registry.read(registry.BIOS, "BaseBoardManufacturer"))
product = usable(registry.read(registry.BIOS, "BaseBoardProduct"))
parts = [part for part in (manufacturer, product) if part]
return " ".join(parts) or None
28 changes: 28 additions & 0 deletions fetch_cord/native/Windows/os_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Windows edition name, matching Win32_OperatingSystem.Caption.

The registry's ProductName still reads "Windows 10 Pro" on Windows 11, so the
build number decides the release and the name is corrected to match what WMI
would report.
"""

from fetch_cord.native.Windows import registry

# Windows 11 starts here; the registry name was never updated for it.
WINDOWS_11_BUILD = 22000


def fetch() -> str | None:
product = registry.read(registry.CURRENT_VERSION, "ProductName")
if product is None:
return None

build = registry.read_int(registry.CURRENT_VERSION, "CurrentBuildNumber")
if build is not None and build >= WINDOWS_11_BUILD:
product = product.replace("Windows 10", "Windows 11")

# WMI's Caption is prefixed; match it so the value is interchangeable with
# the PowerShell command this replaces.
if not product.startswith("Microsoft "):
product = f"Microsoft {product}"

return product
56 changes: 56 additions & 0 deletions fetch_cord/native/Windows/registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Small helpers for reading the Windows registry.

Every native Windows fetcher reads from here rather than shelling out, so the
whole native path stays free of subprocesses and of any external tool.
"""

import contextlib
import sys
from typing import TYPE_CHECKING

# Importable on any platform so the fetchers - and their tests - can be
# imported off Windows; every read simply reports None there. The TYPE_CHECKING
# arm keeps winreg visible to mypy when it runs on a non-Windows host.
if sys.platform == "win32" or TYPE_CHECKING:
import winreg

# Where the values the fetchers need live.
CURRENT_VERSION = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"
BIOS = "HARDWARE\\DESCRIPTION\\System\\BIOS"
CPU0 = "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0"


def read(path: str, name: str) -> str | None:
"""Read one HKEY_LOCAL_MACHINE value, or None if it is not there.

A missing key or value is an expected outcome - hardware varies and values
come and go between Windows releases - so it is reported as None and the
next provider fills the gap.
"""
if sys.platform != "win32":
return None

# Read the 64-bit view explicitly: a 32-bit Python would otherwise be
# redirected to WOW6432Node and see a different (or missing) ProductName.
access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY

with (
contextlib.suppress(OSError),
winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path, 0, access) as key,
):
value = winreg.QueryValueEx(key, name)[0]
return str(value) if value != "" else None

return None


def read_int(path: str, name: str) -> int | None:
"""Read one registry value as an int, or None if it is absent or not numeric."""
value = read(path, name)
if value is None:
return None

try:
return int(value)
except ValueError:
return None
Loading
Loading