I still remember my first real software project. Back in 2011, when I first learned how to code in Visual Basic .NET, I created a little application for keeping track of how much time I spent on various tasks.
It wasn't the most beautiful interface — I was enjoying the power to create dialog boxes a liiiiiiiitle too much — but it worked well, and actually had no major bugs. I loved it!
But time wore on, and the .NET dependencies became outdated. I moved to Linux, and eventually lost the source code and the official installer under circumstances I still cannot recall.
I tried for years to find software that was as smooth and simple as my beloved Timecard. The closest was Project Hamster, which worked fairly well for me until developer interest in the project dried up.
Finally, this month, I came to a realization: "Hey, I'm writing this Dead Simple Python book, and yet I have yet to release a full-blown application in Python!"
So I sat down, fired up Visual Studio Code, and got to work.
Rewriting Timecard from scratch meant literally reimagining the program. I had nothing to work from: no screenshots, no running copy of v1.0, no previous source code.
I've been getting pretty deep into Qt 5 via PySide2, so that was my obvious choice for GUI, although the main deciding factor was that I knew it was possible to package Qt 5 apps. I spent the better part of 2018 trying and failing to package Omission, my still-unreleased puzzle game, thanks to some weirdness with Kivy and its dependencies. (I'm now rewriting that game in PySide2.)
Since those early days, I've fallen out of love with dialog boxes, but they're still such a common desktop UX metaphor! I planned to use only one or two to confirm critical actions, like discarding a time log entry.
But less than an hour into coding, I slam into QTBUG-56893. I briefly considered putting the project on hold until the bug was fixed, but my impatience won out.
"What could I use instead of a dialog box?" I asked myself. After a moment, I decided that a responsive, single-window interface would solve the problem, and would be a breath of fresh air! Clicking a "scary button" like "Stop" or "Reset" would cause the two buttons to change in a very distinct manner, prompting the user to confirm or cancel the action.
Once I'd decided on an adaptive interface, the rest of my design decisions fell into place pretty quickly. The timer and its controls were the most important part of the app, so they would stay on top. The rest of the window would change depending on mode, using four friendly buttons along the bottom. Switching to "Settings" would change the main part of the interface, and the "Settings" button would change to "Log", to allow returning to the time log that was the default view.
Timecard needed to use files for saving its settings and time logs. Unlike the first version of my program, where you had to manually export, I wanted the new Timecard to automatically save changes to file without the user needing to do anything.
This part of the project actually took a large chunk of my time, but it's just as well, since I'm in the middle of writing articles and a Dead Simple Python chapter all about working with files!
One of my main "must have" features was a system tray icon. I have a nasty habit of closing important windows, which is why most timer applications do not work for me! Closing Timecard's window should, by default, hide it in the system tray. Quitting would be done through the "Quit" button in the interface, which would prompt confirmation. (Of course, this hide-to-tray behavior can be changed in Settings.)
If you've ever coded interfaces before, you know that objects can be both a blessing and a curse. Encapsulation is super helpful for ensuring widgets are unified with their associated data and special functions.
Instances, on the other hand? Ugh. Most of the time, you only ever have one instance of, say, the time display widget, and everything that works with it needs to access that same instance. There are various ways of handling this, most of them bad: global instances of everything, singletons (please, no), or a God-class to handle communication between widgets. And yes, I've done all of the above...and recommend none of them.
For Timecard, I knew precisely which objects needed to exist only once, so I wrote those as static classes. By using
@classmethod and class attributes, I was able to sidestep all of the madness of juggling instances, without eschewing any of the encapsulation or namespace goodness that OOP provides.
I know some Python developers are probably reading this and yelling at their screens, but you know what? Classes worked really well for this!
- The code is clean and well-organized, with clearly defined responsibilites.
- No global variables. Everything is namespaced.
- Import statements are obvious.
- Yay readability!
I've already had to dive back into the code to fix a few bugs, but each time, it only took me a few moments to work out where the problem was.
I'm sure there are a few opportunities to refactor the code further, but I shudder to think of the nightmare I'd have if I hadn't used classes.
I'm so incredibly proud of this application, and I've already made it part of my productivity workflow.
To start logging, you just click
Start. You can describe your current activity in the
What are you doing? text field just below the timer. The current timer can be paused with
Pause, and that button becomes the
To stop the timer and save or reset it, you click
Stop, which then becomes the
Confirm Stop button (or else, you can click
Resume to cancel stopping.) The control buttons change once again to
Save, for either discarding your time or saving it to the log.
The time you started is used as the timestamp, and the activity note is taken from that message you typed below the timer.
Closing the window only hides it, and Timecard stays running in the system tray. Clicking on the icon shows the current timer duration, and allows you to pause and resume. (Stopping can only be done from the main window, to prevent mistakes.)
Quit option is disabled as long as a timer is running or unsaved, to ensure you don't accidentally throw away your time with a misclick on the menu.
The Settings view allows you to change where log files are saved. Updating this and clicking
Save will immediately open the new file, so it's easy to maintain and switch between log files. (I have plans to make this even easier later.)
You can also modify the timestamp format as it's displayed in the log view, although this doesn't change the file output for parsing reasons. You can also show durations as decimal hours, which is really handy if you need to enter your time into one of those annoying time reporting apps for work. (I hate always having to drag out a calculator to figure out decimal hours. Who thinks in that?)
Okay, enough of that. You probably want to start using this, don't you?
Currently, the only way to install is from PyPI, although I'm working on packaging in other formats. Still, if you have
pip handy, this is super easy!
pip install --user timecard-app
To start the program, just run: