init
This commit is contained in:
Executable
+161
@@ -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()
|
||||
Reference in New Issue
Block a user