Files
2026-07-12 08:16:17 +03:30

162 lines
4.0 KiB
Python
Executable File

#!/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()