diff --git a/pkgs/build-support/make-desktopitem/default.nix b/pkgs/build-support/make-desktopitem/default.nix index 10caac3a935a..6800bed3faea 100644 --- a/pkgs/build-support/make-desktopitem/default.nix +++ b/pkgs/build-support/make-desktopitem/default.nix @@ -1,119 +1,207 @@ -{ lib, writeTextFile, buildPackages }: - -# All possible values as defined by the spec, version 1.4. -# Please keep in spec order for easier maintenance. -# When adding a new value, don't forget to update the Version field below! -# See https://specifications.freedesktop.org/desktop-entry-spec/latest -lib.makeOverridable ({ name # The name of the desktop file -, type ? "Application" -# version is hardcoded -, desktopName # The name of the application -, genericName ? null -, noDisplay ? null -, comment ? null -, icon ? null -# we don't support the Hidden key - if you don't need something, just don't install it -, onlyShowIn ? [] -, notShowIn ? [] -, dbusActivatable ? null -, tryExec ? null -, exec ? null -, path ? null -, terminal ? null -, actions ? {} # An attrset of [internal name] -> { name, exec?, icon? } -, mimeTypes ? [] # The spec uses "MimeType" as singular, use plural here to signify list-ness -, categories ? [] -, implements ? [] -, keywords ? [] -, startupNotify ? null -, startupWMClass ? null -, url ? null -, prefersNonDefaultGPU ? null -# not supported until version 1.5, which is not supported by our desktop-file-utils as of 2022-02-23 -# , singleMainWindow ? null -, extraConfig ? {} # Additional values to be added literally to the final item, e.g. vendor extensions +{ + lib, + writeTextFile, + buildPackages, }: -let - # There are multiple places in the FDO spec that make "boolean" values actually tristate, - # e.g. StartupNotify, where "unset" is literally defined as "do something reasonable". - # So, handle null values separately. - boolOrNullToString = value: - if value == null then null - else if builtins.isBool value then lib.boolToString value - else throw "makeDesktopItem: value must be a boolean or null!"; - # Multiple values are represented as one string, joined by semicolons. - # Technically, it's possible to escape semicolons in values with \;, but this is currently not implemented. - renderList = key: value: - if !builtins.isList value then throw "makeDesktopItem: value for ${key} must be a list!" - else if builtins.any (item: lib.hasInfix ";" item) value then throw "makeDesktopItem: values in ${key} list must not contain semicolons!" - else if value == [] then null - else builtins.concatStringsSep ";" value; - # The [Desktop Entry] section of the desktop file, as an attribute set. - # Please keep in spec order. - mainSection = { - "Type" = type; - "Version" = "1.4"; - "Name" = desktopName; - "GenericName" = genericName; - "NoDisplay" = boolOrNullToString noDisplay; - "Comment" = comment; - "Icon" = icon; - "OnlyShowIn" = renderList "onlyShowIn" onlyShowIn; - "NotShowIn" = renderList "notShowIn" notShowIn; - "DBusActivatable" = boolOrNullToString dbusActivatable; - "TryExec" = tryExec; - "Exec" = exec; - "Path" = path; - "Terminal" = boolOrNullToString terminal; - "Actions" = renderList "actions" (builtins.attrNames actions); - "MimeType" = renderList "mimeTypes" mimeTypes; - "Categories" = renderList "categories" categories; - "Implements" = renderList "implements" implements; - "Keywords" = renderList "keywords" keywords; - "StartupNotify" = boolOrNullToString startupNotify; - "StartupWMClass" = startupWMClass; - "URL" = url; - "PrefersNonDefaultGPU" = boolOrNullToString prefersNonDefaultGPU; - # "SingleMainWindow" = boolOrNullToString singleMainWindow; - } // extraConfig; +/** + A utility builder to create a desktop entry file at a predetermined location (by default, $out/share/applications). - # Render a single attribute pair to a Key=Value line. - # FIXME: this isn't entirely correct for arbitrary strings, as some characters - # need to be escaped. There are currently none in nixpkgs though, so this is OK. - renderLine = name: value: if value != null then "${name}=${value}" else null; + # Examples - # Render a full section of the file from an attrset. - # Null values are intentionally left out. - renderSection = sectionName: attrs: - lib.pipe attrs [ - (lib.mapAttrsToList renderLine) - (builtins.filter (v: v != null)) - (builtins.concatStringsSep "\n") - (section: '' - [${sectionName}] - ${section} - '') - ]; + ```nix + makeDesktopItem { + name = "Eclipse"; + exec = "eclipse"; + icon = "eclipse"; + comment = "Integrated Development Environment"; + desktopName = "Eclipse IDE"; + genericName = "Integrated Development Environment"; + categories = [ "Development" ]; + } + => «derivation /nix/store/sh017x5n50i2qwns2na1sxdp7zb2zgcw-Eclipse.desktop.drv» + ``` - mainSectionRendered = renderSection "Desktop Entry" mainSection; + # Type - # Convert from javaCase names as used in Nix to PascalCase as used in the spec. - preprocessAction = { name, icon ? null, exec ? null }: { - "Name" = name; - "Icon" = icon; - "Exec" = exec; - }; - renderAction = name: attrs: renderSection "Desktop Action ${name}" (preprocessAction attrs); - actionsRendered = lib.mapAttrsToList renderAction actions; + ``` + makeDesktopItem :: AttrSet -> Derivation + ``` - extension = if type == "Directory" then "directory" else "desktop"; - content = [ mainSectionRendered ] ++ actionsRendered; -in -writeTextFile { - name = "${name}.${extension}"; - destination = "/share/applications/${name}.${extension}"; - text = builtins.concatStringsSep "\n" content; - checkPhase = ''${buildPackages.desktop-file-utils}/bin/desktop-file-validate "$target"''; -}) + # Input + + `attrs` + + : An AttrSet with the following definitions. See https://specifications.freedesktop.org/desktop-entry-spec/1.4/recognized-keys.html#id-1.7.6 for definitions. + + - `name` (string): The name of the desktop file (excluding the .desktop or .directory file extensions) + - `destination` (string): The directory that will contain the desktop entry file (Default: "/share/applications") + - `type` ("Application" | "Link" | "Directory"): The `Type` of the desktop entry + - `desktopName` (string): The `Name` of the desktop entry + - `genericName` (string): The `GenericName` of the desktop entry + - `noDisplay` (bool): The `NoDisplay` of the desktop entry + - `comment` (string): The `Comment` of the desktop entry + - `icon` (string): The `Icon` of the desktop entry + - `onlyShowIn` (string[]): The `OnlyShowIn` of the desktop entry + - `notShowIn` (string[]): The `NotShowIn` of the desktop entry + - `dbusActivatable` (bool): The `DBusActivatable` of the desktop entry + - `tryExec` (string): The `TryExec` of the desktop entry + - `exec` (string): The `Exec` of the desktop entry + - `path` (string): The `Path` of the desktop entry + - `terminal` (bool): The `Terminal` of the desktop entry + - `actions` (AttrSet): An attrset of [internal name] -> { name, exec?, icon? } representing the `Actions` of the desktop entry + - `mimeTypes` (string[]): The `MimeType` of the desktop entry + - `categories` (string[]): The `Categories` of the desktop entry; see https://specifications.freedesktop.org/menu-spec/1.0/category-registry.html for possible values + - `implements` (string[]): The `Implements` of the desktop entry + - `keywords` (string[]): The `Keywords` of the desktop entry + - `startupNotify` (bool): The `StartupNotify` of the desktop entry + - `startupWMClass` (string): The `StartupWMClass` of the desktop entry + - `url` (string): The `URL` of the Link-type desktop entry + - `prefersNonDefaultGPU` (bool): The `PrefersNonDefaultGPU` (non-standard) of the desktop entry + - `extraConfig` (AttrSet): Additional values to be added literally to the final item, e.g. vendor extensions + + # Output + + A derivation that contains the output desktop entry file. + + # Developer Note + + All possible values are as defined by the spec, version 1.4. + Please keep in spec order for easier maintenance. + When adding a new value, don't forget to update the Version field below! + See https://specifications.freedesktop.org/desktop-entry-spec/latest +*/ +lib.makeOverridable ( + { + name, # The name of the desktop file + destination ? "/share/applications", + type ? "Application", + # version is hardcoded + desktopName, # The name of the application + genericName ? null, + noDisplay ? null, + comment ? null, + icon ? null, + # we don't support the Hidden key - if you don't need something, just don't install it + onlyShowIn ? [ ], + notShowIn ? [ ], + dbusActivatable ? null, + tryExec ? null, + exec ? null, + path ? null, + terminal ? null, + actions ? { }, # An attrset of [internal name] -> { name, exec?, icon? } + mimeTypes ? [ ], # The spec uses "MimeType" as singular, use plural here to signify list-ness + categories ? [ ], + implements ? [ ], + keywords ? [ ], + startupNotify ? null, + startupWMClass ? null, + url ? null, + prefersNonDefaultGPU ? null, + # not supported until version 1.5, which is not supported by our desktop-file-utils as of 2022-02-23 + # singleMainWindow ? null, + extraConfig ? { }, # Additional values to be added literally to the final item, e.g. vendor extensions + }: + let + # There are multiple places in the FDO spec that make "boolean" values actually tristate, + # e.g. StartupNotify, where "unset" is literally defined as "do something reasonable". + # So, handle null values separately. + boolOrNullToString = + value: + if value == null then + null + else if builtins.isBool value then + lib.boolToString value + else + throw "makeDesktopItem: value must be a boolean or null!"; + + # Multiple values are represented as one string, joined by semicolons. + # Technically, it's possible to escape semicolons in values with \;, but this is currently not implemented. + renderList = + key: value: + if !builtins.isList value then + throw "makeDesktopItem: value for ${key} must be a list!" + else if builtins.any (item: lib.hasInfix ";" item) value then + throw "makeDesktopItem: values in ${key} list must not contain semicolons!" + else if value == [ ] then + null + else + builtins.concatStringsSep ";" value; + + # The [Desktop Entry] section of the desktop file, as an attribute set. + # Please keep in spec order. + mainSection = { + "Type" = type; + "Version" = "1.4"; + "Name" = desktopName; + "GenericName" = genericName; + "NoDisplay" = boolOrNullToString noDisplay; + "Comment" = comment; + "Icon" = icon; + "OnlyShowIn" = renderList "onlyShowIn" onlyShowIn; + "NotShowIn" = renderList "notShowIn" notShowIn; + "DBusActivatable" = boolOrNullToString dbusActivatable; + "TryExec" = tryExec; + "Exec" = exec; + "Path" = path; + "Terminal" = boolOrNullToString terminal; + "Actions" = renderList "actions" (builtins.attrNames actions); + "MimeType" = renderList "mimeTypes" mimeTypes; + "Categories" = renderList "categories" categories; + "Implements" = renderList "implements" implements; + "Keywords" = renderList "keywords" keywords; + "StartupNotify" = boolOrNullToString startupNotify; + "StartupWMClass" = startupWMClass; + "URL" = url; + "PrefersNonDefaultGPU" = boolOrNullToString prefersNonDefaultGPU; + # "SingleMainWindow" = boolOrNullToString singleMainWindow; + } // extraConfig; + + # Render a single attribute pair to a Key=Value line. + # FIXME: this isn't entirely correct for arbitrary strings, as some characters + # need to be escaped. There are currently none in nixpkgs though, so this is OK. + renderLine = name: value: if value != null then "${name}=${value}" else null; + + # Render a full section of the file from an attrset. + # Null values are intentionally left out. + renderSection = + sectionName: attrs: + lib.pipe attrs [ + (lib.mapAttrsToList renderLine) + (builtins.filter (v: v != null)) + (builtins.concatStringsSep "\n") + (section: '' + [${sectionName}] + ${section} + '') + ]; + + mainSectionRendered = renderSection "Desktop Entry" mainSection; + + # Convert from javaCase names as used in Nix to PascalCase as used in the spec. + preprocessAction = + { + name, + icon ? null, + exec ? null, + }: + { + "Name" = name; + "Icon" = icon; + "Exec" = exec; + }; + renderAction = name: attrs: renderSection "Desktop Action ${name}" (preprocessAction attrs); + actionsRendered = lib.mapAttrsToList renderAction actions; + + extension = if type == "Directory" then "directory" else "desktop"; + content = [ mainSectionRendered ] ++ actionsRendered; + in + writeTextFile { + name = "${name}.${extension}"; + destination = "${destination}/${name}.${extension}"; + text = builtins.concatStringsSep "\n" content; + checkPhase = ''${buildPackages.desktop-file-utils}/bin/desktop-file-validate "$target"''; + } +)