Silly Fakefetch (Neofetch-like) to dom0 just for fun

Original forum link
https://forum.qubes-os.org/t/40613
Original poster
hUt4Ke107Y7VyK
Created at
2026-04-14 05:39:32
Posts count
1
Likes count
7

I did not want to install Neofetch or any other modern version of it in my dom0 and those would not have worked properly anyway, so I ended up prompting this script to the existence.

It uses information from: - /etc/os-release - /proc/uptime - /proc/cpuinfo - xl info - qvm-ls --raw-list

I used the ASCII art from this post by @weyoun.six in my script.

fakefetch.py
import base64
import gzip
import io
import os
import subprocess
import shutil
import re
import textwrap

# --- 1. System Information Gathering ---

def get_os_release():
    try:
        with open("/etc/os-release") as f:
            for line in f:
                if line.startswith("PRETTY_NAME="):
                    return line.split("=")[1].strip().strip('"')
    except Exception:
        pass
    return "Qubes OS"

def get_uptime():
    try:
        with open('/proc/uptime', 'r') as f:
            uptime_seconds = float(f.readline().split()[0])
            hours, remainder = divmod(int(uptime_seconds), 3600)
            minutes, _ = divmod(remainder, 60)
            return f"{hours}h {minutes}m"
    except Exception:
        return "Unknown"

def get_cpu_info():
    try:
        with open("/proc/cpuinfo") as f:
            for line in f:
                if line.startswith("model name"):
                    return line.split(":")[1].strip()
    except Exception:
        return "Unknown"

def get_xen_and_memory():
    try:
        xl_out = subprocess.check_output(['sudo', 'xl', 'info'], stderr=subprocess.DEVNULL).decode()
        data = {}
        for line in xl_out.splitlines():
            if ":" in line:
                key, val = line.split(":", 1)
                data[key.strip()] = val.strip()

        xen_ver = f"{data.get('xen_major', 'X')}.{data.get('xen_minor', 'X')}{data.get('xen_extra', '')}"
        total_mem = int(data.get("total_memory", 0))
        free_mem = int(data.get("free_memory", 0))
        return xen_ver, total_mem - free_mem, total_mem
    except Exception:
        return "Unknown", 0, 0

def get_qubes_stats():
    try:
        total_out = subprocess.check_output(['qvm-ls', '--raw-list'], stderr=subprocess.DEVNULL).decode()
        running_out = subprocess.check_output(['qvm-ls', '--raw-list', '--running'], stderr=subprocess.DEVNULL).decode()
        return len([x for x in running_out.splitlines() if x.strip()]), len([x for x in total_out.splitlines() if x.strip()])
    except Exception:
        return 0, 0

# Store raw data as tuples so we can wrap the values independently of the colored labels
raw_info = [
    ("OS", get_os_release()),
    ("Kernel", os.uname().release),
    ("Hypervisor", f"Xen {get_xen_and_memory()[0]}"),
    ("Uptime", get_uptime()),
    ("Qubes", f"{get_qubes_stats()[0]} running / {get_qubes_stats()[1]} total"),
    ("CPU", get_cpu_info()),
    ("RAM", f"{get_xen_and_memory()[1]} MiB / {get_xen_and_memory()[2]} MiB")
]

# --- 2. Dynamic Scaling & Cropping Logic ---

def crop_ascii(ascii_lines):
    lines = [line.rstrip() for line in ascii_lines]
    while lines and not lines[0]:
        lines.pop(0)
    while lines and not lines[-1]:
        lines.pop()
    if lines:
        min_indent = min(len(line) - len(line.lstrip()) for line in lines if line.strip())
        lines = [line[min_indent:] for line in lines]
    return lines

def scale_ascii(ascii_lines, target_width):
    if not ascii_lines: return ascii_lines
    original_width = max(len(line) for line in ascii_lines)

    if original_width <= target_width or target_width <= 0:
        return ascii_lines

    padded_lines = [line.ljust(original_width) for line in ascii_lines]
    scale_ratio = target_width / original_width
    scaled_lines = []
    num_rows = len(padded_lines)
    target_height = max(1, int(num_rows * scale_ratio))

    for i in range(target_height):
        orig_y = min(int(i / scale_ratio), num_rows - 1)
        orig_line = padded_lines[orig_y]

        new_line = ""
        for j in range(target_width):
            orig_x = min(int(j / scale_ratio), original_width - 1)
            new_line += orig_line[orig_x]
        scaled_lines.append(new_line.rstrip())

    return scaled_lines

# Setup dimensions to prevent layout overlap
try:
    term_width = shutil.get_terminal_size().columns
except Exception:
    term_width = 80

left_margin = 2
padding = 4

# --- 3. ASCII Art Processing ---

d = "H4sIAAAAAAAAA+3T0Q2AIAwE0H+nYBY3IN1/F4GgAXvXVuKPifdHvHtRoynFkvMWq51x61nFnOi6O+MTc+rP+Bz3+BUl7CVgTXwpgcTAGI8oPZSoYW83JRliEVyYjJ/4EvHCd8EUmeMRd0Z09I8KjM6AORQYEp9D4tFaCfjmbWBCCBAxLqfV62kRaZCMpxVBp80PHAZb0PEGAAA="
l = gzip.GzipFile(fileobj=io.BytesIO(base64.b64decode(d))).read().decode("utf-8")
c = {"Q": (48, 185, 214), "B": (29, 77, 140), "S": (4, 45, 107)}

raw_ascii_lines = l.split('\n')
raw_ascii_lines = crop_ascii(raw_ascii_lines)

# Cap the ASCII art at 45% of the terminal width so the info column always has room
orig_ascii_width = max((len(line) for line in raw_ascii_lines), default=0)
target_ascii_width = min(orig_ascii_width, max(15, int(term_width * 0.45)))
raw_ascii_lines = scale_ascii(raw_ascii_lines, target_ascii_width)
max_ascii_width = max((len(line) for line in raw_ascii_lines), default=0)

# --- 4. Text Wrapping & Formatting ---

# Calculate exactly how much space is left for the text
info_col_width = max(20, term_width - max_ascii_width - padding - left_margin)
info_lines = []

for label, value in raw_info:
    combined_text = f"{label}: {value}"

    # Calculate a hanging indent to align multi-line values perfectly
    indent_len = len(label) + 2 
    indent_str = " " * indent_len if indent_len < info_col_width // 2 else "  "

    wrapped = textwrap.wrap(combined_text, width=info_col_width, subsequent_indent=indent_str)

    if not wrapped:
        continue

    # Re-apply the blue ANSI color ONLY to the label in the first line
    first_line = wrapped[0].replace(f"{label}:", f"\x1b[1;34m{label}\x1b[0m:", 1)
    info_lines.append(first_line)

    # Add any remaining wrapped lines
    for line in wrapped[1:]:
        info_lines.append(line)

# --- 5. Side-by-Side Output ---

print()
max_lines = max(len(raw_ascii_lines), len(info_lines))

for i in range(max_lines):
    raw_line = raw_ascii_lines[i] if i < len(raw_ascii_lines) else ""
    info_line = info_lines[i] if i < len(info_lines) else ""

    colored_ascii = "".join([f"\x1b[38;2;{c[char][0]};{c[char][1]};{c[char][2]}m{char}\x1b[0m" if char in c else char for char in raw_line])

    padding_spaces = max_ascii_width - len(raw_line) + padding
    print(f"{' ' * left_margin}{colored_ascii}{' ' * padding_spaces}{info_line}")

print()

fakefetch|690x474