build-support/makeDesktopItem: make fully declarative, add all missing options

This updates makeDesktopItem to explicitly support all the fields in the spec,
converts list-like fields to native Nix lists instead of semicolon-separated strings,
and allows automatically generating [Desktop Action] sections from Nix code
instead of hardcoding them as extraConfig strings.
This commit is contained in:
K900 2022-02-22 16:35:17 +03:00 committed by Jonathan Ringer
parent 5364c438e7
commit 0c713dbed4

View file

@ -1,67 +1,118 @@
{ lib, runCommandLocal, desktop-file-utils }:
{ lib, writeTextFile, desktop-file-utils }:
# 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/desktop-entry-spec-latest.html
{ name # The name of the desktop file
, type ? "Application"
, exec
, icon ? null
, comment ? null
, terminal ? false
# version is hardcoded
, desktopName # The name of the application
, genericName ? null
, mimeType ? null
, categories ? null
, startupNotify ? 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
, extraDesktopEntries ? { } # Extra key-value pairs to add to the [Desktop Entry] section. This may override other values
, extraEntries ? "" # Extra configuration. Will be appended to the end of the file and may thus contain extra sections
, fileValidation ? true # whether to validate resulting desktop file.
# 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
# like builtins.toString, but null -> null instead of null -> ""
nullableToString = value:
# 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 builtins.toString value;
else throw "Value must be a boolean or null!";
# The [Desktop entry] section of the desktop file, as attribute set.
# 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 = value:
if !builtins.isList value then throw "Value must be a list!"
else if builtins.any (item: lib.hasInfix ";" item) value then throw "Values in 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" = toString type;
"Exec" = nullableToString exec;
"Icon" = nullableToString icon;
"Comment" = nullableToString comment;
"Terminal" = nullableToString terminal;
"Name" = toString desktopName;
"GenericName" = nullableToString genericName;
"MimeType" = nullableToString mimeType;
"Categories" = nullableToString categories;
"StartupNotify" = nullableToString startupNotify;
"NoDisplay" = nullableToString noDisplay;
"PrefersNonDefaultGPU" = nullableToString prefersNonDefaultGPU;
} // extraDesktopEntries;
"Type" = type;
"Version" = "1.4";
"Name" = desktopName;
"GenericName" = genericName;
"NoDisplay" = boolOrNullToString noDisplay;
"Comment" = comment;
"Icon" = icon;
"OnlyShowIn" = renderList onlyShowIn;
"NotShowIn" = renderList notShowIn;
"DBusActivatable" = boolOrNullToString dbusActivatable;
"TryExec" = tryExec;
"Exec" = exec;
"Path" = path;
"Terminal" = boolOrNullToString terminal;
"Actions" = renderList (builtins.attrNames actions);
"MimeType" = renderList mimeTypes;
"Categories" = renderList categories;
"Implements" = renderList implements;
"Keywords" = renderList keywords;
"StartupNotify" = boolOrNullToString startupNotify;
"StartupWMClass" = startupWMClass;
"URL" = url;
"PrefersNonDefaultGPU" = boolOrNullToString prefersNonDefaultGPU;
# "SingleMainWindow" = boolOrNullToString singleMainWindow;
} // extraConfig;
# Map all entries to a list of lines
desktopFileStrings =
[ "[Desktop Entry]" ]
++ builtins.filter
(v: v != null)
(lib.mapAttrsToList
(name: value: if value != null then "${name}=${value}" else null)
mainSection
)
++ (if extraEntries == "" then [ ] else [ "${extraEntries}" ]);
# 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: !isNull v))
(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;
content = [ mainSectionRendered ] ++ actionsRendered;
in
runCommandLocal "${name}.desktop"
{
nativeBuildInputs = [ desktop-file-utils ];
writeTextFile {
name = "${name}.desktop";
destination = "/share/applications/${name}.desktop";
text = builtins.concatStringsSep "\n" content;
checkPhase = "${desktop-file-utils}/bin/desktop-file-validate $target";
}
(''
mkdir -p "$out/share/applications"
cat > "$out/share/applications/${name}.desktop" <<EOF
${builtins.concatStringsSep "\n" desktopFileStrings}
EOF
'' + lib.optionalString fileValidation ''
echo "Running desktop-file validation"
desktop-file-validate "$out/share/applications/${name}.desktop"
'')