├── 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 |
--------------------------------------------------------------------------------