#!/usr/bin/env python3
# Resolve a module state to an icon path from the desktop's icon theme.
# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
# Licensed under the GNU General Public License v2 only.
#
# waybar's `image` module runs a command and reads "$path\n$tooltip" from its
# stdout. That is the only way a themed icon reaches a module that is not the
# taskbar or the tray, so volume, microphone and presentation mode all come
# through here.
#
#     wb-icon volume     # reads wpctl, prints the matching icon
#     wb-icon mic
#     wb-icon --selftest # every name this bar asks for must resolve
#
# Icons keep their own colours: this desktop's icon theme draws them with
# baked-in gradients, so they are not recolourable and are not meant to be.
# Only the two icons this repo ships are monochrome.

import subprocess
import sys

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk  # noqa: E402  (must follow require_version)

# The icon theme to draw from. Kept in one place because changing it is a
# single edit, and because --selftest needs the same value the modules use.
THEME = "Material-Black-Plum-Suru"

SIZE = 22

# State to icon name. Every name here is checked by --selftest, so a theme that
# drops one fails at install time rather than rendering an empty pill.
ICONS = {
    "volume": {
        "muted": "audio-volume-muted",
        "low": "audio-volume-low",
        "medium": "audio-volume-medium",
        "high": "audio-volume-high",
    },
    "mic": {
        "muted": "microphone-sensitivity-muted",
        "on": "audio-input-microphone",
    },
}


def lookup(name):
    """Absolute path of an icon in THEME, or None."""
    theme = Gtk.IconTheme.new()
    theme.set_custom_theme(THEME)
    info = theme.lookup_icon(name, SIZE, 0)
    return info.get_filename() if info else None


def wpctl(node):
    """(volume percent, muted) for a wireplumber node, or (None, False).

    wpctl prints e.g. "Volume: 0.62" or "Volume: 0.62 [MUTED]".
    """
    try:
        out = subprocess.run(
            ["wpctl", "get-volume", node],
            capture_output=True, text=True, timeout=2,
        ).stdout
    except (OSError, subprocess.SubprocessError):
        return None, False
    if "Volume:" not in out:
        return None, False
    muted = "MUTED" in out
    try:
        return round(float(out.split("Volume:")[1].split()[0]) * 100), muted
    except (IndexError, ValueError):
        return None, muted


def volume_state(pct, muted):
    if muted or pct == 0:
        return "muted"
    if pct < 34:
        return "low"
    if pct < 67:
        return "medium"
    return "high"


def emit(name, tooltip):
    """Print what the image module expects: a path, then a tooltip."""
    path = lookup(name)
    if not path:
        # An unresolved icon is a broken bar, not a warning: say so on stderr
        # and print nothing, so the module stays empty rather than showing a
        # stale icon.
        print(f"wb-icon: no icon named {name!r} in {THEME}", file=sys.stderr)
        return 1
    print(path)
    print(tooltip)
    return 0


def selftest():
    """Every icon this bar asks for, including the ones set in CSS."""
    # The workspace icons live in styles/modules.css rather than here, because
    # waybar's format-icons takes text and not paths. They are still this
    # bar's icons, so they are checked here too.
    workspace_icons = [
        "web-browser", "utilities-terminal", "text-editor", "network-server",
        "document-edit", "applications-graphics", "internet-chat",
        "input-gaming",
    ]
    clock_icons = ["x-office-calendar", "clock"]

    # Presentation mode is a custom module: statusctl emits the state as a
    # class and modules.css picks the icon, so these names appear nowhere in
    # this file. They are still the bar's icons, so they are checked here.
    presentation_icons = [
        "preferences-desktop-screensaver",  # off
        "x-office-presentation",            # on
        "dialog-warning",                   # no state file
    ]

    names = sorted(
        {n for group in ICONS.values() for n in group.values()}
        | set(workspace_icons) | set(clock_icons) | set(presentation_icons)
    )
    missing = [n for n in names if not lookup(n)]
    for n in names:
        print(f"  {'ok  ' if n not in missing else 'MISS'} {n}")
    if missing:
        print(f"\n{len(missing)} icon(s) missing from {THEME}", file=sys.stderr)
        return 1
    print(f"\nall {len(names)} icons resolve in {THEME}")
    return 0


def main(argv):
    if len(argv) < 2 or argv[1] in ("-h", "--help"):
        print(__doc__ or "usage: wb-icon <volume|mic|idle|--selftest> [state]")
        return 0

    what = argv[1]

    if what == "--selftest":
        return selftest()

    if what == "volume":
        pct, muted = wpctl("@DEFAULT_AUDIO_SINK@")
        if pct is None:
            return emit(ICONS["volume"]["muted"], "Volume: unavailable")
        state = volume_state(pct, muted)
        label = "muted" if state == "muted" else f"{pct}%"
        return emit(ICONS["volume"][state], f"Volume: {label}")

    if what == "mic":
        pct, muted = wpctl("@DEFAULT_AUDIO_SOURCE@")
        if pct is None:
            return emit(ICONS["mic"]["muted"], "Microphone: unavailable")
        state = "muted" if muted or pct == 0 else "on"
        label = "muted" if state == "muted" else f"{pct}%"
        return emit(ICONS["mic"][state], f"Microphone: {label}")

    print(f"wb-icon: unknown subject {what!r}", file=sys.stderr)
    return 2


if __name__ == "__main__":
    sys.exit(main(sys.argv))
