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.
- Passing around the
- 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
- Tiny hurdle due ot having to pass the
- 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).