One-Click DevonThink Markdown Journal Entry

With COVID-19 running wild here in Quebec we are homeschooling this year. One of the ways that the government evaluates the progress of homeschool students is by asking parents to submit a portfolio. Since I’m primarily responsible for English classes I need to have records. I already use DevonThink as a data repository, so that’s where the homeschooling records are going.

English Journal

Every time we spend some time working on English I create a journal entry as a new Markdown in Devonthink. I thought about keeping one large document and continually appending to it but several small documents seems more searchable and gives me accurate timestamps and geolocations, (if we ever travel again). The journal entries are all kept in a Homeschool > English Journal group.

Since creating a new markdown document several times a week is kind of slow I made a template, and added a button to the menu bar in Devonthink to create a new English Journal entry with one click.

Creating a Devonthink Template

As a fairly new Devonthink user this was my first foray into using templates. I assumed there would be some sort of template editor in Devonthink but there isn’t. You can create an existing document as a template or copy an existing template in Finder. There are also two kinds of templates: normal, and “smart” templates. Normal templates aren’t completely dumb – they have some placeholders that can be replaced by dynamic values, (things like Date, Time, or the user’s name). Smart templates are a bundle of files, including an AppleScript file. The AppleScript file is the main file in the template and controls everything. There can be other files in the bundle, (like a template.md file), and the AppleScript file can refer to those files. I ended up with a Smart template, with some minimal smarts.

A screenshot of the directory structure of the English Journal Devonthink template.The script in my template opens my “Home” database, makes sure the Homeschool > English Journal group exists, then creates a new markdown document in the English Journal group based on the English Journal.md file. The script is relatively simple:


-- Import helper library
tell application "Finder" to set pathToAdditions to ((path to application id "DNtp" as string) & "Contents:Resources:Template Script Additions.scpt") as alias
set helperLibrary to load script pathToAdditions

-- Get the template file path.
set theTemplateFile to helperLibrary's pathToLocalizedResources() & "English Journal.md"

tell application id "DNtp"
	
	
	-- Open the database.
	set theDatabase to open database "/Users/John/Databases/Home.dtBase2"
	-- Get a reference to the group I want.
	set theLocation to create location "/Homeschool/English Journal" in theDatabase
	
	-- Create the document based on the template file.
	set entry to import theTemplateFile to theLocation placeholders {}
	
	-- Open the new document.
	open tab for record entry
	
end tell

The markdown file is pretty simple too. %time% and %longDate% are Devonthink placeholders to put the date & time into the journal entry.

# English Activity Record
## %time% %longDate%

### Activities
- Pages ### - ### in _Toute ma 3e année_.

### Parent Reading Aloud

I may adjust it to prompt for a title for each entry, and to add my current location to the text of each entry.

One-Click Template Use

With a working template bundle, (in ~/Library/Application Support/DEVONthink 3/Templates.noindex), it was time to put a button in the Devonthink menu bar:

How to put a template in the Devonthink toolbar:

  1. Move the template bundle into ~/Library/Application Support/DEVONthink 3/Templates.noindex/Toolbar
  2. Restart Devonthink
  3. Go to View > Customize Toolbar in Devonthink
  4. Drag the “English” button to the Toolbar.

While View > Customize Toolbar is open you can choose to show the Icon and Text in the toolbar if you want.

Set an icon for the toolbar button:

It is possible to set a custom icon for the toolbar button, (by default it’s a gear). Devonthink has a weird way of setting the icon, (weird in a good way): set the icon of the template bundle in Finder. Devonthink takes whatever icon Finder thinks the template should have and puts it in the toolbar.

How to customize a file’s icon on macOS:

  1. Open the image you want to use as a custom icon, (in Preview, or wherever).
  2. Copy the image, (Command-C, or Edit > Copy).
  3. Option-click the file that will get the custom icon.
  4. Select “Get Info” from the menu.
  5. Click the file icon in the “Get Info” window so it’s highlighted.
  6. Command-V to paste the image you copied in Step 2 as the custom icon.

If you ever want to remove the custom icon to back to the Get Info window and Command-X to remove it.

The new “English” button in my Devonthink.

Mobile Entry?

This system only works on my computer. I’d like to have a mobile option but in this moment it’s not a pressing need.

OmniFocus Progress Chart Part 1: Extract & Save Data

Measured progress motivates me. I want to see how many things are in my OmniFocus library and if it’s trending larger or smaller. My desktop has been home to a list of today’s completed OmniFocus tasks for a long time, and now it’s home to a pair of charts: remaining items over the last 30 days, and items completed per day for the last 30 days.

The charts are created in 3 steps, and I am breaking these up into two posts:

  1. Extract a snapshot of the status of the database every day, (this post).
  2. Use that snapshot to create a text-based chart, (next post).
  3. Show the chart on my desktop, (next post).

Steps 1 & 2 are handled by a Javascript for Automation application written in Script Editor. Step 3 is handled by GeekTool.

Extracting & Saving the Database Status

There are a few ways to build a program that interacts with other programs on a Mac. For the completed task list I used Automator, but I don’t like editing Java- or Apple- script in the tiny Automator windows, so I tried out Script Editor. Script Editor lets us create an app bundle with somewhat modularized code, but isn’t the greatest IDE and crashes more than it should. Save your work often.

Applescript is foreign to me so I opted for Javascript for Automation, aka JXA, Apple’s, (maybe abandoned?), attempt to get a Javascript version of Applescript running. It works ok, but documentation of how it actually works, and what parts of modern Javascript are supported, is hard to find.

