#!/usr/bin/env python3 import os import subprocess import sys from pathlib import Path DMENU_ARGS = [ "dmenu", "-i", "-fn", "JetBrainsMono Nerd Font-10", "-nb", "#121212", "-nf", "#e0e0e0", "-sb", "#222222", "-sf", "#e0e0e0", "-l", "10", "-p", "Launch:", ] DESKTOP_DIRS = [ Path.home() / ".local/share/applications", Path.home() / ".local/share/flatpak/exports/share/applications", Path("/var/lib/flatpak/exports/share/applications"), Path("/usr/share/applications"), Path("/usr/local/share/applications"), ] INVALID_EXEC_PREFIXES = ("wine", "wscript", "rundll32", "winhlp32", "hh ") def parse_desktop_file(path: Path): """ Returns (name, exec_cmd) or (None, None) if the file should be skipped. Handles: NoDisplay=true, Hidden=true, missing Name/Exec, field codes, env wrappers. """ props = {} in_desktop_entry = False try: with path.open(encoding="utf-8", errors="replace") as f: for line in f: line = line.rstrip("\n") if line.startswith("["): in_desktop_entry = line == "[Desktop Entry]" continue if not in_desktop_entry or "=" not in line: continue key, _, value = line.partition("=") key = key.strip() if key not in props: props[key] = value.strip() except OSError: return None, None if props.get("NoDisplay", "").lower() == "true": return None, None if props.get("Hidden", "").lower() == "true": return None, None if props.get("Type", "") != "Application": return None, None name = props.get("Name") exec_raw = props.get("Exec") if not name or not exec_raw: return None, None exec_cmd = clean_exec(exec_raw) if not exec_cmd: return None, None lower = exec_cmd.lower() if any(lower.startswith(p) for p in INVALID_EXEC_PREFIXES): return None, None terminal = props.get("Terminal", "").lower() == "true" if terminal: term = os.environ.get("TERMINAL", "xterm") exec_cmd = f"{term} -e {exec_cmd}" return name, exec_cmd def clean_exec(exec_raw: str) -> str: tokens = exec_raw.split() cleaned = [] for token in tokens: if token.startswith("%") and len(token) == 2 and token[1].isalpha(): continue cleaned.append(token) return " ".join(cleaned).strip() def collect_apps(): seen_stems = {} for directory in DESKTOP_DIRS: if not directory.is_dir(): continue for desktop_file in sorted(directory.glob("*.desktop")): stem = desktop_file.stem.lower() if stem not in seen_stems: seen_stems[stem] = desktop_file apps = {} for desktop_file in seen_stems.values(): name, cmd = parse_desktop_file(desktop_file) if name and cmd and name not in apps: apps[name] = cmd return apps def run_dmenu(app_names: list[str]) -> str | None: names_input = "\n".join(sorted(app_names, key=str.casefold)).encode() try: result = subprocess.run( DMENU_ARGS, input=names_input, capture_output=True, ) except FileNotFoundError: sys.exit("error: dmenu not found in PATH") if result.returncode != 0: return None return result.stdout.decode().strip() def launch(cmd: str): subprocess.Popen( cmd, shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, ) def main(): apps = collect_apps() if not apps: sys.exit("error: no applications found") selected = run_dmenu(list(apps.keys())) if not selected: return cmd = apps.get(selected) if not cmd: sys.exit(f"error: no command found for '{selected}'") launch(cmd) if __name__ == "__main__": main()