One of the most confusing aspects of packaging applications on Linux continues to be icon themes. Despite the simplicity of the original concept, getting your application’s icon to display correctly in a broad range of desktop environments continues to challenge even the best of programmers.
Before we begin taking a look at how this is correctly implemented, it would be wise to consult the documentation. freedesktop.org provides us with the Icon Theme Specification which describes how the icons should be organized on the filesystem. Let’s take a look at how an icon “name” is resolved to an absolute path on the filesystem.
According to the specification, three “base” directories are searched for icons:
The first of these exists for backwards compatibility and can safely be ignored.
$XDG_DATA_DIRS consists of a colon-separated list of directories. For example, on a clean installation of Wily (Ubuntu 15.10), this is set to:
Each icon theme exists as a subdirectory within the base directories. If an icon does not exist within the current theme, the default theme (named “hicolor”) is searched as a last resort. Each theme directory provides a text file named
index.theme that describes where the individual icons can be found within the theme. For example, the hicolor theme’s
index.theme looks something like this:
[Icon Theme] Name=Hicolor Comment=Fallback icon theme Hidden=true Directories=16x16/actions,... [16x16/actions] Size=16 Context=Actions Type=Threshold
Each of these fields is described in more detail on this page. The excerpt above indicates that 16x16 icons for the “actions” context (category) are located in the
It is worth noting that although hicolor organizes its directories in the format
[size]/[context], some other themes use the format
[context]/[size]. Don’t rely on a certain directory hierarchy.
Within the directories specified in
index.theme are the individual icon files. All implementations support PNG and XPM images, although XPM is deprecated and should no longer be used. Support for SVG images is optional and implementations that do not support them simply ignore them.
In order to help understand how this all fits together, I created a set of icons for an imaginary application which I named “IconPro”. I began by creating an empty binary in
sudo touch /usr/local/bin/iconpro sudo chmod 755 /usr/local/bin/iconpro
I also created a desktop file (
[Desktop Entry] Name=IconPro Exec=/usr/local/bin/iconpro Icon=iconpro Type=Application Categories=Graphics;RasterGraphics;
The following command needed to be run in order to register the
sudo desktop-file-install iconpro.desktop
Since I didn’t have any icons installed yet, most desktop environments simply displayed a generic icon in their respective menus / launchers. The following was displayed by Unity 7, Gnome, and KDE respectively:
Copying a file named
/usr/share/pixmaps caused the icon to appear correctly in the menus and launchers of most desktop environments. This is by far the most effective way to ensure that an icon is displayed. Beyond this, things start to get weird very quickly.
Copying a 48x48 image to
/usr/share/icons/hicolor/48x48/apps should in theory cause desktop environments to display it in place of the one in
/usr/share/pixmaps but in practice, this didn’t always happen. Unity and Gnome implement the expected behavior (
update-icon-caches can be used to avoid restarting the desktop environment). KDE continued to display the icon from
By default Gnome uses the Adwaita theme. Putting an icon in
/usr/share/icons/Adwaita/48x48/apps does indeed override the icon in hicolor. This works in Unity as well with the
ubuntu-mono-dark theme. Both desktop environments seem to prefer the highest resolution icon when both small and larger ones are available.
For the last part of this article, I decided to do a bit of digging into GTK internals. I compiled this simple C application, ran it through
strace, and searched for a non-existent icon at 48x48. (The default theme was set to
The following directories were searched in order, where
[THEME] was set to
hicolor, and finally left empty: