diff options
| author | coasteen <coasteen@proton.me> | 2026-07-09 10:42:28 +0330 |
|---|---|---|
| committer | coasteen <coasteen@proton.me> | 2026-07-09 10:42:28 +0330 |
| commit | 8cbadb61604667b407b53ee1258433994fa4765a (patch) | |
| tree | 97765fb29418fd6278fcb4cbb9760893afaf7a58 /local/bin/dmenu.py | |
Diffstat (limited to 'local/bin/dmenu.py')
| -rwxr-xr-x | local/bin/dmenu.py | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/local/bin/dmenu.py b/local/bin/dmenu.py new file mode 100755 index 0000000..948dbd8 --- /dev/null +++ b/local/bin/dmenu.py @@ -0,0 +1,161 @@ +#!/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() |
