#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vi: ft=python:tw=0:sw=4:ts=4:noet
# Author:		Jan Christoph Ebersbach <jceb@e-jc.de>

# dex
# DesktopEntry Execution, is a program to generate and execute DesktopEntry
# files of the type Application
#
# Depends: Python
#
# Copyright (C) 2010 - 2024 Jan Christoph Ebersbach
#
# http://www.e-jc.de/
#
# All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.

__version__ = "0.10.1"

import os
import subprocess
import sys

try:
    from os import scandir
except ImportError:

    class _DirEntry:
        def __init__(self, name, path):
            self.name = name
            self.path = path

        def is_file(self):
            return os.path.isfile(self.path)

    def scandir(path="."):
        return [_DirEntry(f, os.path.join(path, f)) for f in os.listdir(path)]


# DesktopEntry exceptions
class DesktopEntryTypeException(Exception):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)


class ApplicationExecException(Exception):
    def __init__(self, value):
        self.value = value
        Exception.__init__(self, value)

    def __str__(self):
        return repr(self.value)


# DesktopEntry class definitions
class DesktopEntry(object):
    """
    Implements some parts of Desktop Entry specification:
    http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.1.html
    """

    def __init__(self, filename=None):
        """
        @param	filename	Desktop Entry File
        """
        if (
            filename is not None
            and os.path.islink(filename)
            and os.readlink(filename) == os.path.sep + os.path.join("dev", "null")
        ):
            # ignore links to /dev/null
            pass
        elif filename is None or not os.path.isfile(filename):
            raise IOError("File does not exist: %s" % filename)
        self._filename = filename
        self.groups = {}

    def __str__(self):
        if self.Name:
            return self.Name
        elif self.filename:
            return self.filename
        return repr(self)

    def __lt__(self, y):
        return self.filename < y.filename

    @property
    def filename(self):
        """
        The absolute filename
        """
        return self._filename

    @classmethod
    def fromfile(cls, filename):
        """Create DesktopEntry for file

        @params	filename	Create a DesktopEntry object for file and determine
                                                the type automatically
        """

        de = cls(filename=filename)

        # determine filetype
        de_type = "Link"
        if os.path.exists(filename):
            if os.path.isdir(filename):
                de_type = "Directory"
                # TODO fix the value for directories
                de.set_value("??", filename)
            else:
                de_type = "Application"
                de.set_value("Exec", filename)
                de.set_value("Name", os.path.basename(filename))
                if os.name == "posix":
                    whatis = subprocess.Popen(
                        ["whatis", filename],
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE,
                    )
                    stdout, stderr = whatis.communicate()
                    res = stdout.decode(sys.stdin.encoding).split("- ", 1)
                    if len(res) == 2:
                        de.set_value("Comment", res[1].split(os.linesep, 1)[0])
        else:
            # type Link
            de.set_value("URL", filename)

        de.set_value("Type", de_type)

        return de

    def load(self):
        """Load or reload contents of desktop entry file"""
        self.groups = {}  # clear settings
        grp_desktopentry = "Desktop Entry"

        with open(self.filename, "r") as fp:
            current_group = None
            for l in fp:
                l = l.strip("\n")
                # handle comments and empty lines
                if l.startswith("#") or l.isspace() or not l:
                    continue

                # handle groups
                if l.startswith("["):
                    if not l.endswith("]"):
                        raise DesktopEntryTypeException(
                            "'%s' is not a valid Desktop Entry because of line '%s'."
                            % (self.filename, l)
                        )
                    group = l[1:-1]
                    if group in self.groups:
                        raise DesktopEntryTypeException(
                            "'%s' is not a valid Desktop Entry because group '%s' is specified multiple times."
                            % (self.filename, group)
                        )
                    current_group = group
                    continue

                # handle all the other lines
                if not current_group:
                    raise DesktopEntryTypeException(
                        "'%s' is not a valid Desktop Entry because line '%s' does not belong to a group."
                        % (self.filename, l)
                    )
                kv = l.split("=", 1)
                if len(kv) != 2 or kv[0] == "":
                    raise DesktopEntryTypeException(
                        "'%s' is not a valid Desktop Entry because line '%s' is not a valid key=value pair."
                        % (self.filename, l)
                    )
                k = kv[0]
                v = kv[1]
                # TODO: parse k for locale specific settings
                # TODO: parse v for multivalue fields
                self.set_value(k, v, current_group)

        if grp_desktopentry not in self.groups:
            raise DesktopEntryTypeException(
                "'%s' is not a valid Desktop Entry group is missing." % (self.filename,)
            )
        if not self.Type or not self.Name:
            if self.Type != "Service":
                # allow files with type Service and no Name
                raise DesktopEntryTypeException(
                    "'%s' is not a valid Desktop Entry because Type or Name keys are missing."
                    % (self.filename,)
                )
        _type = self.Type
        if _type in ("Application", "Service"):
            if not self.Exec:
                raise DesktopEntryTypeException(
                    "'%s' is not a valid Desktop Entry of type '%s' because Exec is missing."
                    % (self.filename, _type)
                )
        elif _type == "Link":
            if not self.URL:
                raise DesktopEntryTypeException(
                    "'%s' is not a valid Desktop Entry of type '%s' because URL is missing."
                    % (self.filename, _type)
                )
        elif _type == "Directory":
            pass
        else:
            raise DesktopEntryTypeException(
                "'%s' is not a valid Desktop Entry because Type '%s' is unknown."
                % (self.filename, self.Type)
            )

    # another name for load
    reload = load

    def write(self, fp):
        """Write DesktopEntry to a file

        @param	fp	DesktopEntry is written to file
        """
        for group in self.groups:
            fp.write("[%s]\n" % (group,))
            for key in self.groups[group]:
                fp.write("%s=%s\n" % (key, self.groups[group][key]))

    def set_value(self, key, value, group="Desktop Entry"):
        """Set a key, value pair in group

        @param	key	Key
        @param	value	Value
        @param	group	The group key and value are set in. Default: Desktop Entry
        """
        if group not in self.groups:
            self.groups[group] = {}
        self.groups[group][key] = str(value)
        return value

    def _get_value(self, key, group="Desktop Entry", default=None):
        if not self.groups:
            self.load()
        if group not in self.groups:
            raise KeyError("Group '%s' not found." % group)
        return self.groups[group].get(key, default)

    def get_boolean(self, key, group="Desktop Entry", default=False):
        val = self._get_value(key, group=group, default=default)
        if type(val) == bool:
            return val
        if val in ["true", "True"]:
            return True
        if val in ["false", "False"]:
            return False
        raise ValueError(
            "'%s's value '%s' in group '%s' is not a boolean value." % (key, val, group)
        )

    def get_list(self, key, group="Desktop Entry", default=None):
        list_of_strings = []
        res = self.get_string(key, group=group, default=default)
        if type(res) == str:
            list_of_strings = [x for x in res.split(";") if x]
        return list_of_strings

    def get_string(self, key, group="Desktop Entry", default=""):
        return self._get_value(key, group=group, default=default)

    def get_strings(self, key, group="Desktop Entry", default=""):
        raise Exception("Not implemented yet.")

    def get_localestring(self, key, group="Desktop Entry", default=""):
        raise Exception("Not implemented yet.")

    def get_numeric(self, key, group="Desktop Entry", default=0.0):
        val = self._get_value(key, group=group, default=default)
        if type(val) == float:
            return val
        return float(val)

    @property
    def Type(self):
        return self.get_string("Type")

    @property
    def Version(self):
        return self.get_string("Version")

    @property
    def Name(self):
        # SHOULD be localestring!
        return self.get_string("Name")

    @property
    def GenericName(self):
        return self.get_localestring("GenericName")

    @property
    def NoDisplay(self):
        return self.get_boolean("NoDisplay")

    @property
    def Comment(self):
        return self.get_localestring("Comment")

    @property
    def Icon(self):
        return self.get_localestring("Icon")

    @property
    def Hidden(self):
        return self.get_boolean("Hidden")

    @property
    def OnlyShowIn(self):
        return self.get_list("OnlyShowIn")

    @property
    def NotShowIn(self):
        return self.get_list("NotShowIn")

    @property
    def TryExec(self):
        return self.get_string("TryExec")

    @property
    def Exec(self):
        return self.get_string("Exec")

    @property
    def Path(self):
        return self.get_string("Path")

    @property
    def Terminal(self):
        return self.get_boolean("Terminal")

    @property
    def MimeType(self):
        return self.get_strings("MimeType")

    @property
    def Actions(self):
        return self.get_list("Actions")

    @property
    def Categories(self):
        return self.get_strings("Categories")

    @property
    def StartupNotify(self):
        return self.get_boolean("StartupNotify")

    @property
    def StartupWMClass(self):
        return self.get_string("StartupWMClass")

    @property
    def URL(self):
        return self.get_string("URL")


