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