Globally register functions in Obsidian

Published on:2024/12/15
Banner for Globally register functions in Obsidian

Introduction

I have recently been trying the user scripts in templater.

And while QuickAdd was my current solution for building "layers of scripts" (understand the ability to configure things mostly relying on version-controlled and trackable code versus plugins, settings and nested menus) as described before, I surprisingly stumbled into a much better solution.

Requirements

What we're hopig to achieve in such a system ideally:

  • describe functions atomically in different files
  • being able to use those modules in another module to reuse as much code as possible
  • being able to easily call those functions remotely (see Obsidian I/O)
  • programmatically register those functions (the presence of the file or a function being exported should be enough)

Implementation

So How does this work?
Along with all the tp function from the Templater API, all the CommonJS module exported in a templater dedicated script folder are registered and can be called with tp.user.myJSfile() .

For example: the content of scripts/myJSfile.js:

function functionName () {
    console.log('I execute from that file')
}
module.exports = functionName;

You can pass arguments, return values etc, it all works.

It works, but with one big limit: the tp object is not accesible from the context of that module.
We can't access the rest of the templater api, which could be fine if that wasn't the place where we register all of our functions.
So, let's pass the tp context into the module then.

Accessing tp from the CommonJs modules

Let's take a second file scripts/second.js with this content.

function functionName (tp) {
    tp.user.myJSfile() 
    console.log('I execute from the second file')
}
module.exports = functionName;

Executing that second function will output both console logs.
And so, in a templater's template we can call it such as:

<% tp.user.second(tp) %>

Where tp is globally available in the context of a Templater template

Remote execution

The same logic has to be applied in other contexts:

let tp = app.plugins.plugins["templater-obsidian"].templater
tp.user.myModule(tp, someOtherArgument)

From bash for instance, using Rofi for user input


title=$(rofi -dmenu -p "Enter a title:") # set title using rofi

js_code="let tp  = app.plugins.plugins[\"templater-obsidian\"].templater.current_functions_object; app.plugins.plugins[\"templater-obsidian\"].templater.current_functions_object.user.myModule(tp, \"$title\")" # Generic bit of bash encoded js code to repeat the logic stated right before

encoded_js_code=$(printf '%s' "$js_code" | jq -sRr @uri) # Making it URI friendly
uri="obsidian://advanced-uri?eval=${encoded_js_code}" # Putting it together with Advanced URI
obsidian "$uri" # calling it with obsidian

Layering inside and outside of Obsidian

The idea being to rewrite as little code as possible, you could split functions in such a wayt that they can, with minimal work, be called from either Obsidian or the rest of the OS.
For instance:

function titleSetter(tp) {
    let title = await tp.system.prompt("Enter a title:")
    tp.user.somethingElse(title)
}

And

function titleUser(tp, title) {
    console.log('here I do something with the title which is: ' + title)
}

With 2 methods of orchestration:

  • One similar to the previous bash example where the title is set with Rofi and passed to Obsidian
  • One in JS only
function titleOrchestrator(tp) {
    let title = tp.user.titleSetter(tp) ;
    tp.user.titleUser(tp, title) ;
}

Which is a bit longer but allows to modify the titleUser() function to update the behavior inside and "outside" of Obsidian at the same time.

Return values

Let's create a module that write to file the return value of the last module.

function returnValueToFile(queue, value) {
    const fs = require('fs');
    const path = require('path');
    
    const vaultPath = app.vault.adapter.basePath;
    const filePath = path.join(vaultPath, '_tmp', queue);
    
    fs.mkdirSync(path.dirname(filePath), { recursive: true }); //mkdir -p filePath
    fs.writeFileSync(filePath, value, 'utf8'); // > filePath
return 

So that in your orchestration module you can do:

let myvalue = tp.user.func2(tp.user.func1("some value here"))
tp.user.returnValueToFile('myExpectedFile', myvalue)

So that outside of bash you could do something like that: (say if you were creating a new file and wanting to open in neovim to keep it simple)

cd /my/vault/path
read -r content < "_tmp/myExpectedFile" 
nvim $(find . -name $content.md -print -quit)

That said, for such a simple example, clipboard could be enough

Conclusion

Let's go over our requirements first

  • Describe functions atomically in different files
    • That's a pass
  • Being able to use those modules in another module to reuse as much code as possible
    • Passing around the tp context is not the worst thing and not disimilar to QuickAdd's way of doing it, albeit simpler and more efficient.
  • Being able to easily call those functions remotely (see Obsidian I/O)
    • Tiny hurdle due ot having to pass the tp context, but that's also a pass here
  • Programmatically register those functions (the presence of the file or a function being exported should be enough)
    • That's a pass, creating a file and exporting a commongJS module is enough

This is overall a great find that Templater should advertise more actively (well, if that is the desired behavior).