class Application(DesktopEntry):
    """
    Implements application files
    """

    def __init__(self, filename):
        """
        @param	filename	Absolute path to a Desktop Entry File
        """
        if not os.path.isabs(filename):
            filename = os.path.join(os.getcwd(), filename)
        super(Application, self).__init__(filename)
        self._basename = os.path.basename(filename)
        if self.Type not in ("Application", "Service"):
            raise DesktopEntryTypeException(
                "'%s' is not of type 'Application'." % self.filename
            )

    @classmethod
    def _build_cmd(cls, exec_string, needs_terminal=False, term="x-terminal-emulator"):
        """
        # test single and multi argument commands
        >>> Application._build_cmd('gvim')
        ['gvim']
        >>> Application._build_cmd('gvim test')
        ['gvim', 'test']

        # test quotes
        >>> Application._build_cmd('"gvim" test')
        ['gvim', 'test']
        >>> Application._build_cmd('"gvim test"')
        ['gvim test']

        # test escape sequences
        # >>> Application._build_cmd('"gvim test" test2 "test \\\\" 3"')
        # ['gvim test', 'test2', 'test " 3']
        # >>> Application._build_cmd(r'"test \\\\\\\\ \\" moin" test')
        # ['test \\\\ " moin', 'test']
        # >>> Application._build_cmd(r'"gvim \\\\\\\\ \\`test\\$"')
        # ['gvim \\\\ \\`test\\$']
        >>> Application._build_cmd(r'vim ~/.vimrc', True)
        ['x-terminal-emulator', '-e', 'vim', '~/.vimrc']
        >>> Application._build_cmd('vim ~/.vimrc', False)
        ['vim', '~/.vimrc']
        >>> Application._build_cmd("vim '~/.vimrc test'", False)
        ['vim', '~/.vimrc test']
        >>> Application._build_cmd('vim \\'~/.vimrc " test\\'', False)
        ['vim', '~/.vimrc " test']
        >>> Application._build_cmd('sh -c \\'vim ~/.vimrc " test\\'', False)
        ['sh', '-c', 'vim ~/.vimrc " test']
        >>> Application._build_cmd("sh -c 'vim ~/.vimrc \\" test\\"'", False)
        ['sh', '-c', 'vim ~/.vimrc " test"']

        # expand field codes by removing them
        >>> Application._build_cmd("vim %u", False)
        ['vim']
        >>> Application._build_cmd("vim ~/.vimrc %u", False)
        ['vim', '~/.vimrc']
        >>> Application._build_cmd("vim '%u' ~/.vimrc", False)
        ['vim', '%u', '~/.vimrc']
        >>> Application._build_cmd("vim %u ~/.vimrc", False)
        ['vim', '~/.vimrc']
        >>> Application._build_cmd("vim /%u/.vimrc", False)
        ['vim', '//.vimrc']
        >>> Application._build_cmd("vim %u/.vimrc", False)
        ['vim', '/.vimrc']
        >>> Application._build_cmd("vim %U/.vimrc", False)
        ['vim', '/.vimrc']
        >>> Application._build_cmd("vim /%U/.vimrc", False)
        ['vim', '//.vimrc']
        >>> Application._build_cmd("vim %U .vimrc", False)
        ['vim', '.vimrc']

        # preserved escaped field codes
        >>> Application._build_cmd("vim \\\\%u ~/.vimrc", False)
        ['vim', '%u', '~/.vimrc']

        # test for non-valid field codes, they should be preserved
        >>> Application._build_cmd("vim %x .vimrc", False)
        ['vim', '%x', '.vimrc']
        >>> Application._build_cmd("vim %x/.vimrc", False)
        ['vim', '%x/.vimrc']
        """
        cmd = []
        if needs_terminal:
            cmd += [term, "-e"]
        _tmp = exec_string.replace("\\\\", "\\")
        _arg = ""
        in_esc = False
        in_quote = False
        in_singlequote = False
        in_fieldcode = False

        for c in _tmp:
            if in_esc:
                in_esc = False
            else:
                if in_fieldcode:
                    in_fieldcode = False
                    if c in ("u", "U", "f", "F"):
                        # TODO ignore field codes for the moment; at some point
                        # field codes should be supported
                        # strip %-char at the end of the argument
                        _arg = _arg[:-1]
                        continue

                if c == '"':
                    if in_quote:
                        in_quote = False
                        cmd.append(_arg)
                        _arg = ""
                        continue
                    if not in_singlequote:
                        in_quote = True
                        continue

                elif c == "'":
                    if in_singlequote:
                        in_singlequote = False
                        cmd.append(_arg)
                        _arg = ""
                        continue
                    if not in_quote:
                        in_singlequote = True
                        continue

                elif c == "\\":
                    if not in_quote:
                        in_esc = True
                        continue

                elif c == "%" and not (in_quote or in_singlequote):
                    in_fieldcode = True

                elif c == " " and not (in_quote or in_singlequote):
                    if not _arg:
                        continue
                    cmd.append(_arg)
                    _arg = ""
                    continue

            _arg += c

        if _arg and not (in_esc or in_quote or in_singlequote):
            cmd.append(_arg)
        elif _arg:
            raise ApplicationExecException(
                "Exec value contains an unbalanced number of quote characters."
            )

        return cmd

    def execute(self, action=None, term=None, wait=False, dryrun=False, verbose=False):
        """
        Execute application or, if given, a specific action
        @return	Return subprocess.Popen object
        """
        if self.TryExec:
            executable = self.TryExec
            if action:
                executable = Action(owner=self, identifier=action).Exec
            if not os.path.isabs(executable):
                executable = which(executable)
            if not os.access(executable, mode=os.F_OK | os.X_OK):
                if verbose:
                    print(
                        "Ignoring file, TryExec not found or not executable file: %s"
                        % executable,
                        file=sys.stderr,
                    )
                return

        path = self.Path
        cmd = self._build_cmd(
            exec_string=self.Exec, needs_terminal=self.Terminal, term=term
        )
        if not cmd:
            raise ApplicationExecException("Failed to build command string.")
        if dryrun or verbose:
            if verbose:
                print("Autostart file: %s" % self.filename)
            if path:
                print("Changing directory to: " + path)
            print("Executing command: " + " ".join(cmd))
        if dryrun:
            return

        _execute_fn = subprocess.Popen
        if wait:
            _execute_fn = subprocess.run
        if path:
            return _execute_fn(cmd, cwd=path, env=os.environ)
        return _execute_fn(cmd, env=os.environ)


