├── README.rst ├── gtkexcepthook.py ├── icons ├── timeclock_16x16.png ├── timeclock_32x32.png ├── timeclock_48x48.png ├── timeclock_64x64.png └── timeclock_icon.svgz ├── timeclock.glade └── timeclock.py /README.rst: -------------------------------------------------------------------------------- 1 | The Procrastinator's Timeclock is a simple application designed to help 2 | easily-distracted people remain focused on getting a certain amount of work done 3 | per day while remaining flexible enough to adapt to changes in motivation, mood, 4 | and external stresses. 5 | 6 | It does this by working to counter several key contributors to procrastination: 7 | 8 | 1. You never intend to waste your entire day/week/etc. on distractions. 9 | Having an easy-to-use timeclock helps you to see how quickly your little 10 | distractions are adding up. 11 | 2. Willpower has nothing to do with will. According to researchers, self-control 12 | depends on the ability to distract oneself from undesirable influences... 13 | something the timeclock helps with whenever you check how much time is left. 14 | 3. Sometimes, people procrastinate as a way of avoiding "going on the clock". 15 | The timeclock helps you to get used to the idea that, even if you're just 16 | trying to finish a book before bedtime, you're always on the clock. 17 | 18 | However, as with any solution, discouragement is always a risk. Please keep the 19 | following in mind while using the timeclock: 20 | 21 | - Initially, you will probably fall short of your goals. I recommend budgeting 22 | six hours and expecting to initially average about four hours of productivity 23 | per day. (This assumes a schedule which allots eight hours including breaks) 24 | - "Work before pleasure" is ideal, but it's much more likely that you'll start 25 | out spending all your leisure and daily routine time before you start working. 26 | Don't let this discourage you. Once you get used to having a guaranteed 27 | minimum amount of leisure time, it'll become easier to motivate yourself to 28 | work first and relax afterward. 29 | - Given a tendency to only want to work when all leisure time is exhausted, 30 | it's probably a good idea to budget an extra hour or two for sleep so you 31 | won't end up sleep-deprived if you use up all of your daily routine time 32 | before you begin preparing for bed. 33 | -------------------------------------------------------------------------------- /gtkexcepthook.py: -------------------------------------------------------------------------------- 1 | # vim: sw=4 ts=4: 2 | # 3 | # (c) 2003 Gustavo J A M Carneiro gjc at inescporto.pt 4 | # 2004-2005 Filip Van Raemdonck 5 | # 6 | # http://www.daa.com.au/pipermail/pygtk/2003-August/005775.html 7 | # Message-ID: <1062087716.1196.5.camel@emperor.homelinux.net> 8 | # "The license is whatever you want." 9 | 10 | import inspect, linecache, pydoc, sys, traceback 11 | from cStringIO import StringIO 12 | from gettext import gettext as _ 13 | from smtplib import SMTP 14 | 15 | import pygtk 16 | pygtk.require ('2.0') 17 | import gtk, pango 18 | 19 | #def analyse (exctyp, value, tb): 20 | # trace = StringIO() 21 | # traceback.print_exception (exctyp, value, tb, None, trace) 22 | # return trace 23 | 24 | def lookup (name, frame, lcls): 25 | '''Find the value for a given name in the given frame''' 26 | if name in lcls: 27 | return 'local', lcls[name] 28 | elif name in frame.f_globals: 29 | return 'global', frame.f_globals[name] 30 | elif '__builtins__' in frame.f_globals: 31 | builtins = frame.f_globals['__builtins__'] 32 | if type (builtins) is dict: 33 | if name in builtins: 34 | return 'builtin', builtins[name] 35 | else: 36 | if hasattr (builtins, name): 37 | return 'builtin', getattr (builtins, name) 38 | return None, [] 39 | 40 | def analyse (exctyp, value, tb): 41 | import tokenize, keyword 42 | 43 | trace = StringIO() 44 | nlines = 3 45 | frecs = inspect.getinnerframes (tb, nlines) 46 | trace.write ('Traceback (most recent call last):\n') 47 | for frame, fname, lineno, funcname, context, cindex in frecs: 48 | trace.write (' File "%s", line %d, ' % (fname, lineno)) 49 | args, varargs, varkw, lcls = inspect.getargvalues (frame) 50 | 51 | def readline (lno=[lineno], *args): 52 | if args: print args 53 | try: return linecache.getline (fname, lno[0]) 54 | finally: lno[0] += 1 55 | all, prev, name, scope = {}, None, '', None 56 | for ttype, tstr, stup, etup, line in tokenize.generate_tokens (readline): 57 | if ttype == tokenize.NAME and tstr not in keyword.kwlist: 58 | if name: 59 | if name[-1] == '.': 60 | try: 61 | val = getattr (prev, tstr) 62 | except AttributeError: 63 | # XXX skip the rest of this identifier only 64 | break 65 | name += tstr 66 | else: 67 | assert not name and not scope 68 | scope, val = lookup (tstr, frame, lcls) 69 | name = tstr 70 | if val: 71 | prev = val 72 | #print ' found', scope, 'name', name, 'val', val, 'in', prev, 'for token', tstr 73 | elif tstr == '.': 74 | if prev: 75 | name += '.' 76 | else: 77 | if name: 78 | all[name] = (scope, prev) 79 | prev, name, scope = None, '', None 80 | if ttype == tokenize.NEWLINE: 81 | break 82 | 83 | trace.write (funcname + 84 | inspect.formatargvalues (args, varargs, varkw, lcls, formatvalue=lambda v: '=' + pydoc.text.repr (v)) + '\n') 85 | trace.write (''.join ([' ' + x.replace ('\t', ' ') for x in filter (lambda a: a.strip(), context)])) 86 | if len (all): 87 | trace.write (' variables: %s\n' % str (all)) 88 | 89 | trace.write ('%s: %s' % (exctyp.__name__, value)) 90 | return trace 91 | 92 | def _info (exctyp, value, tb): 93 | trace = None 94 | dialog = gtk.MessageDialog (parent=None, flags=0, type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_NONE) 95 | dialog.set_title (_("Bug Detected")) 96 | if gtk.check_version (2, 4, 0) is not None: 97 | dialog.set_has_separator (False) 98 | 99 | primary = _("A programming error has been detected during the execution of this program.") 100 | secondary = _("It probably isn't fatal, but should be reported to the developers nonetheless.") 101 | 102 | try: 103 | setsec = dialog.format_secondary_text 104 | except AttributeError: 105 | raise 106 | dialog.vbox.get_children()[0].get_children()[1].set_markup ('%s\n\n%s' % (primary, secondary)) 107 | #lbl.set_property ("use-markup", True) 108 | else: 109 | del setsec 110 | dialog.set_markup (primary) 111 | dialog.format_secondary_text (secondary) 112 | 113 | try: 114 | email = feedback 115 | dialog.add_button (_("Report..."), 3) 116 | except NameError: 117 | # could ask for an email address instead... 118 | pass 119 | dialog.add_button (_("Details..."), 2) 120 | dialog.add_button (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE) 121 | dialog.add_button (gtk.STOCK_QUIT, 1) 122 | 123 | while True: 124 | resp = dialog.run() 125 | if resp == 3: 126 | if trace == None: 127 | trace = analyse (exctyp, value, tb) 128 | 129 | # TODO: prettyprint, deal with problems in sending feedback, &tc 130 | try: 131 | server = smtphost 132 | except NameError: 133 | server = 'localhost' 134 | 135 | message = 'From: buggy_application"\nTo: bad_programmer\nSubject: Exception feedback\n\n%s' % trace.getvalue() 136 | 137 | s = SMTP() 138 | s.connect (server) 139 | s.sendmail (email, (email,), message) 140 | s.quit() 141 | break 142 | 143 | elif resp == 2: 144 | if trace == None: 145 | trace = analyse (exctyp, value, tb) 146 | 147 | # Show details... 148 | details = gtk.Dialog (_("Bug Details"), dialog, 149 | gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, 150 | (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE, )) 151 | details.set_property ("has-separator", False) 152 | 153 | textview = gtk.TextView(); textview.show() 154 | textview.set_editable (False) 155 | textview.modify_font (pango.FontDescription ("Monospace")) 156 | 157 | sw = gtk.ScrolledWindow(); sw.show() 158 | sw.set_policy (gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) 159 | sw.add (textview) 160 | details.vbox.add (sw) 161 | textbuffer = textview.get_buffer() 162 | textbuffer.set_text (trace.getvalue()) 163 | 164 | monitor = gtk.gdk.screen_get_default ().get_monitor_at_window (dialog.window) 165 | area = gtk.gdk.screen_get_default ().get_monitor_geometry (monitor) 166 | try: 167 | w = area.width // 1.6 168 | h = area.height // 1.6 169 | except SyntaxError: 170 | # python < 2.2 171 | w = area.width / 1.6 172 | h = area.height / 1.6 173 | details.set_default_size (int (w), int (h)) 174 | 175 | details.run() 176 | details.destroy() 177 | 178 | elif resp == 1 and gtk.main_level() > 0: 179 | gtk.main_quit() 180 | break 181 | else: 182 | break 183 | 184 | dialog.destroy() 185 | 186 | sys.excepthook = _info 187 | 188 | if __name__ == '__main__': 189 | class X (object): 190 | pass 191 | x = X() 192 | x.y = 'Test' 193 | x.z = x 194 | w = ' e' 195 | #feedback = 'developer@bigcorp.comp' 196 | #smtphost = 'mx.bigcorp.comp' 197 | 1, x.z.y, f, w 198 | raise Exception (x.z.y + w) 199 | -------------------------------------------------------------------------------- /icons/timeclock_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/timeclock/3c3f25124951f8174488e25ee1e544e1f72b6670/icons/timeclock_16x16.png -------------------------------------------------------------------------------- /icons/timeclock_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/timeclock/3c3f25124951f8174488e25ee1e544e1f72b6670/icons/timeclock_32x32.png -------------------------------------------------------------------------------- /icons/timeclock_48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/timeclock/3c3f25124951f8174488e25ee1e544e1f72b6670/icons/timeclock_48x48.png -------------------------------------------------------------------------------- /icons/timeclock_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/timeclock/3c3f25124951f8174488e25ee1e544e1f72b6670/icons/timeclock_64x64.png -------------------------------------------------------------------------------- /icons/timeclock_icon.svgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssokolow/timeclock/3c3f25124951f8174488e25ee1e544e1f72b6670/icons/timeclock_icon.svgz -------------------------------------------------------------------------------- /timeclock.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | True 7 | Time Clock 8 | False 9 | GTK_WIN_POS_CENTER 10 | icons/timeclock_48x48.png 11 | 12 | 13 | 14 | True 15 | 5 16 | 4 17 | 3 18 | 5 19 | 5 20 | 21 | 22 | True 23 | True 24 | _Work 25 | True 26 | 0 27 | btn_sleepMode 28 | 29 | 30 | 31 | 1 32 | 2 33 | 1 34 | 2 35 | GTK_EXPAND 36 | 37 | 38 | 39 | 40 | True 41 | True 42 | _Leisure 43 | True 44 | 0 45 | True 46 | btn_sleepMode 47 | 48 | 49 | 50 | 2 51 | 3 52 | 1 53 | 2 54 | GTK_EXPAND 55 | 56 | 57 | 58 | 59 | True 60 | True 61 | _Daily Routine 62 | True 63 | 0 64 | True 65 | btn_sleepMode 66 | 67 | 68 | 69 | 1 70 | 2 71 | GTK_EXPAND 72 | 73 | 74 | 75 | 76 | True 77 | True 78 | Pause all timers 79 | _Asleep 80 | True 81 | 0 82 | True 83 | 84 | 85 | 86 | 2 87 | 3 88 | 4 89 | GTK_EXPAND 90 | 91 | 92 | 93 | 94 | True 95 | GTK_PROGRESS_RIGHT_TO_LEFT 96 | 97 | 98 | 1 99 | 2 100 | GTK_EXPAND 101 | 102 | 103 | 104 | 105 | True 106 | GTK_PROGRESS_RIGHT_TO_LEFT 107 | 108 | 109 | 2 110 | 3 111 | GTK_EXPAND 112 | 113 | 114 | 115 | 116 | True 117 | GTK_PROGRESS_RIGHT_TO_LEFT 118 | 119 | 120 | GTK_EXPAND 121 | 122 | 123 | 124 | 125 | True 126 | 127 | 128 | 3 129 | 2 130 | 3 131 | 5 132 | 133 | 134 | 135 | 136 | True 137 | 138 | 139 | True 140 | True 141 | True 142 | Preferences 143 | 0 144 | 145 | 146 | 147 | True 148 | gtk-preferences 149 | 150 | 151 | 152 | 153 | False 154 | False 155 | 5 156 | 157 | 158 | 159 | 160 | True 161 | True 162 | True 163 | Reset all timers 164 | _Reset 165 | True 166 | 0 167 | 168 | 169 | 170 | 1 171 | 172 | 173 | 174 | 175 | 2 176 | 3 177 | 3 178 | 4 179 | GTK_EXPAND 180 | 181 | 182 | 183 | 184 | 185 | 186 | 5 187 | GTK_WIN_POS_CENTER_ON_PARENT 188 | GDK_WINDOW_TYPE_HINT_DIALOG 189 | False 190 | 191 | 192 | True 193 | 2 194 | 195 | 196 | True 197 | True 198 | 199 | 200 | True 201 | 10 202 | 3 203 | 3 204 | 5 205 | 5 206 | 207 | 208 | True 209 | 0 210 | Hours 211 | 212 | 213 | 2 214 | 3 215 | 2 216 | 3 217 | 218 | 219 | 220 | 221 | 222 | True 223 | 0 224 | Hours 225 | 226 | 227 | 2 228 | 3 229 | 1 230 | 2 231 | 232 | 233 | 234 | 235 | 236 | True 237 | 0 238 | Hours 239 | 240 | 241 | 2 242 | 3 243 | 244 | 245 | 246 | 247 | 248 | True 249 | True 250 | 1 251 | 6 1 15 0.25 1 1 252 | 2 253 | True 254 | 255 | 256 | 1 257 | 2 258 | 2 259 | 3 260 | GTK_FILL 261 | 262 | 263 | 264 | 265 | 266 | True 267 | True 268 | 1 269 | 6 0 14 0.25 1 1 270 | 2 271 | True 272 | 273 | 274 | 1 275 | 2 276 | 1 277 | 2 278 | GTK_FILL 279 | 280 | 281 | 282 | 283 | 284 | True 285 | 1 286 | _Leisure: 287 | True 288 | True 289 | spinBtn_leisureMode 290 | 291 | 292 | 2 293 | 3 294 | 295 | 296 | 297 | 298 | 299 | True 300 | 1 301 | _Work: 302 | True 303 | True 304 | spinBtn_workMode 305 | 306 | 307 | 1 308 | 2 309 | 310 | 311 | 312 | 313 | 314 | True 315 | 1 316 | Daily _Routine: 317 | True 318 | True 319 | spinBtn_overheadMode 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | True 328 | True 329 | 1 330 | 4 1 15 0.25 1 1 331 | 2 332 | True 333 | 334 | 335 | 1 336 | 2 337 | GTK_FILL 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | True 346 | _Timers 347 | True 348 | True 349 | 350 | 351 | tab 352 | False 353 | 354 | 355 | 356 | 357 | True 358 | 5 359 | 2 360 | 361 | 362 | True 363 | True 364 | display notifications (Requires pynotify) 365 | 0 366 | True 367 | 368 | 369 | 1 370 | 2 371 | 372 | 373 | 374 | 375 | 376 | True 377 | 0 378 | When a timer runs out... 379 | 380 | 381 | 382 | 5 383 | 384 | 385 | 386 | 387 | 1 388 | 389 | 390 | 391 | 392 | True 393 | _Notifications 394 | True 395 | True 396 | 397 | 398 | tab 399 | 1 400 | False 401 | 402 | 403 | 404 | 405 | 1 406 | 407 | 408 | 409 | 410 | True 411 | GTK_BUTTONBOX_END 412 | 413 | 414 | True 415 | True 416 | True 417 | gtk-cancel 418 | True 419 | 0 420 | 421 | 422 | 423 | 424 | 425 | True 426 | True 427 | True 428 | gtk-ok 429 | True 430 | 0 431 | 432 | 433 | 434 | 1 435 | 436 | 437 | 438 | 439 | False 440 | GTK_PACK_END 441 | 442 | 443 | 444 | 445 | 446 | 447 | -------------------------------------------------------------------------------- /timeclock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | A simple application to help lazy procrastinators (me) to manage their time. 5 | See http://ssokolow.github.com/timeclock/ for a screenshot. 6 | 7 | @todo: Planned improvements: 8 | - I think PyNotify takes HTML as input. Confirm that I should be XML-escaping 9 | ampersands and friends and then add it. 10 | - Clicking the preferences button while the dialog is shown shouldn't reset the 11 | unsaved preference changes. 12 | - Figure out some intuitive, non-distracting way to allow the user to make 13 | corrections. (eg. if you forget to set the timer to leisure before going AFK) 14 | - Should I offer preferences options for remembering window position and things 15 | like "always on top" and "on all desktops"? 16 | - Have the system complain if overhead + work + leisure + sleep (8 hours) > 24 17 | and enforce minimums of 1 hour for leisure and overhead. 18 | - Rework the design to minimize dependence on GTK+ (in case I switch to Qt for 19 | Phonon) 20 | - Report PyGTK's uncatchable xkill response on the bug tracker. 21 | 22 | @todo: Notification TODO: 23 | - Provide a fallback for when libnotify notifications are unavailable. 24 | (eg. Windows and Slax LiveCD/LiveUSB desktops) 25 | - Offer to turn the timer text a user-specified color (default: red) when it 26 | goes into negative values. 27 | - Finish the preferences page. 28 | - Add optional sound effects for timer completion using gst-python or PyGame: 29 | - http://mail.python.org/pipermail/python-list/2006-October/582445.html 30 | - http://www.jonobacon.org/2006/08/28/getting-started-with-gstreamer-with-python/ 31 | - Set up a callback for timer exhaustion. 32 | 33 | @todo: Consider: 34 | - Changing this into a Plasma widget (Without dropping PyGTK support) 35 | - Using PyKDE's bindings to the KDE Notification system (for the Plasma widget) 36 | 37 | @todo: Publish this on listing sites: 38 | - http://gtk-apps.org/ 39 | - http://pypi.python.org/pypi 40 | 41 | @newfield appname: Application Name 42 | """ 43 | 44 | __appname__ = "The Procrastinator's Timeclock" 45 | __authors__ = ["Stephan Sokolow (deitarion/SSokolow)", "Charlie Nolan (FunnyMan3595)"] 46 | __version__ = "0.2" 47 | __license__ = "GNU GPL 2.0 or later" 48 | 49 | # Mode constants. 50 | SLEEP, OVERHEAD, WORK, LEISURE = range(4) 51 | MODE_NAMES = ("sleep", "overhead", "work", "leisure") 52 | 53 | default_modes = { 54 | OVERHEAD : int(3600 * 3.5), 55 | WORK : 3600 * 6, 56 | LEISURE : int(3600 * 5.5), 57 | } 58 | 59 | import logging, os, signal, sys, time, pickle 60 | 61 | SELF_DIR = os.path.dirname(os.path.realpath(__file__)) 62 | DATA_DIR = os.environ.get('XDG_DATA_HOME', os.path.expanduser('~/.local/share')) 63 | if not os.path.isdir(DATA_DIR): 64 | try: 65 | os.makedirs(DATA_DIR) 66 | except OSError: 67 | raise SystemExit("Aborting: %s exists but is not a directory!" 68 | % DATA_DIR) 69 | SAVE_FILE = os.path.join(DATA_DIR, "timeclock.sav") 70 | SAVE_INTERVAL = 60 * 5 # 5 Minutes 71 | file_exists = os.path.isfile 72 | 73 | logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') 74 | 75 | try: 76 | import pygtk 77 | pygtk.require("2.0") 78 | except: 79 | pass 80 | 81 | import gtk, gobject 82 | import gtk.glade 83 | import gtkexcepthook 84 | 85 | try: 86 | import pynotify 87 | except ImportError: 88 | have_pynotify = False 89 | notify_exhaustion = lambda timer_name: None 90 | else: 91 | have_pynotify = True 92 | pynotify.init(__appname__) 93 | 94 | # Make the notifications in advance, 95 | notifications = {} 96 | for mode in default_modes: 97 | mode_name = MODE_NAMES[mode] 98 | notification = pynotify.Notification( 99 | "%s Time Exhausted" % mode_name.title(), 100 | "You have used up your alotted time for %s" % mode_name.lower(), 101 | os.path.join(SELF_DIR, "icons", "timeclock_48x48.png")) 102 | notification.set_urgency(pynotify.URGENCY_NORMAL) 103 | notification.set_timeout(pynotify.EXPIRES_NEVER) 104 | notification.last_shown = 0 105 | notifications[mode] = notification 106 | 107 | def notify_exhaustion(mode): 108 | """Display a libnotify notification that the given timer has expired.""" 109 | notification = notifications[mode] 110 | now = time.time() 111 | if notification.last_shown + 900 < now: 112 | notification.last_shown = now 113 | notification.show() 114 | 115 | CURRENT_SAVE_VERSION = 3 #: Used for save file versioning 116 | class TimeClock: 117 | def __init__(self, default_mode="sleep"): 118 | self.default_mode = default_mode 119 | 120 | #Set the Glade file 121 | self.gladefile = os.path.join(SELF_DIR, "timeclock.glade") 122 | self.wTree = gtk.glade.XML(self.gladefile) 123 | 124 | self.last_tick = time.time() 125 | self.last_save = 0 126 | self._init_widgets() 127 | 128 | self.notify = True 129 | 130 | # Load the save file, if it exists. 131 | if file_exists(SAVE_FILE): 132 | try: 133 | # Load the data, but leave the internal state unchanged in case 134 | # of corruption. 135 | loaded = pickle.load(open(SAVE_FILE)) 136 | version = loaded[0] 137 | if version == CURRENT_SAVE_VERSION: 138 | version, total, used, notify = loaded 139 | elif version == 2: 140 | version, total, used = loaded 141 | notify = True 142 | elif version == 1: 143 | version, total_old, used_old = loaded 144 | translate = ["N/A", "btn_overheadMode", "btn_workMode", 145 | "btn_playMode"] 146 | total = dict( (translate.index(key), value) 147 | for key, value in total_old.items() ) 148 | used = dict( (translate.index(key), value) 149 | for key, value in used_old.items() ) 150 | notify = True 151 | else: 152 | raise ValueError("Save file too new!") 153 | 154 | # Sanity checking could go here. 155 | 156 | except Exception, e: 157 | logging.error("Unable to load save file. Ignoring: %s", e) 158 | else: 159 | # File loaded successfully, now we put the data in place. 160 | self.total = total 161 | self.used = used 162 | self.notify = notify 163 | self.update_progressBars() 164 | 165 | # Connect signals 166 | dic = { "on_mode_toggled" : self.mode_changed, 167 | "on_reset_clicked" : self.reset_clicked, 168 | "on_prefs_clicked" : self.prefs_clicked, 169 | "on_prefs_commit" : self.prefs_commit, 170 | "on_prefs_cancel" : self.prefs_cancel, 171 | "on_mainWin_destroy" : gtk.main_quit } 172 | self.wTree.signal_autoconnect(dic) 173 | gobject.timeout_add(1000, self.tick) 174 | 175 | def _init_widgets(self): 176 | """All non-signal, non-glade widget initialization.""" 177 | # Set up the data structures 178 | self.timer_widgets = {} 179 | self.total, self.used = {}, {} 180 | for mode in default_modes: 181 | widget = self.wTree.get_widget('btn_%sMode' % MODE_NAMES[mode]) 182 | widget.mode = mode 183 | self.timer_widgets[widget] = \ 184 | self.wTree.get_widget('progress_%sMode' % MODE_NAMES[mode]) 185 | self.total[mode] = default_modes[mode] 186 | self.used[mode] = 0 187 | sleepBtn = self.wTree.get_widget('btn_sleepMode') 188 | sleepBtn.mode = SLEEP 189 | 190 | self.selectedBtn = self.wTree.get_widget('btn_%sMode' % self.default_mode) 191 | self.selectedBtn.set_active(True) 192 | 193 | # Because PyGTK isn't reliably obeying Glade 194 | self.update_progressBars() 195 | for widget in self.timer_widgets: 196 | widget.set_property('draw-indicator', False) 197 | sleepBtn.set_property('draw-indicator', False) 198 | 199 | def update_progressBars(self): 200 | """Common code used for initializing and updating the progress bars.""" 201 | for widget in self.timer_widgets: 202 | pbar = self.timer_widgets[widget] 203 | total, val = self.total[widget.mode], self.used[widget.mode] 204 | remaining = round(total - val) 205 | if pbar: 206 | if remaining >= 0: 207 | pbar.set_text(time.strftime('%H:%M:%S', time.gmtime(remaining))) 208 | else: 209 | pbar.set_text(time.strftime('-%H:%M:%S', time.gmtime(abs(remaining)))) 210 | pbar.set_fraction(max(float(remaining) / self.total[widget.mode], 0)) 211 | 212 | def mode_changed(self, widget): 213 | """Callback for clicking the timer-selection radio buttons""" 214 | if widget.get_active(): 215 | self.selectedBtn = widget 216 | 217 | if self.selectedBtn.mode == SLEEP: 218 | self.doSave() 219 | 220 | def reset_clicked(self, widget): 221 | """Callback for the reset button""" 222 | self.used = dict((x, 0) for x in self.used) 223 | self.wTree.get_widget('btn_%sMode' % MODE_NAMES[SLEEP]).set_active(True) 224 | self.update_progressBars() 225 | 226 | def prefs_clicked(self, widget): 227 | """Callback for the preferences button""" 228 | # Set the spin widgets to the current settings. 229 | for mode in self.total: 230 | widget_spin = 'spinBtn_%sMode' % MODE_NAMES[mode] 231 | widget = self.wTree.get_widget(widget_spin) 232 | widget.set_value(self.total[mode] / 3600.0) 233 | 234 | # Set the notify option to the current value, disable and explain if 235 | # pynotify is not installed. 236 | notify_box = self.wTree.get_widget('checkbutton_notify') 237 | notify_box.set_active(self.notify) 238 | if have_pynotify: 239 | notify_box.set_sensitive(True) 240 | notify_box.set_label("display notifications") 241 | else: 242 | notify_box.set_sensitive(False) 243 | notify_box.set_label("display notifications (Requires pynotify)") 244 | 245 | self.wTree.get_widget('prefsDlg').show() 246 | 247 | def prefs_cancel(self, widget): 248 | """Callback for cancelling changes the preferences""" 249 | self.wTree.get_widget('prefsDlg').hide() 250 | 251 | def prefs_commit(self, widget): 252 | """Callback for OKing changes to the preferences""" 253 | # Update the time settings for each mode. 254 | for mode in self.total: 255 | widget_spin = 'spinBtn_%sMode' % MODE_NAMES[mode] 256 | widget = self.wTree.get_widget(widget_spin) 257 | self.total[mode] = (widget.get_value() * 3600) 258 | 259 | notify_box = self.wTree.get_widget('checkbutton_notify') 260 | self.notify = notify_box.get_active() 261 | 262 | # Remaining cleanup. 263 | self.update_progressBars() 264 | self.wTree.get_widget('prefsDlg').hide() 265 | 266 | def tick(self): 267 | """Once-per-second timeout callback for updating progress bars.""" 268 | mode = self.selectedBtn.mode 269 | now = time.time() 270 | if mode != SLEEP: 271 | self.used[mode] += (now - self.last_tick) 272 | self.update_progressBars() 273 | 274 | if self.used[mode] >= self.total[mode] and self.notify: 275 | notify_exhaustion(mode) 276 | 277 | if now >= (self.last_save + SAVE_INTERVAL): 278 | self.doSave() 279 | 280 | self.last_tick = now 281 | 282 | return True 283 | 284 | def doSave(self): 285 | """Exit/Timeout handler for the app. Gets called every five minutes and 286 | on every type of clean exit except xkill. (PyGTK doesn't let you) 287 | 288 | Saves the current timer values to disk.""" 289 | pickle.dump( (CURRENT_SAVE_VERSION, self.total, self.used, self.notify), 290 | open(SAVE_FILE, "w") ) 291 | self.last_save = time.time() 292 | return True 293 | 294 | def main(): 295 | from optparse import OptionParser 296 | parser = OptionParser(version="%%prog v%s" % __version__) 297 | #parser.add_option('-v', '--verbose', action="store_true", dest="verbose", 298 | # default=False, help="Increase verbosity") 299 | parser.add_option('-m', '--initial-mode', 300 | action="store", dest="mode", default="sleep", 301 | metavar="MODE", help="start in MODE. (Use 'help' for a list)") 302 | 303 | opts, args = parser.parse_args() 304 | if opts.mode == 'help': 305 | print "Valid mode names are: %s" % ', '.join(MODE_NAMES) 306 | parser.exit(0) 307 | elif (opts.mode not in MODE_NAMES): 308 | print "Mode '%s' not recognized, defaulting to sleep." % opts.mode 309 | opts.mode = "sleep" 310 | app = TimeClock(default_mode=opts.mode) 311 | 312 | # Make sure that state is saved to disk on exit. 313 | sys.exitfunc = app.doSave 314 | signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(0)) 315 | signal.signal(signal.SIGHUP, lambda signum, stack_frame: sys.exit(0)) 316 | signal.signal(signal.SIGQUIT, lambda signum, stack_frame: sys.exit(0)) 317 | signal.signal(signal.SIGINT, lambda signum, stack_frame: sys.exit(0)) 318 | 319 | try: 320 | gtk.main() 321 | except KeyboardInterrupt: 322 | sys.exit(0) 323 | 324 | if __name__ == '__main__': 325 | main() 326 | --------------------------------------------------------------------------------