🔨 Hand-crafting a todo widget for Obsidian

TL;DR

Noob tries to build task widget without internet access, fails miserably. Code:

let tasks = await dv.tryQuery('TASK from "" where !completed', "", "")
let grouped = dv.array(tasks.values).groupBy((t) => Link.file(t.path))

grouped.forEach((t) => {
  t.rows = t.rows.groupBy((tt) => {
    return tt.section.withDisplay(tt.section.subpath)
  })
})

// Download

dv.taskList(grouped, false)
let mdlist = dv.markdownTaskList(grouped)

const regex = /\[\[.*\|(.+)\]\]/gm
const subst = `$1`
mdlist = mdlist.replace(regex, subst)

let path = "../"+this.app.vault.getName()+"-tasks.md"
this.app.vault.adapter.write(path, mdlist)

Hello all! This is my first post ever in my community. I want to share with you the experience of creating a task list widget from Obsidian, and lessons learned along the way.

Prelude

As an avid notion user, I’ve found ObsidianMD a few days ago and instantly fell in love with it. This love exploded exponentially once I found DataView and the possibilities it offered. Mostly peculiar was the idea of dataview-js: it seems so innovative, yet so logical: JavaScript is an interpreted language, then why couldn’t you just slap code inside an eval() and get something like a Jupiter notebook, but on steroids AND cross-platform? Makes a ton of sense. Anyway.

I quickly began experimenting with various ideas, a basic one being a two-level grouped task list:

let tasks = await dv.tryQuery('TASK from "" where !completed', "", "")
let grouped = dv.array(tasks.values).groupBy((t) => Link.file(t.path))

grouped.forEach((t) => {
  t.rows = t.rows.groupBy((tt) => {
    return tt.section.withDisplay(tt.section.subpath)
  })
})

dv.taskList(grouped, false)

Then, the thought came: I have this pretty view, I want to put it on my home screen now!

The problems

The most of this adventure took place while travelling, without any mobile data, internet access [1] or previous knowledge of DataView/Obsidian plugins. It was kinda fun to have to experiment, only debugging with endless alert() chains (is there a better way to do this, btw?)

I looked at the usual places for the widget: just long pressing the Android home screen, maybe some hidden setting? I found nothing, at the end. But this problem didn’t let me sleep; and I figured that there’s definitely an app that can display a file as a widget. Turns out, there is.

Writing a text file from dataviewjs

So now the quest became: how could I export a DataView list as a simple Markdown file? I am not aware of any way to simply ask the Obsidian API to render a full file, then pass it to the widget so I had to go down this way.

I found this community topic from 2019 that asked the exact same question, and got an answer. In rejoice, I tried the code and … nothing happened. It was weird, because if I changed the URL to https://google.com, it did work just fine.

So what went wrong? I copy-pasted the output to Chrome, and voila, there was the error message. For safety reasons, I presume(?), data://, market:// and such links are no longer allowed on Android.

The solution to writing the file ended up being a call to app.vault.create(). Cool, I can now create and edit text files!

Gathering the task list

I wasn’t aware of how DataView worked internally, and didn’t know how to actually get the data contained in the task list. I started writing a recursive function that would display the contents, but it didn’t work really well. Then, I realized DataView does have a function for exactly this: markdownTaskList().

The weird refresh problem

I threw together some code based on the previous findings, and was very happy with how it turned out. But then the problem arise. You see, for some reason, the task list was repainting without any user interaction every 2500 ms (i oughta know, I measured it). It wouldn’t have been a big deal if it kept the previous contents and then rendered over it, but there was a ⅓-second flicker every time it did it, and that was very annoying. Digging around the DataView settings, I found the solution. This wasn’t a bug, but a feature: the extension rebuilds lists if a file in it’s vault changed – and me writing a file created a loop:

Mermaid code of this"

graph TD;
    A(DataView builds the list)-->B(Code writes the file);
    B-->C(Obsidian emits file change event);

C --> A;

FYI, excluding the created file from the task query with -"tasks.md" doesn’t work either

Writing outside of the vault

My idea to fix this problem was just simply not to write the file in the vault, but the directory outside it. This raised another issue: the previous logic was to try to create a file, and if an error occurs (the file is already there), modify it. It turns out, however, that app.vault.getAbstractFileByPath() doesn’t understand paths outside the vault. This can be circumvented by using app.vault.adapter.write() instead, which seems to call system APIs under the hood, and generally not to care. I chose the file name [Vault name]-tasks.md but you can modify that to your liking.

Regex magic

The results so far are promising, but it kept bugging my eyes that the section headings were represented by Obsidian links (eg [[hello/world/file/path|path]]), so I wrote a simple function to use the display name, if given, and the last segment of the path if not:

function getLinkpath(p){
    if(p.display){
        return p.display
    }
    
    const regex = /\[\[.*\|(.+)\]\]/gm
    const subst = `$1`
    p = p.path.replace(regex, subst).split("/").last()
    return p
}

Putting it all together (the finished code)

let tasks = await dv.tryQuery('TASK from "" where !completed', "", "")
let grouped = dv.array(tasks.values).groupBy((t) => Link.file(t.path))

grouped.forEach((t) => {
  t.rows = t.rows.groupBy((tt) => {
    return tt.section.withDisplay(tt.section.subpath)
  })
})

// Download

dv.taskList(grouped, false)
let mdlist = dv.markdownTaskList(grouped)

const regex = /\[\[.*\|(.+)\]\]/gm
const subst = `$1`
mdlist = mdlist.replace(regex, subst)

let path = "../"+this.app.vault.getName()+"-tasks.md"
this.app.vault.adapter.write(path, mdlist)

What’s next?

I would love to have the widget render actual markdown, with headers, bold text and such, instead of just plain text, but I didn’t find any app to do this.
I may write a simple one that leverages some popular markdown render implementation. Another good feature would be launching Obsidian when clicking on tasks or the widget itself – that could also be solved by this.

Thank you for reading!!

Written with :heart: in Obsidian


Notes

[1] I did have a few moments of internet access with public WiFi hotspots. That’s when I read the post about exporting DataView tables, for example. Nevertheless, I did all the coding offline.]

3 Likes