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.
Creating Timecard 2.0
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.)
No Dialogs
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.
Files
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!
System Tray
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.)
Static Classes
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.
Meet Timecard 2.0!
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 Resume
button.
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 Reset
and 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.)
The 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?)
Download
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:
timecard-app
For more information about Timecard, including the features I have planned for the future, check out the official page and GitHub. I welcome feedback, issue reports, and pull requests.
Top comments (11)
Well, I wouldn't be so harsh as to consider it "nasty". Perhaps you have simply gotten into the habit of closing windows in order to minimize potential distractions. 😉
Anyway, cool app! It's a nice and simple breath of fresh air from an oversaturated market of Electron apps. Are there any future plans for this app, or would you consider it to be "feature-complete" as is?
I definitely have future plans, chief of which being Pomodoro technique features. I haven't nailed them all down definitely yet, but my current concept is:
All of these features would be entirely optional. You'll be able to toggle Pomodoro Mode in the Settings.
In addition, I'm also planning the following:
I've been wanting to use the Pomodoro technique in my work - when you get that in there I'll definitely download and star on GitHub! :)
Hey, awesome! It's on my short list of tasks for the month.
In the meantime, I'd really appreciate any other feedback you have about the application as it stands right now. It'd be great to know if anything else needs to be improved in v2.1.
I've never heard about the Pomodoro technique until now. Thanks for sharing! I'm sure it would be a very useful feature for a productivity app.
Interesting, thanks for the article.
Couple of suggestions:
Make the activities a list saved into a separate file that could be selected from a combo box.
A minimal mode for the window, with the activity combo box, start and stop buttons.
Thanks for the feedback!
I tend to use very specific activity, such as Phabricator object codes or article names. The combo box would feel like overkill for that, I feel.
Hmm...that sounds tricky to pull off, but very appealing if I could! I'll add that to my feature wishlist. (In the meantime, the system tray icon mostly fills this role by displaying the timer duration and allowing you to pause and resume.)
Guess it depends on your work patterns, but if you have specific activities that you work on, then a list is more effective than having to type in all the time.
Personally, I don't really see the difference between global variables and static classes for storing the state.
But, the size of this project means trying to engineer a more robust solution would probably be overkill, and if you like working with static classes the best, that's the right choice.
Great job with this 👍
You're correct that there isn't a huge difference. I think the main difference is that, with global instances of objects, you have to worry about checking if they're
None
throughout most of the code. Static classes don't have that worry (as long as you architect that code right).That's really cool idea of a project with GUI. Nicely done.