To extract the data we can use OmniFocus’s flattenedTasks list, which gets a flattened list of all tasks in the database, then we can filter that list of tasks by status, creation date, completion date, and so on:

// Get the "document" that we need to work with.
const ofdoc = Application('OmniFocus').defaultDocument;

// Get a list of all tasks in the DB.
const tasks = ofdoc.flattenedTasks;
// now tasks.length is the total number of tasks in the DB
// (this will change a lot when you archive old tasks)

// Filter using the .whose method.
const remainingTasks = tasks.whose({
      effectivelyCompleted: false,
      effectivelyDropped: false
});

// ... etc. for completed tasks, and tasks completed/added/dropped in the past day.

Filtering with whose is pretty slow so this can’t be run every few seconds, (it takes several seconds to run), but for now I’m only updating daily so it’s fine if it’s slow.

I tried iterating over the tasks and checking the effectivelyCompleted property on each one, thinking it would be faster, but effectivelyCompleted has to send a message to OmniFocus to get a response, and doing that for my entire library is much slower than a single .whose() call.

Once all the tasks are filtered the current stats put into a Javascript object, which is .push()‘ed onto the end of a Javascript array of all the stats and stored on disk as a JSON file.

Modularizing the code was a hurdle.

I tried using module exports & imports but they don’t work. It seems like there is an import() function available but I can’t find what it does. It’s hard to see what Javascript/ECMAScript features are supported in JXA. Some documentation says that it uses the same Javascript engine as Safari, but if that was true when it was released it doesn’t appear to be true now.

Even without exports & imports the Release Notes say we can import libraries into our script like so:

// Imports from StatsProcessor.scpt - supposedly. Also, where is StatsProcessor.scpt?
const StatsProcessor = Library("StatsProcessor");

The 10.11 Release Notes say that we can put our library scripts into the ‘Contents/Library/Script Libraries’ directory of the App we’re making. I couldn’t find a way to do this, or even see the Script Libraries folder in Script Editor, but once I moved everything around with finder the library was recognized and the import worked. But this code crashed:

const StatsProcessor = Library("StatsProcessor");
StatsProcessor.anyPublicMethod();

Not only did it crash, but it made Script Editor crash as well. Calling any method on an imported library caused a crash. Apparently we need to use a “compiled” script – a .scptd file – for libraries. This isn’t documented anywhere that I can find, and there seems to be no way to change between a .scpt and .scptd file, at least not using a GUI.

There’s also no documentation for that a Library actually is and what’s available when it’s imported. From what I can see any global function declared in the imported file is available as a method on the imported object.

Once the modularization was working it was relatively straightforward to figure out where to store the data, (answer: In ~/Library/Application Support/OmniFocusStats), and save it as desired.

Next up: Using the data to make & display a chart.

Show the OmniFocus Tasks You Did Today on your Desktop

This morning Ken Case retweeted this:

Using Automator, not TextExpander

I’ve wanted a solution to show me what I accomplished in a day for a while, but I don’t use TextExpander. Luckily Colter Reed’s script to log today’s completed OmniFocus tasks is Javascript for Automation, which can be run with the built-in OSX app, Automator, so I built an Automator workflow that runs the script, (I just pasted Colter Reed’s script into an Automator “Run Javascript” action, then made one tweak when I ran into a small problem). If you’ve never used Automator before don’t worry – I hadn’t either! Here’s my Automator workflow file, (it’ll need to be unzipped). It returns a bunch of text, which we’ll deal with next.

Put the Results on your Desktop with GeekTool

GeekTool can take text from a shell script and display it on your desktop, and you can set it to refresh that text however often you want. I created a new shell Geeklet with GeekTool with the following script:

Automator Runner ~/Developer/Scripts/omnifocus-completed.workflow | sed -e 's/^"//' -e 's/"$//' -e 's/\\"/"/g'

For some reason the Automator Runner returns the text with quotation marks around it, so the parts after the pipe remove the quotation marks, (using instructions from Stackoverflow).

When choosing your refresh interval keep in mind that the script grabs focus from OmniFocus when it runs, which will be annoying if you are working in OmniFocus and you have a short refresh interval. I also don’t know how much battery this will eat for those of us on laptops.

With some font & colour tweaking, my desktop now looks like this:

omnifocus-completed-desktop-screensho

Update (October 9, 2015): This morning a bunch of tasks I did last night, but before midnight, were still on my desktop, so I went debugging. It looks like OmniFocus, (at least my version, which is a pre-release test version), is exposing the completedDate value to Javascript as a UTC date/time, but with a Timezone offset set, so things done in the evening might remain on the list of “today’s” completed tasks, (this might reverse on the other side of the world, with things you do in the morning not appearing). To fix this I’ve modified the startOfDay() function to take the Timezone Offset into account. I assume this is a bug and will be temporary, so I’m not updating the downloadable Automator action above. Here’s the updated startOfDay() function:


function startOfDay() {
// The day started at midnight this morning
    var d = new Date(),
    hours = 0,
    minutes = 0;
    
    if( d.getTimezoneOffset() !== 0 ) {
        hours = Math.round( d.getTimezoneOffset() / 60 );
        minutes = d.getTimezoneOffset() % 60;
    }
    
    d.setHours( hours );
    d.setMinutes( minutes );
    d.setSeconds( 0 );
    
    return d;
}

Update (November 3, 2015): I used quotation marks in a task name, and they came out escaped on my desktop. I’ve updated the command line for the geeklet to strip the slashes from before double quotation marks;