Bad dog! Practice your Spanish flashcards

I wrote baddog.py to manage my time a little better. It wont let you download textbooks into your brain or even let you leave work early, but it does annoy you if you slack off online before finishing your to-do list and that might add up to a new skill in a year.

There’s not actually a lot going on in baddog.py. It’s the plumbing that lets my task tracker (habitica) see if I’m being productive (arbtt) and if I’m not, prompt me to do something productive like anki or coursera.

arbtt + habitica + cron + { anki, coursera, …} = baddog

I’ve chosen tools that work well for me as someone who has been writing software professionally for almost a decade. If you’re inspired by this, you don’t need my exact tools to make a baddog.py of your own.

if screwing around now:
  if anything left to do:
    suggest alternative

Arbtt (are you screwing around?)

Arbtt tracks what program is active on your screen are and lets you write rules to categorize your time. I’ve written a short guide to getting started here. For this project, it’s important to have some rules like the following:

# .arbtt/categorize.cfg
current window $program == "chrome" ==> {
    current window $title =~
      m!YouTube!  ==> tag Project:ScrewingAround,
    current window $title =~
      m!imgur!  ==> tag Project:ScrewingAround
    ...
}

#
$sampleage <= 00:10 ==> tag last-ten-minutes,
$sampleage <= 00:01 ==> tag now,

Which can be read as

If your active program is chrome and your current tab title includes either YouTube or imgur, you’re screwing around.

With rules like these (and you’ll need more to match your personal time sinks), arbtt can generate a report on your current time usage.

arbtt-stats -o now -c Project

Statistics for category "Project"
=================================
__________ _Tag_|___Time_|_Percentage_
Project:Blogging|  1m00s |     100.00

From within python, we can read this with some code like

import subprocess

def percent_wasting_time():
    arbtt_response = subprocess.run([
        "arbtt-stats",
        "-o", "now",
        "-c", "Project",
        "--output-format", "csv",
    ], stdout=subprocess.PIPE, check=True)

    buf = io.StringIO(arbtt_response.stdout.decode())

    for row in csv.DictReader(buf):
        if row['Tag'] == 'Project:ScrewingAround':
            return row['Percentage']
    return 0

Habitica (what’s left on my to-do list?)

Habitica is not only a great task tracker with some really cute functionality, it also has an awesome API that made baddog.py possible. Habitica organizes your to-do list into three parts: habits are actions like “read” that you want to do often or “smoke” that you never want to do, dailies are recurring tasks like “water my plants” or “study Spanish flashcards,” and to-dos are one-time tasks like “schedule a doctor’s appointment.”

For this project, we’re primarily interested in incomplete dailies. The below code will loop through your dailies and print the ones that you should do now.

# grab these from https://habitica.com/user/settings/api
HABITICA_HEADERS = {
  "x-api-key": ...,
  "x-api-user": ...
}

resp = requests.get(
    "https://habitica.com/api/v3/tasks/user",
    params={"type": "dailys"},  # NOTE: spelling
    headers=HABITICA_HEADERS)
resp.raise_for_status()

for daily in resp.json['data']:
    if daily['text'] == task_name:
        if daily['isDue'] and not daily['completed']:
          print(task_name)

where API returns a response like

{
  "data": [
    {
      "id": ...,
      "text": "anki",
      "type": "daily",
      "isDue": true,
      "completed": false,
      ...
    },
  ]
}

I’ve shoved that logic into a decorator so I can wrap a action like “open flashcard program” with @if_habitica_due("Anki") to ensure the program launches only when the task is due and not yet completed.

import requests_cache

# cache requests for 60 seconds
requests_cache.install("baddog", expire_after=60)

def if_habitica_due(task_name):
    """ check if task_name is an incomplete daily """
    def task_filter():
        resp = requests.get(
            "https://habitica.com/api/v3/tasks/user",
            params={"type": "dailys"},  # NOTE: spelling
            headers=HABITICA_HEADERS)
        resp.raise_for_status()

        for daily in resp.json['data']:
            if daily['text'] == task_name:
                return daily['isDue'] and \
                    not daily['completed']

    return if_filter(task_filter)

@decorator
def if_filter(action, filter_func=None):
    if filter_func():
        action()
        return True

Bonus tip: if you can’t find the right endpoint in the API docs, open the network tab on your browser and watch what happens when you execute the action through the web interface. Habitica uses it’s own API.

Finding something to do?

Now that you have all of the information to decide that you’ve been screwing around and what things you have left to do, let suggest some alternative uses of your time. Hooking up a desktop notification like “Bad dog! Study your Spanish flashcards” would be good, but even better would be opening your flashcard program.

Here are a few of mine where I launch an particular program or open a particular website:

def desktop_alert(message):
    subprocess.run(["notify-send", "Bad Dog!", message])

def browser(url):
    subprocess.Popen(["chromium-browser", url])
    subprocess.Popen(["wmctrl", "-a", "chromium"])

@if_habitica_due("Anki")
def suggest_anki():
    desktop_alert("Study your Spanish flashcards")
    subprocess.Popen(["/usr/bin/anki"])
    sleep(2)  # make sure that Anki has opened

    # switch to anki window
    subprocess.Popen(["wmctrl", "-a", "anki"])

@if_habitica_due("coursera")
def suggest_coursera():
    desktop_alert("Work on stats course")
    browser("https://www.coursera.org/learn/statistical-inferences")

@if_habitica_due("read ten pages")
def suggest_book():
    desktop_alert("Read ten pages")
    subprocess.Popen(["evince", os.path.join(
        "/home",
        "alex",
        "Calibre Library",
        "Statistical Rethinking",
        "McElreath (2015) (3)",
        "McElreath (2015) - Statistical Rethinking.pdf"
    )])

Putting it all together

From my scheduling program of choice (cron) I execute a main function that checks if I’m wasting time and if I am, looks for a incomplete daily to suggest.

diversions = [
    suggest_anki,
    suggest_coursera,
    suggest_book,
]

shuffle(diversions)

if percent_wasting_time() > 50:
    for diverision in diversions:
        # returns True and opens the diversion
        if diversion():
            break

The code will need to be heavily customized to work on any other environment, so I don’t think there’s a ton of use in sharing the entire script, but I hope you implement your own baddog.py, baddog.js, or baddog.R.

Bonus: the missing pieces

That was a version of my script that I edited heavily to simplify the interface, minimize line lengths, reduce the number of diversions, and remove some extra functionality. One of the most important pieces I trimmed is that every time baddog.py catches me screwing around, it dings me on habitica.

habitica penalty dialogue

I have a “habit” called “baddog” that can only be sub

habitica penalty dialogue

BADDOG_HABIT = ...   # task-id for "habit" which can be

def scold():
    """ penalize me on habitica habit """
    resp = requests.post(
        f"https://habitica.com/api/v3/tasks/{BADDOG_HABIT}/score/down",
        headers=HABITICA_HEADERS)
    resp.raise_for_status()

My current iteration, instead of looking just at whether I’m currently screwing around, looks at how long I’ve been screwing around for. Sometimes I watch YouTube videos or look at silly memes and it’s not the end of the world, so I make sure that I’ve been screwing around for at least 50% of the last 5 minutes and am currently screwing around.

I have more categories than just “Project:ScrewingAround” like “Project:SocialMedia” which get summed up into my percent_wasting_time().

Updated: