diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index d8640934d..2b3a9a5d5 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -37,6 +37,11 @@
/modules/misc/xdg-system-dirs.nix @tadfisher
/tests/modules/misc/xdg/system-dirs.nix @tadfisher
+/modules/misc/xdg-desktop-entries.nix @cwyc
+/tests/modules/misc/xdg/desktop-entries.nix @cwyc
+/tests/modules/misc/xdg/desktop-full-expected.desktop @cwyc
+/tests/modules/misc/xdg/desktop-min-expected.desktop @cwyc
+
/modules/programs/aria2.nix @JustinLovinger
/modules/programs/autojump.nix @evanjs
diff --git a/modules/misc/xdg-desktop-entries.nix b/modules/misc/xdg-desktop-entries.nix
new file mode 100644
index 000000000..382d6c2b5
--- /dev/null
+++ b/modules/misc/xdg-desktop-entries.nix
@@ -0,0 +1,178 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ desktopEntry = {
+ options = {
+ # Since this module uses the nixpkgs/pkgs/build-support/make-desktopitem function,
+ # our options and defaults follow its parameters, with the following exceptions:
+
+ # `desktopName` on makeDesktopItem is controlled by `name`.
+ # This is what we'd commonly consider the name of the application.
+ # `name` on makeDesktopItem is controlled by this module's key in the attrset.
+ # This is the file's filename excluding ".desktop".
+
+ # `extraEntries` on makeDesktopItem is controlled by `extraConfig`,
+ # and `extraDesktopEntries` by `settings`,
+ # to match what's commonly used by other home manager modules.
+
+ # `startupNotify` on makeDesktopItem asks for "true" or "false" strings,
+ # for usability's sake we ask for a boolean.
+
+ # `mimeType` and `categories` on makeDesktopItem ask for a string in the format "one;two;three;",
+ # for the same reason we ask for a list of strings.
+
+ # Descriptions are taken from the desktop entry spec:
+ # https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#recognized-keys
+
+ type = mkOption {
+ description = "The type of the desktop entry.";
+ default = "Application";
+ type = types.enum [ "Application" "Link" "Directory" ];
+ };
+
+ exec = mkOption {
+ description = "Program to execute, possibly with arguments.";
+ type = types.str;
+ };
+
+ icon = mkOption {
+ description = "Icon to display in file manager, menus, etc.";
+ type = types.nullOr types.str;
+ default = null;
+ };
+
+ comment = mkOption {
+ description = "Tooltip for the entry.";
+ type = types.nullOr types.str;
+ default = null;
+ };
+
+ terminal = mkOption {
+ description = "Whether the program runs in a terminal window.";
+ type = types.bool;
+ default = false;
+ };
+
+ name = mkOption {
+ description = "Specific name of the application.";
+ type = types.str;
+ };
+
+ genericName = mkOption {
+ description = "Generic name of the application.";
+ type = types.nullOr types.str;
+ default = null;
+ };
+
+ mimeType = mkOption {
+ description = "The MIME type(s) supported by this application.";
+ type = types.nullOr (types.listOf types.str);
+ default = null;
+ };
+
+ categories = mkOption {
+ description =
+ "Categories in which the entry should be shown in a menu.";
+ type = types.nullOr (types.listOf types.str);
+ default = null;
+ };
+
+ startupNotify = mkOption {
+ description = ''
+ If true, it is KNOWN that the application will send a "remove"
+ message when started with the DESKTOP_STARTUP_ID
+ environment variable set. If false, it is KNOWN that the application
+ does not work with startup notification at all.'';
+ type = types.nullOr types.bool;
+ default = null;
+ };
+
+ extraConfig = mkOption {
+ description = ''
+ Extra configuration. Will be appended to the end of the file and
+ may thus contain extra sections.
+ '';
+ type = types.lines;
+ default = "";
+ };
+
+ settings = mkOption {
+ type = types.attrsOf types.string;
+ description = ''
+ Extra key-value pairs to add to the [Desktop Entry] section.
+ This may override other values.
+ '';
+ default = { };
+ example = literalExample ''
+ {
+ Keywords = "calc;math";
+ DBusActivatable = "false";
+ }
+ '';
+ };
+
+ fileValidation = mkOption {
+ type = types.bool;
+ description = "Whether to validate the generated desktop file.";
+ default = true;
+ };
+ };
+ };
+
+ #formatting helpers
+ ifNotNull = a: a': if a == null then null else a';
+ stringBool = bool: if bool then "true" else "false";
+ semicolonList = list:
+ (concatStringsSep ";" list) + ";"; # requires trailing semicolon
+
+ #passes config options to makeDesktopItem in expected format
+ makeFile = name: config:
+ pkgs.makeDesktopItem {
+ name = name;
+ type = config.type;
+ exec = config.exec;
+ icon = config.icon;
+ comment = config.comment;
+ terminal = config.terminal;
+ desktopName = config.name;
+ genericName = config.genericName;
+ mimeType = ifNotNull config.mimeType (semicolonList config.mimeType);
+ categories =
+ ifNotNull config.categories (semicolonList config.categories);
+ startupNotify =
+ ifNotNull config.startupNotify (stringBool config.startupNotify);
+ extraEntries = config.extraConfig;
+ extraDesktopEntries = config.settings;
+ };
+in {
+ meta.maintainers = with maintainers; [ cwyc ];
+
+ options.xdg.desktopEntries = mkOption {
+ description = ''
+ Desktop Entries allow applications to be shown in your desktop environment's app launcher.
+ You can define entries for programs without entries or override existing entries.
+ See for more information on options.
+ '';
+ default = { };
+ type = types.attrsOf (types.submodule desktopEntry);
+ example = literalExample ''
+ {
+ firefox = {
+ name = "Firefox";
+ genericName = "Web Browser";
+ exec = "firefox %U";
+ terminal = false;
+ categories = [ "Application" "Network" "WebBrowser" ];
+ mimeType = [ "text/html" "text/xml" ];
+ };
+ }
+ '';
+ };
+
+ config.home.packages = mkIf (config.xdg.desktopEntries != { })
+ (map hiPrio # we need hiPrio to override existing entries
+ (attrsets.mapAttrsToList makeFile config.xdg.desktopEntries));
+
+}
diff --git a/modules/modules.nix b/modules/modules.nix
index e7f4c1b63..c0dd457c7 100644
--- a/modules/modules.nix
+++ b/modules/modules.nix
@@ -41,6 +41,7 @@ let
(loadModule ./misc/version.nix { })
(loadModule ./misc/vte.nix { })
(loadModule ./misc/xdg-system-dirs.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./misc/xdg-desktop-entries.nix { condition = hostPlatform.isLinux; })
(loadModule ./misc/xdg-mime.nix { condition = hostPlatform.isLinux; })
(loadModule ./misc/xdg-mime-apps.nix { condition = hostPlatform.isLinux; })
(loadModule ./misc/xdg-user-dirs.nix { condition = hostPlatform.isLinux; })
diff --git a/tests/modules/misc/xdg/default.nix b/tests/modules/misc/xdg/default.nix
index 45610154c..b637cd1bf 100644
--- a/tests/modules/misc/xdg/default.nix
+++ b/tests/modules/misc/xdg/default.nix
@@ -2,4 +2,5 @@
xdg-mime-apps-basics = ./mime-apps-basics.nix;
xdg-file-attr-names = ./file-attr-names.nix;
xdg-system-dirs = ./system-dirs.nix;
+ xdg-desktop-entries = ./desktop-entries.nix;
}
diff --git a/tests/modules/misc/xdg/desktop-entries.nix b/tests/modules/misc/xdg/desktop-entries.nix
new file mode 100644
index 000000000..098aa7eee
--- /dev/null
+++ b/tests/modules/misc/xdg/desktop-entries.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ xdg.desktopEntries = {
+ full = { # full definition
+ type = "Application";
+ exec = "test --option";
+ icon = "test";
+ comment = "My Application";
+ terminal = true;
+ name = "Test";
+ genericName = "Web Browser";
+ mimeType = [ "text/html" "text/xml" ];
+ categories = [ "Network" "WebBrowser" ];
+ startupNotify = false;
+ extraConfig = ''
+ [X-ExtraSection]
+ Exec=foo -o
+ '';
+ settings = {
+ Keywords = "calc;math";
+ DBusActivatable = "false";
+ };
+ fileValidation = true;
+ };
+ min = { # minimal definition
+ exec = "test --option";
+ name = "Test";
+ };
+ };
+
+ #testing that preexisting entries in the store are overridden
+ home.packages = [
+ (pkgs.makeDesktopItem {
+ name = "full";
+ desktopName = "We don't want this";
+ exec = "no";
+ })
+ (pkgs.makeDesktopItem {
+ name = "min";
+ desktopName = "We don't want this";
+ exec = "no";
+ })
+ ];
+
+ nmt.script = ''
+ assertFileExists home-path/share/applications/full.desktop
+ assertFileExists home-path/share/applications/min.desktop
+ assertFileContent home-path/share/applications/full.desktop \
+ ${./desktop-full-expected.desktop}
+ assertFileContent home-path/share/applications/min.desktop \
+ ${./desktop-min-expected.desktop}
+ '';
+ };
+}
diff --git a/tests/modules/misc/xdg/desktop-full-expected.desktop b/tests/modules/misc/xdg/desktop-full-expected.desktop
new file mode 100644
index 000000000..fd5ace1b9
--- /dev/null
+++ b/tests/modules/misc/xdg/desktop-full-expected.desktop
@@ -0,0 +1,16 @@
+[Desktop Entry]
+Categories=Network;WebBrowser;
+Comment=My Application
+DBusActivatable=false
+Exec=test --option
+GenericName=Web Browser
+Icon=test
+Keywords=calc;math
+MimeType=text/html;text/xml;
+Name=Test
+StartupNotify=false
+Terminal=true
+Type=Application
+[X-ExtraSection]
+Exec=foo -o
+
diff --git a/tests/modules/misc/xdg/desktop-min-expected.desktop b/tests/modules/misc/xdg/desktop-min-expected.desktop
new file mode 100644
index 000000000..9475024ae
--- /dev/null
+++ b/tests/modules/misc/xdg/desktop-min-expected.desktop
@@ -0,0 +1,5 @@
+[Desktop Entry]
+Exec=test --option
+Name=Test
+Terminal=false
+Type=Application