James Williams
LinkedInMastodonGithub

Creating a Groovy add-on for Todo.txt

Tags: Groovy

Even though I've only been using Todo.txt for a couple days, I've fallen in love with it enought to figure out how to write an add-on. Because it uses a lightly formatted text file, if you have a good grasp of String manipulation and File I/O, you are off to the races.

One of the add-ons that I installed with Todo is lately. It gives you a listing of the tasks you have completed in a certain threshold. I loved the add-on but my list is a combination of my work and personal tasks. I wanted a way to be able to grab the tasks for a specific project (mainly because my team has daily standups). So I decided to build an addon in Groovy.

Making Todo.txt invoke Groovy

The snippet below shows the code for the bash script that will execute our Groovy file. I took the lately script file and adding another argument to specify the project.

    #!/bin/bash

    action=$1
    flag=$2
    project=$3
    shift

    [ "$action" = "usage" ] && {
        echo "  Recently comlpeted tasks:"
        echo "    lately+"
        echo "      generates a list of completed tasks during the last 7 days."
        echo "      Optional argument (integer) overrides number of days."
        echo "      Optional argument (String) project name."
        echo ""
        exit
    }

    [ "$action" = "lately+" ] && {
             groovy ~/.todo.actions.d/lately+.groovy "$TODO_DIR" $flag $project
    }

The lately+ Groovy script runs by parsing the argument list and then working through the done.txt file line by line to see if the completed task falls in the threshold or is from the specified project.

Colored Text with JANSI

The JANSI project allows you to use Java to print colored text and use console effects like blinking and bolding. As opposed to having to bundle a jar I used @Grab to automagically download the dependency.

    @Grapes(
        @Grab(group='org.fusesource.jansi', module='jansi', version='1.8')
    )

JANSI works well inside print and println. You print text by grabbing a static Ansi instance, setting foreground/text (fg) colors and printing text with a. You can reset to default console colors by calling reset. The following snippet prints the date in green and the task description in yellow.

    println ansi().fg(GREEN).a(date).fg(YELLOW).a(" "+restOfLine).reset()

After I got a basic script working, I started to benchmark it. Whereas the old lately command runs in 0.044-0.069 secs, the Groovy version took between 2.078s for a warm JVM and 5.918s for a cold JVM. For a command that would be run once a day, this isn't totally awful but it's not fun. So I set out see if I could get it faster.

Speeding Groovy with GroovyServ

GroovyServ is a library that runs a Groovy server in the background with a fully loaded JVM. Its groovyclient keyword is a drop-in replacement for groovy. With GroovyServ, I was able to get the run time down to 0.274s to 0.338s. You do get a hit for initial startup of the JVM but that can't be avoided if you want to use Groovy. GroovyServ pipes your content to the server and back so you lose all of the formatting from JANSI, which is a bummer. Speed doesn't come at no cost.

Full lately+.groovy file

    #!/usr/bin/env groovy
    @Grapes(
        @Grab(group='org.fusesource.jansi', module='jansi', version='1.8')
    )
    import org.fusesource.jansi.AnsiConsole
    import static org.fusesource.jansi.Ansi.*
    import static org.fusesource.jansi.Ansi.Color.*
    /*
            Enhancement of the Lately add-on
            Adds the ability to specify a project

            Arguments:
            1. TODO_DIR
            2. Number of days (optional, default is 7)
            3. Project name (optional)
    */
    def dir, days, project

    dir = this.args[0]
    //Optional
    if (this.args.size() > 1)
        days = this.args[1]
    else days = 7
    if (this.args.size() > 2)
        project = this.args[2]

    // Get Done file
    def file = new File(dir+File.separator+"done.txt")

    def today = new Date().clearTime()
    def startDate = today - new Integer(days)

    AnsiConsole.systemInstall()
    println()
    print(ansi().fg(RED).a("Closed tasks since ${startDate.format('yyyy-MM-dd')}"))
    if (project != null) 
        print( ansi().a(" for Project: ${project}").reset())
    print "

"

    file.eachLine {
        def line = it
        // Get date portion
        def date = line.substring(2,12)
        def restOfLine = line - 'x ' - date
        def d = Date.parse('yyyy-MM-dd', date)

        if (startDate <= d && project == null)
            println ansi().fg(GREEN).a(date).fg(YELLOW).a(" "+restOfLine).reset()
        if (startDate <= d && project != null && line.contains(project))
            println ansi().fg(GREEN).a(date).fg(YELLOW).a(" "+restOfLine).reset()
    }

    AnsiConsole.systemUninstall()

If you want to install lately+, just drop the two files (lately+ is the name of the bash script) into your ~/.todo.actions.d directory and provided you have Groovy on your path, you're off to the races.