Offer a more complex plug-in UI with embedded HTML files
Problem: app.prompt()
only works for single-line text input. Using the prompt to create long-form text doesn't work well because the app 'freezes' while the prompt is displayed, so you can't use your notes to assemble the text.
From my perspective as the app's developer, writing a tool for you to create a custom user interface would be a lot of work and very error-prone.
Solution: Instead, we can leverage the web browser to render complex forms with well-known standard form elements, and pass the data to plug-ins via the thearchive://
URL scheme.
How the URL scheme works
The URL scheme has this form:
thearchive://plugin/PLUGIN_ID/run?var1=value1&var2=value2
PLUGIN_ID
is a placeholder for your plug-in's ID.?var1=value1&var2=value2
is the query parameter of a URL; it's optional.
The query parameter list will be passed to the plug-in during execution via the new (as of b257 2024-11-16) app.env
object. In your code, you could access these two query parameters as:
const var1 = app.env["var1"]; const var2 = app.env["var2"];
⚠️ Warning: Plug-ins can always also be started from the app, so
app.env
will be empty. You need to write your plug-ins to be as defensive as possible and ideally offer either hard-coded default values or prompt the user for input.
Plug-in code: Creating a new file with content
The following plug-in main.js
expects two query parameters, filename
and content
:
let filename = app.env["filename"]; let content = app.env["content"]; if (!filename) cancel("query parameter `filename' missing"); if (!content) cancel("query parameter `content' missing"); output.changeFile.filename = filename; output.changeFile.content = content;
Its manifest does not require any input, but it will produce an output to change a file with a dynamic filename; the manifest.json
looks like this:
{ "appVersion" : "1.8.0", "authors" : [ { "name" : "Christian Tietze" } ], "dependencies" : [ ], "description" : "", "identifier" : "de.zettelkasten.url-demo", "input" : { "notes" : [], "pasteboard" : false, "text" : [] }, "output" : { "changeFile" : { "programmaticFilename" : true }, "onCompletion" : "showFile" }, "releaseDate" : "2024-11-15", "title" : "URL Demo", "version" : "1.0.0" }
Once enabled, it can run via e.g.:
thearchive://plugin/de.zettelkasten.url-demo/run?filename=New%20File&content=Wow
that would create New File.md
with the content "Wow
".
These URL's are not intended to be assembled manually, though. This approach shines when combined with automation tools.
HTML form example and template
This basic HTML form contains a filename and content text box, and will invoke thearchive://plugin/de.zettelkasten.url-demo/run
to run my test plug-in with the ID "de.zettelkasten.url-demo"
, passing the filename
and content
variables as query parameters:
<form action="thearchive://plugin/de.zettelkasten.url-demo/run" method="GET"> <label for="filename">Filename:</label> <input id="filename" name="filename" type="text" value="" required/><br> <label for="content">Content:</label> <textarea cols="30" id="content" name="content" rows="10" required></textarea><br> <input type="submit" value="Create"/> </form> <script type="text/javascript"> let form = document.querySelector("form"); form.addEventListener("formdata", (e) => { const formData = e.formData; for (let [key, value] of formData.entries()) { formData.set(key, encodeURIComponent(value)); } }); </script>
Sadly, we need to include some JavaScript in the otherwise very simple HTML file to encode spaces in text not as +
but %20
. Read this for details if you're interested: https://christiantietze.de/posts/2024/11/percent-encode-html-form-values-spaces/
With this setup, the result of submitting the form will be a URL scheme request like so, appending the variables as query parameters:
thearchive://plugin/de.zettelkasten.url-demo/run?filename=The%20Filename&content=Wow
All ugly pieces in action:
Embedding HTML files in the bundle
You can add anything to a .thearchiveplugin
bundle (aka directory). Anything but main.js
and manifest.json
will be irrelevant when it comes to executing plug-ins, though.
But you can bundle HTML files, and then link to them from your plug-in description.
A relative file links in the Markdown description would look like so:
"description" : "[**Show form**](./form.html) (open the HTML file in your browser)",
... and render as links relative to the plug-in bundle's folder (see tooltip in the picture):
Full HTML example with some basic styling
Here's the full form.html
file from my test plug-in with some minimal CSS. It renders like so:
<html> <head> <meta charset="UTF-8"/> <title>Form</title> <style> html { /* via <https://modernfontstacks.com/> */ font-family: Seravek, 'Gill Sans Nova', Ubuntu, Calibri, 'DejaVu Sans', source-sans-pro, sans-serif; font-weight: normal; font-size: clamp(18px, 2vw, 32px); line-height: 1.6; height: 100vh; } body { margin: 0; padding: 0; height: 100vh; display: flex; align-items: center; justify-content: center; } section { padding: 1rem; border-radius: 0.5rem; background: #eee; max-width: 40ch; } form { display: flex; flex-direction: column; } input, textarea { font: inherit; font-size: inherit; border: 1px solid #444; } input[type="submit"] { background: #444; color: #fff; } </style> </head> <body> <section> <form action="thearchive-dev://plugin/de.zettelkasten.url-demo/run" method="GET"> <label for="filename">Filename:</label> <input id="filename" name="filename" type="text" value="" required/><br> <label for="content">Content:</label> <textarea cols="30" id="content" name="content" rows="10" required></textarea><br> <input type="submit" value="Create"/> </form> </section> <script type="text/javascript"> let form = document.querySelector("form"); form.addEventListener("formdata", (e) => { const formData = e.formData; for (let [key, value] of formData.entries()) { formData.set(key, encodeURIComponent(value)); } }); </script> </body> </html>
Author at Zettelkasten.de • https://christiantietze.de/
Howdy, Stranger!
Comments
Where to go from here
Well, that's up to you But it's possible to write HTML forms to collect data, e.g. for daily notes for "quantified self"-style sleep quality tracking using a slider (range input) component.
Or checkboxes and radio buttons.
You can write a flexible HTML dashboard with multiple forms (think: templates) and run them all through the same minimal plug-in that mostly only needs to deal with processing the query parameters from the
app.env
collection.Author at Zettelkasten.de • https://christiantietze.de/
I'm going to try this out. Likely, I misunderstand. I have a couple of questions.
Will Simpson
My zettelkasten is for my ideas, not the ideas of others. I don’t want to waste my time tinkering with my ZK; I’d rather dive into the work itself. My peak cognition is behind me. One day soon, I will read my last book, write my last note, eat my last meal, and kiss my sweetie for the last time.
kestrelcreek.com
Yeah, currently users need to go to the form manually at least once (and then e.g. copy it somewhere else, create an alias on the desktop, bookmark it, etc.)
Would be nicer than the current hack, yes. I also tried creating an HTML form in the ./media folder and link to that. While this also works, it requires a note with a link, not much better.
With future updates, plug-ins will have access to other files in the bundle (e.g. to embed library code). When accessing another JS file safely works, I'll also revisit offering access to other bundle resources like images, HTML files, and such.
Not yet, though! First release is hacky release
Not sure; can you give a (made up) example?
Author at Zettelkasten.de • https://christiantietze.de/
An idea that comes to mind to ease opening "attachments" like HTML files from the outside:
Given that
thearchive://plugin/PLUGIN_ID/run
is used to execute the plug-in itself, the URL scheme can offer a different action to/run
, like/open
, and then opening the form can becomethearchive://plugin/PLUGIN_ID/open/form.html
. Similar with other attachments.That doesn't help to bring up the HTML file from running the plug-in, yet, though.
How one could go about this later: introduce branching execution paths. Show the form when the plug-in is executed directly (i.e. no form data is known from the plug-ins side).
Author at Zettelkasten.de • https://christiantietze.de/
I envision this functionality to be useful in gathering multiple variables at plugin execution.
1. Variables in the form of selections from a list of templates for new notes.
2. Multiple checkboxes confirming or denying various run-time options.
3. In-depth help.
I envision this functionality to be useful in outputting data from The Archive.
1. ZK Stats in a pretty format.
2. Convert a note to a blog post.
3. Print a note.
4. Unify several search queries into a single result.
Will Simpson
My zettelkasten is for my ideas, not the ideas of others. I don’t want to waste my time tinkering with my ZK; I’d rather dive into the work itself. My peak cognition is behind me. One day soon, I will read my last book, write my last note, eat my last meal, and kiss my sweetie for the last time.
kestrelcreek.com