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.

# -*- 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,
    setattr(TkinterWrapper, name, subwidget_method)

# These are the subwidget types I’m currently using.
for widget_class_name in 'Label Entry Button Frame'.split():

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:

### Application code

def append_to_file(filename, astring):
    "Append the given string to the file with the given name."
    f = open(filename, 'a')


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

        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',
        self.button.bind('<Return>', self.click)

        self.allbutton = buttons.Button(text='Log for 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.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."

    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[:]:

    def close_with(self, message):
        "Logs the specified message and closes the window."
        if message == '':      # ignore attempts to log empty messages
        append_to_file(self.filename, '%s %s\n' % (self.datetime, message))

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):

def main(argv):
    if len(argv) == 2:
        filename = argv[1]
        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())

if __name__ == '__main__':

# 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

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

Reply via email to