summarylogtreecommitdiffstats
path: root/fbrokendesktop.py
blob: 276e1cab881134caf559d6eca18f1fb8b7503771 (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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#!/bin/python3

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

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_hidden: 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 not de.getNoDisplay() or show_hidden:
            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/missing executables"
    )
    parser.add_argument(
        "--hidden",
        "--show_hidden",
        action="store_true",
        help="show hidden (or NoDisplay) desktop entries",
        default=False,
    )
    parser.add_argument(
        "-d",
        "--delete",
        action="store_true",
        help="delete the missing entries",
        default=False,
    )
    parser.add_argument(
        "-u",
        "--user",
        action="store_true",
        help="list only the entries which are owned by the current user",
        default=False,
    )
    args = parser.parse_args()
    for d in find_desktop_directories():
        for df in find_missing_desktop_files(d, args.hidden):
            if args.user and not os.access(df, os.W_OK | os.R_OK):
                continue
            print(df)
            if args.delete:
                os.remove(df)
                print(f"Deleted {df}")