class Action(object):
    def __init__(self, owner, identifier):
        """
        @param	owner	The Application that this action is part of
        @param	identifier	The Desktop Action's identifier (not its Name!)
        """
        self._owner = owner
        self._id = identifier

    def group_name(self):
        return "Desktop Action %s" % self._id

    @property
    def identifier(self):
        return self._id

    @property
    def Name(self):
        return self._owner.get_string("Name", group=self.group_name())

    @property
    def Exec(self):
        return self._owner.get_string("Exec", group=self.group_name())


# local methods
def which(filename):
    path = os.environ.get("PATH", None)
    if path:
        for _p in path.split(os.pathsep):
            _f = os.path.join(_p, filename)
            if os.path.isfile(_f):
                return _f


def get_autostart_directories(args):
    """
    Generate the list of autostart directories
    """
    if args.searchpaths:
        return [
            os.path.expandvars(os.path.expanduser(p))
            for p in args.searchpaths[0].split(os.pathsep)
        ]

    # generate list of autostart directories
    autostart_directories = []

    config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
    autostart_directories.append(os.path.join(config_home, "autostart"))

    config_dirs = os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg")
    for d in config_dirs.split(os.pathsep):
        if not d:
            continue
        autostart_dir = os.path.join(d, "autostart")
        if autostart_dir not in autostart_directories:
            autostart_directories.append(autostart_dir)

    return autostart_directories


