summarylogtreecommitdiffstats
path: root/fbrokendesktop.py
blob: 496933395c5b0605b5fe2af98c5116d455de8267 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#!/usr/bin/python3

import argparse
import glob
import re
import shlex
import shutil
import subprocess
import sys

from typing import cast
from os import path

from xdg import BaseDirectory
from xdg.DesktopEntry import DesktopEntry

# allow matching empty envs with .*
env_re = re.compile(r"\w+=.*")

# installed gapps
gapps = []


def strip_command_parent(cmd_args: list[str], is_first: bool = True) -> list[str]:
    while cmd_args and env_re.match(cmd_args[0]):
        cmd_args = cmd_args[1:]
    try:
        cmd = cmd_args[0]
    except IndexError:
        cmd = ""
    if is_first and cmd == "exec":
        return strip_command_parent(cmd_args[1:], is_first=False)
    if cmd == "env" or cmd.endswith("/env"):
        return strip_command_parent(cmd_args[1:], is_first=False)
    return cmd_args


def is_valid_gapp_cmd(cmd: str):
    app_id = cmd
    if not gapps:
        try:
            output = subprocess.check_output(["gapplication", "list-apps"], text=True)
            gapps.extend((output or "").split("\n"))
        except subprocess.CalledProcessError:
            gapps.append("")
    return app_id in gapps


def is_gapp_cmd(cmd_args: list[str]):
    return (
        len(cmd_args) > 2
        and (cmd_args[0] == "gapplication" or cmd_args[0].endswith("/gapplication"))
        and cmd_args[1] == "launch"
    )


def find_missing_desktop_files(desktop_dir: str, show_all: bool):
    for df in glob.iglob("*.desktop", root_dir=desktop_dir):
        file_path = path.join(desktop_dir, df)
        de = DesktopEntry(file_path)
        file_name = shlex.quote(de.getFileName())
        if de.getHidden():
            yield file_name
            continue

        if show_all or not de.getNoDisplay():
            if exc := cast(str | None, (de.getExec() or de.getTryExec())):
                try:
                    cmd = shlex.split(exc)
                    cmd = strip_command_parent(cmd)
                    if is_gapp_cmd(cmd):
                        if not is_valid_gapp_cmd(cmd[2]):
                            yield file_name
                    elif not (cmd and shutil.which(cmd[0])):
                        yield file_name
                except ValueError as err:
                    print(f"Error parsing '{file_path}': {err}", file=sys.stderr)


def find_desktop_directories():
    """
    https://wiki.archlinux.org/title/desktop_entries#Modify_desktop_files
    https://wiki.archlinux.org/title/XDG_Autostart#Directories
    """
    yield from BaseDirectory.load_data_paths("applications")
    yield from BaseDirectory.load_config_paths("autostart")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Find desktop entries files with broken executables"
    )
    parser.add_argument(
        "-a",
        "--all",
        action="store_true",
        help='show all desktop entries regardless of "NoDisplay" value',
        default=False,
    )
    args = parser.parse_args()
    for d in find_desktop_directories():
        for df in find_missing_desktop_files(d, args.all):
            print(df)