Skip to content

Commit

Permalink
build-support/makeDesktopItem: make fully declarative, add all missin…
Browse files Browse the repository at this point in the history
…g 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.
  • Loading branch information
K900 authored and Jonathan Ringer committed Feb 25, 2022
1 parent 5364c43 commit 0c713db
Showing 1 changed file with 101 additions and 50 deletions.
151 changes: 101 additions & 50 deletions pkgs/build-support/make-desktopitem/default.nix
Original file line number Diff line number Diff line change
@@ -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;

# 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;

# 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}" ]);
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"
'')

1 comment on commit 0c713db

@voobscout
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was the version hardcoded? it breaks stuff on mate, also it's up to the user to choose the version format.

Please sign in to comment.