def get_autostart_files(args, verbose=False):
    """
    Generate a list of autostart files according to autostart-spec 0.5

    TODO: do filetype recognition according to spec
    """
    seen_files = set()
    autostart_files = []  # autostart files, excluding files marked as hidden

    for d in get_autostart_directories(args):
        if os.path.exists(d) and os.path.isdir(d):
            for entry in scandir(d):
                if not entry.is_file() or not entry.name.endswith(".desktop"):
                    if verbose:
                        print("Ignoring non-file: %s" % entry.path, file=sys.stderr)
                    continue
                elif entry.name in seen_files:
                    if verbose:
                        print(
                            "Ignoring file, overridden by other autostart file: %s"
                            % entry.path,
                            file=sys.stderr,
                        )
                    continue

                seen_files.add(entry.name)
                try:
                    app = Application(entry.path)
                except DesktopEntryTypeException as ex:
                    continue
                except ValueError as ex:
                    if verbose:
                        print(ex, file=sys.stderr)
                    continue
                except IOError as ex:
                    if verbose:
                        print(ex, file=sys.stderr)
                    continue

                if verbose:
                    if app.NotShowIn:
                        print(
                            "Not show in environments %s: %s"
                            % (", ".join(app.NotShowIn), app.filename),
                            file=sys.stderr,
                        )
                    if app.OnlyShowIn:
                        print(
                            "Only show in environments %s: %s"
                            % (", ".join(app.OnlyShowIn), app.filename),
                            file=sys.stderr,
                        )

                if app.Hidden:
                    if verbose:
                        print(
                            "Ignoring file, hidden attribute is set: %s" % app.filename,
                            file=sys.stderr,
                        )
                    continue
                elif app.OnlyShowIn and not (
                    args.environment and args.environment in app.OnlyShowIn
                ):
                    if verbose:
                        print(
                            "Ignoring file, it must only start in specific environments (%s): %s"
                            % (", ".join(app.OnlyShowIn), app.filename),
                            file=sys.stderr,
                        )
                    continue
                elif (
                    app.NotShowIn
                    and args.environment
                    and args.environment in app.NotShowIn
                ):
                    if verbose:
                        print(
                            "Ignoring file, it must not start in specific environments (%s): %s"
                            % (", ".join(app.NotShowIn), app.filename),
                            file=sys.stderr,
                        )
                    continue

                autostart_files.append(app)

    return sorted(autostart_files)


