Originally posted on indeliblebluepen.com.
In my free time (yes, I actually managed to scrape some together!), I've started work on a project I've been planning for quite some time - building the music library application of my dreams! I picked up my favorite language, Python, and dove right in.
As to the GUI, I recently swore off GTK in all forms, after a particularly aggravating incident with my company's Infiltrator game project. One of my IRC friends pointed me to Kivy, a modern GUI library for Python, and I immediately fell in love.
The challenge is, Kivy still has some rough edges which, while a potential source of frustration, also means lots of opportunities for adventure!
This is a great opportunity for me to hone my skills, and I'll be sharing various discoveries, tricks, and stories from the Elements project here on Indelible Blue Pen. You can also follow the Elements project on Github.
I'd wager that, for many developers, adding an application icon makes your project feel more “real”. You certainly can't ship without it!
I like keeping my icons in the repository, in a directory relative to my Python files, for easy access. Basically, my repository structure looks like this…
Elements
└── elements
├── (My Python files)
├── icons
│ ├── app
│ │ └── (Application icons)
│ └── ui
│ └── (Other interface icons)
└── tests
└── (My tests.)
I created my icon in SVG, and have .png images in just about every common icon size, from 512×512 to 16×16. I dropped the whole lot of them in my “elements/icons/app” path.
Most GUI toolkits make it fairly easy to add an icon. For example, Kivy only requires you to set self.icon
in your build(self)
function...
class ElementsApp(App):
"""
Application-level class, builds the application
"""
def build(self):
self.title = "Elements"
self.icon = "icons/app/elements_icon_512.png"
elements_app = ElementsWindow()
return elements_app
Easy enough, right? Sure! On most operating systems, even Windows, those two little lines of code set the application name and icon, and you're good to go.
Not so with Ubuntu Unity! No matter what I did, the icon would not show up in the Unity Launcher when I started the application.
After a frustrating while hashing out the problem in the #kivy Freenode IRC room, I began researching how other GUI toolkits did it, and I quickly found that Qt has the same problem! Hmm.
I remembered that I had been able to set an icon for Redstring, another Python application project I had worked on. Redstring had been built with the Python bindings for GTK, the GUI of choice for Ubuntu Unity. Perhaps they had access to some special API?
Turns out, yes…and no! While GTK allowed setting an icon, many developers complained that it was “blurry”.
After a bit of digging, I found that the only reliable way of setting an application icon (or, for that matter, an application name) was by creating a .desktop file. Problem solved, right?
“Absolute” Chaos
The problem with .desktop files is that they require absolute paths to the executable and the icon, and none of the bash variables work. My project was running from a local clone of a repository in my home folder, which meant that I would have to start those paths with “/home/jason/…”, which is hardly portable. I'd be the only person able to test out Elements with the icon, and I just wasn't okay with that.
Now, .desktop files work because programs and their resources are stored in common places, such as “/usr/bin” and “/usr/share/icons”. I really didn't want to install stuff to those sacred system paths just for a demo, though – that's just asking for trouble. Secondarily, having to use root privileges to run a demo felt wrong.
But, what if I were to copy the application and its resources to some system path that didn't require root privileges? Since the demo install was, at minimum, a once-off execution, the “/tmp” directory fit the bill perfectly. You can dump whatever cruft you want into “/tmp”, and your computer won't care – it will just send its digital janitors to clear that directory out when you restart the system.
By installing to “/tmp”, I would have an absolute path that would work on any Linux system!
Now I needed a place to put the .desktop file. The system looks for those files in two places: “/usr/share/applications” for system-wide applications, and “~/.local/share/applications” for applications only available to the current user. Since only the current user would care about the demo, and I didn't want to use root privileges, the second option was the obvious choice.
I typed up my .desktop file, which I would keep in my repository and copy it to where I needed it later.
[Desktop Entry]
Version=1.0
Name=Elements (Demo)
Comment=A demo version of Elements, installed to /tmp.
Exec=python3 /tmp/bin/elements/elements.py
Icon=/tmp/bin/elements/icons/app/elements_icon_512.png
Terminal=false
Type=Application
I doubt this will be the last file I need for deploying my application in some form or another, so I created a handy little “deploy/” folder in my repository, and put “elements_tmp.desktop” in it.
Now I just needed a means of automatically performing all this magic, so I turned to my go-to Linux magician: the Makefile.
Make Magic
Even in Python projects, I like having a Makefile handy. It is infinitely easier to run “make run” than to run “python3 elements/elements.py”.
Thus, to deploy a test version of my application, I simply needed to add a couple of targets to my Makefile.
demo_deploy: demo_clean
@mkdir -p /tmp/bin
@cp -rf elements /tmp/bin/elements
@cp -f deploy/elements_tmp.desktop ~/.local/share/applications/
@gtk-launch elements_tmp
@rm -f ~/.local/share/applications/elements_tmp.desktop
demo_clean:
@rm -rf /tmp/bin/elements
@rm -f ~/.local/share/applications/elements_tmp.desktop
.PHONY: demo_clean demo_deploy
Let's break that down. Note that the “demo_deploy” target calls the “demo_clean” target, which I'll cover shortly.
To deploy, first we need to create a new folder in “/tmp” to store our demo. Because I plan to reuse this method, I figure having a common directory for all temporary binaries would be helpful, so I'll call it “bin/”.
By passing the -p
flag to mkdir
, I ensure that the target doesn't fail if the directory already exists. Technically, that flag will also create the parent directories if they don't exist, but since “/tmp” already exists, that's a moot point.
Second, we copy the directory containing our application and its resources from our repository, where the Makefile is running from, to that “/tmp/bin” path. Obviously, we need -r
to copy the directory, and -f
to “force” the copy (since we don't want this failing if it doesn't have to).
Third, we copy our “elements_tmp.desktop” to the directory where the system is going to look for it.
Fourth, we don't want to have to wait for the Unity Dash to refresh itself to start our demo, so we'll just launch it from the command line using that .desktop file. (This command is based on this answer on AskUbuntu.)
Finally, since my deployment in “/tmp” will get deleted as soon as the computer is restarted, I really don't want any chance of a broken .desktop file floating around. Since the system already got what it needed from the .desktop file when I launched, we actually don't need it anymore, so I delete it here.
That cleanup target is pretty simple: delete the temporary deployment AND the .desktop file (if it happened to be around).
Some may argue that last step is pointless, since I always delete the .desktop file at the end of the deployment target, but I've worked around computers long enough to know that “can't happen” does happen when you least expect it. All in all, it doesn't hurt to have that command there.
Finally, I just run make demo_deploy and...
In Summary
In general, this should work on any Linux operating system that uses .desktop files. However, you'll want to double check that “gtk-launch” is installed and working, as that program is absent from some older versions of Ubuntu.
The cool thing about this approach is that it'll work for just about any sort of project, Python or otherwise. You can adapt the Makefile to copy whatever directories you need for the demo deployment, and tweak the .desktop file's “Exec” property to correctly start your application.
Has this helped you? Do you have any advice for using this approach with other sorts of projects? Leave a comment!