I was curious where my time was going, so I programmed my computer to
ask me, “What are you doing right now?” in a popup window about four
times an hour. It logs the answers for my later perusal. Apparently I
read a lot and don’t have sex a very large fraction of the time.
I tried porting this to GTK with the Python GTK binding, which has
better fonts and keybindings, but I couldn’t figure out how to get the
appropriately annoying focus-stealing and pop-up-on-top behavior.
#!/usr/bin/python
# -*- coding: utf-8; -*-
"""Asks, "What are you doing right now?" at random, averaging every 15 minutes.
Results are appended to `~/waydrn15.log` by default.
"""
import sys, os, Tkinter, time, random
### Tkinter interface
# This is because Tkinter makes it unnecessarily difficult to create
# and configure Tk widgets (unless you import a zillion names into
# your own namespace).
class TkinterWrapper:
"Small wrapper to make it easier to create Tk subwidgets."
def __init__(self, underlying_widget):
self.underlying_widget = underlying_widget
def __getattr__(self, name):
"Transparently pass on attribute requests other than for subwidgets."
return getattr(self.underlying_widget, name)
def _attach_subwidget_method(name):
"Create a method on TkinterWrapper to create a kind of subwidget."
widget_class = getattr(Tkinter, name)
def subwidget_method(self, *args, **kwargs):
return TkinterWrapper(widget_class(self.underlying_widget, *args,
**kwargs))
setattr(TkinterWrapper, name, subwidget_method)
# These are the subwidget types I’m currently using.
for widget_class_name in 'Label Entry Button Frame'.split():
_attach_subwidget_method(widget_class_name)
def Tk(*args, **kwargs):
"Wrapper for Tkinter.Tk."
return TkinterWrapper(Tkinter.Tk(*args, **kwargs))
def Toplevel(*args, **kwargs):
"Wrapper for Tkinter.Toplevel."
return TkinterWrapper(Tkinter.Toplevel(*args, **kwargs))
def pack(*args, **kwargs):
"A pack function like Tk’s pack command, which takes multiple widgets."
for widget in args:
widget.pack(**kwargs)
### Application code
def append_to_file(filename, astring):
"Append the given string to the file with the given name."
f = open(filename, 'a')
try:
f.write(astring)
finally:
f.close()
class QueryWindow:
"The window that pops up to ask you what you’re doing."
def __init__(self, filename, windows):
"""Instantiate a QueryWindow object, but don’t open the window.
filename: the filename to log to.
windows: the shared list of windows.
You can't call any other methods on the QueryWindow until you
call `.open()`.
"""
self.filename = filename
self.windows = windows
windows.append(self)
now = time.localtime(time.time())
self.datetime = time.strftime('%Y-%m-%d %H:%M', now)
self.text = ('What are you doing right now? (%s)' %
time.strftime('%H:%M', now))
def open(self):
"Open the window on the display."
self.toplevel = Toplevel()
self.entry = self.toplevel.Entry(width=80)
self.entry.bind('<Return>', self.click)
buttons = self.toplevel.Frame()
self.button = buttons.Button(text='Log it for this time',
command=self.click)
self.button.bind('<Return>', self.click)
self.allbutton = buttons.Button(text='Log for all',
command=self.log_all)
self.allbutton.bind('<Return>', self.log_all)
pack(self.button, self.allbutton, side='left')
pack(self.toplevel.Label(text=self.text), self.entry, buttons)
self.entry.focus()
self.toplevel.lift() # raise above other windows, stealing focus
def message(self):
"Returns the message the user has typed."
return self.entry.get().encode('utf-8')
# *args because .bind passes an event argument, while a button
# command doesn’t.
def click(self, *args):
"Event handler that logs the user's message and closes the window."
self.close_with(self.message())
def log_all(self, *args):
"Logs the user’s message for all open windows and closes them."
# Save the message, since we can’t call self.message() after
# the window closes.
message = self.message()
# Copy the list of windows since it will be changed as we
# iterate over it and each window removes itself.
for window in self.windows[:]:
window.close_with(message)
def close_with(self, message):
"Logs the specified message and closes the window."
if message == '': # ignore attempts to log empty messages
return
append_to_file(self.filename, '%s %s\n' % (self.datetime, message))
self.windows.remove(self)
self.toplevel.destroy()
class PoissonScheduler:
"A scheduler object that schedules itself with Tk to run a thunk randomly."
def __init__(self, mainwin, seconds, thunk):
"""Instantiate the scheduler, but don’t start it running.
mainwin: the Tk widget to use for scheduling (say, a Tk())
seconds: the average interval between random runs of the thunk.
thunk: the code to run periodically.
"""
self.mainwin = mainwin
self.seconds = seconds
self.thunk = thunk
def run(self):
# Generate an exponential-variate delay in order to produce a
# Poisson process, i.e. the probability of a window popping up
# in any particular time interval is independent of when the
# last one popped up.
seconds = random.expovariate(1.0/self.seconds)
self.mainwin.after(int(1000 * seconds), self.fire)
def fire(self):
self.thunk()
self.run()
def main(argv):
if len(argv) == 2:
filename = argv[1]
else:
filename = os.path.join(os.environ['HOME'], 'waydrn15.log')
mainwin = Tk()
mainwin.title('time sampling applet')
pack(mainwin.Label(text='Close this window to stop logging your time.'),
mainwin.Label(text='Logging to %r.' % filename))
windows = []
seconds = 15*60
# uncomment for debugging: seconds = 5
scheduler = PoissonScheduler(mainwin, seconds,
lambda: QueryWindow(filename, windows).open())
scheduler.run()
mainwin.mainloop()
if __name__ == '__main__':
main(sys.argv)
# Local Variables:
# compile-command: "./waydrn15.py"
# End:
(End of `waydrn15.py`.)
This software is available via
git clone http://canonical.org/~kragen/sw/inexorable-misc.git
(or in <http://canonical.org/~kragen/sw/inexorable-misc>) in the file
`waydrn15.py`.
Like everything else posted to kragen-hacks without a notice to the
contrary, this software is in the public domain.
--
To unsubscribe: http://lists.canonical.org/mailman/listinfo/kragen-hacks