def _test(args):
    """
    run tests
    """
    import doctest

    doctest.testmod()


def _autostart(args):
    """
    perform autostart
    """
    if args.dryrun and args.verbose:
        print("Dry run, nothing is executed.", file=sys.stderr)

    exit_value = 0
    for app in get_autostart_files(args, verbose=args.verbose):
        try:
            app.execute(term=args.term, dryrun=args.dryrun, verbose=args.verbose)
        except Exception as ex:
            exit_value = 1
            print(
                "Execution failed: %s%s%s" % (app.filename, os.linesep, ex),
                file=sys.stderr,
            )
    return exit_value


def _run(args):
    """
    execute specified DesktopEntry files, or specified action of each file
    """
    if args.dryrun and args.verbose:
        print("Dry run, nothing is executed.", file=sys.stderr)

    exit_value = 0
    if not args.files:
        print("Nothing to execute, no DesktopEntry files specified!", file=sys.stderr)
        parser.print_help()
        exit_value = 1
    else:
        for f in args.files:
            try:
                app = Application(f)
                app.execute(
                    action=args.action,
                    term=args.term,
                    wait=args.wait,
                    dryrun=args.dryrun,
                    verbose=args.verbose,
                )
            except ValueError as ex:
                print(ex, file=sys.stderr)
            except IOError as ex:
                print(ex, file=sys.stderr)
            except Exception as ex:
                exit_value = 1
                print("Execution failed: %s%s%s" % (f, os.linesep, ex), file=sys.stderr)
    return exit_value


def _create(args):
    """
    create a new DesktopEntry file from the given argument
    """
    target = args.create[0]
    if args.verbose:
        print("Creating DesktopEntry for file %s." % target)

    de = DesktopEntry.fromfile(target)
    if args.verbose:
        print("Type: %s" % de.Type)

    # determine output file
    output = ".".join(
        (os.path.basename(target), "directory" if de.Type == "Directory" else "desktop")
    )
    if args.targetdir:
        output = os.path.join(args.targetdir[0], output)
    elif len(args.create) > 1:
        output = args.create[1]

    if args.verbose:
        print("Output: %s" % output)

    try:
        targetfile = sys.stdout if output == "-" else open(output, "w")
    except FileNotFoundError:
        print("Target directory does not exist: %s" % os.path.dirname(output))
        return 1
    de.write(targetfile)

    if args.targetdir and len(args.create) > 1:
        args.create = args.create[1:]
        return _create(args)
    return 0


def _property(args):
    """
    Display DesktopEntry property value
    """
    exit_value = 0
    if not args.files:
        print("Nothing to parse, no DesktopEntry files specified!", file=sys.stderr)
        parser.print_help()
        exit_value = 1
    else:
        properties = (
            "Type",
            "Version",
            "Name",
            "NoDisplay",
            "Hidden",
            "OnlyShowIn",
            "NotShowIn",
            "TryExec",
            "Exec",
            "Path",
            "Terminal",
            "Actions",
            "StartupNotify",
            "StartupWMClass",
            "URL",
        )
        action_properties = ("Name", "Exec")
        property = args.property[0]
        for f in args.files:
            try:
                app = Application(f)

                allowed_properties = properties
                error_keyword = "Entry"
                if args.action:
                    app = Action(owner=app, identifier=args.action)
                    allowed_properties = action_properties
                    error_keyword = "Action"

                if property in allowed_properties:
                    print(getattr(app, property))
                else:
                    exit_value = 1
                    print(
                        "'%s' is not a valid Desktop %s property."
                        % (property, error_keyword),
                        file=sys.stderr,
                    )
            except ValueError as ex:
                print(ex, file=sys.stderr)
            except IOError as ex:
                print(ex, file=sys.stderr)
            except Exception as ex:
                exit_value = 1
                print("Parse failed: %s%s%s" % (f, os.linesep, ex), file=sys.stderr)
    return exit_value


# start execution
if __name__ == "__main__":
    from argparse import ArgumentParser

    parser = ArgumentParser(
        usage="%(prog)s [options] [DesktopEntryFile [DesktopEntryFile ...]]",
        description="dex, DesktopEntry Execution, is a program to generate and execute DesktopEntry files of the type Application",
        epilog="Example usage: list autostart programs: dex -ad",
    )
    parser.add_argument(
        "--action",
        dest="action",
        help='identifier of an "additional application action" to operate on. Also known as a quicklist/jumplist entry',
    )
    parser.add_argument(
        "--test", action="store_true", dest="test", help="perform a self-test"
    )
    parser.add_argument(
        "-v", "--verbose", action="store_true", dest="verbose", help="verbose output"
    )
    parser.add_argument(
        "-V", "--version", action="version", version="%%(prog)s %s" % __version__
    )
    parser.add_argument("files", nargs="*", help="DesktopEntry files")

    property = parser.add_argument_group("property")
    property.add_argument(
        "-p",
        "--property",
        nargs=1,
        dest="property",
        help="display DesktopEntry property value. Supported properties are: Type, Version, Name, NoDisplay, Hidden, OnlyShowIn, NotShowIn, TryExec, Exec, Path, Terminal, Actions, StartupNotify, StartupWMClass, URL. For ACTIONs, only Name and Exec are supported",
    )

    run = parser.add_argument_group("run")
    run.add_argument(
        "-a",
        "--autostart",
        action="store_true",
        dest="autostart",
        help="autostart programs",
    )
    run.add_argument(
        "-d",
        "--dry-run",
        action="store_true",
        dest="dryrun",
        help="dry run, don't execute any command",
    )
    run.add_argument(
        "-e",
        "--environment",
        dest="environment",
        help="specify the Desktop Environment an autostart should be performed for; works only in combination with --autostart",
        default=os.environ.get("XDG_CURRENT_DESKTOP"),
    )
    run.add_argument(
        "-s",
        "--search-paths",
        nargs=1,
        dest="searchpaths",
        help="colon separated list of paths to search for desktop files, overriding the default search list",
    )
    run.add_argument(
        "--term",
        dest="term",
        help="the terminal emulator that will be used to run the program if Terminal=true is set in the desktop file, defaults to x-terminal-emulator",
    )
    run.add_argument(
        "-w",
        "--wait",
        action="store_true",
        dest="wait",
        help="block until the program exits",
    )

    create = parser.add_argument_group("create")
    create.add_argument(
        "-c",
        "--create",
        nargs="+",
        dest="create",
        help="create a DesktopEntry file for the given program. If a second argument is provided it's taken as output filename or written to stdout (filename: -). By default a new file with the postfix .desktop is created",
    )
    create.add_argument(
        "-t",
        "--target-directory",
        nargs=1,
        dest="targetdir",
        help="create files in target directory",
    )

    parser.set_defaults(
        func=_run,
        term="x-terminal-emulator",
        wait=False,
        dryrun=False,
        test=False,
        autostart=False,
        verbose=False,
    )

    args = parser.parse_args()
    if args.autostart:
        args.func = _autostart
    elif args.create:
        args.func = _create
    elif args.test:
        args.func = _test
    elif args.property:
        args.func = _property

    sys.exit(args.func(args))
