Opening files or directories in TiddlyWiki revisited

The following applies to the Node.js edition of TiddlyWiki. Tested on TUXEDO OS (Ubuntu based OS).

I managed to get links to my files inside TiddlyWiki to be opened by using the following procedure.

Highlights of requirements:

  • A script
  • A .desktop entry
  • A file association
  • A macro

It appears to be a lot, but once you set it up it doesn’t require any maintenance!

Provenance

This text documents the creation of a custom twfile:// URL scheme handler for TiddlyWiki asset links under Linux (KDE Plasma). The goal was to allow links like twfile:///home/user/Downloads/example.txt to open directly in the appropriate application.

Bash Script

The handler script lives in ~/scripts/tiddlywiki_file_opener.sh and is marked executable (chmod +x). It strips the scheme, decodes URL-encoded characters, and opens files or directories with the default system handler.

Script: See below.

File Association

  1. In KDE System Settings → Applications → File Associations, add a new type: * Type: x-scheme-handler/twfile
    Name: TiddlyWiki File Opener (friendly label)

  2. Under the Applications tab, set TiddlyWiki File Opener as the preferred application.

  3. Refresh caches if needed:

    update-desktop-database ~/.local/share/applications
    kbuildsycoca5 --noincremental

Verification

Check the association:

gio mime x-scheme-handler/twfile

Expected output:

Default application for “x-scheme-handler/twfile”: tiddlywiki-file-opener.desktop
Registered applications:
        tiddlywiki-file-opener.desktop
Recommended applications:
        tiddlywiki-file-opener.desktop

Testing

xdg-open "twfile:///home/user/Downloads/icedrive.txt"

  • Files open in their default app.
  • Directories open in the file manager.
  • Invalid paths trigger a notification and fall back to ~/Documents.

Notes

This setup is cross-desktop (KDE, GNOME, etc.) since it uses the XDG MIME system.

The tiddlywiki_file_opener.sh is:

#!/bin/bash
# twfile:// protocol handler (robust normalization)

URL="$1"
LOGFILE="/tmp/twfile.log"
SAFE_FALLBACK="$HOME/Documents"

echo "Incoming URL: $URL" >> "$LOGFILE"

# 1) Strip scheme (handles twfile:/ and twfile://)
SCHEME_STRIPPED="${URL#twfile:}"

# 2) Parse optional //host/path form
#    - If host is empty or 'localhost', treat as local.
#    - If host is non-empty (e.g., 'home'), convert to '/home/...'.
if [[ "$SCHEME_STRIPPED" =~ ^//([^/]*)(/.*)?$ ]]; then
  HOST="${BASH_REMATCH[1]}"
  PATH_PART="${BASH_REMATCH[2]}"

  # Empty PATH_PART means root
  [[ -z "$PATH_PART" ]] && PATH_PART="/"

  if [[ -z "$HOST" || "$HOST" == "localhost" ]]; then
    FILE_PATH="$PATH_PART"
  else
    FILE_PATH="/$HOST$PATH_PART"
  fi
else
  # No host form; just use the remainder as path
  FILE_PATH="$SCHEME_STRIPPED"
fi

# 3) Ensure leading slash for local absolute paths
[[ "$FILE_PATH" != /* ]] && FILE_PATH="/$FILE_PATH"

# 4) Collapse multiple slashes (///home -> /home)
FILE_PATH="$(echo "$FILE_PATH" | sed -E 's|/{2,}|/|g')"

# 5) Percent-decode (%20 -> space, etc.)
FILE_PATH="$(printf '%b' "${FILE_PATH//%/\\x}")"

echo "Normalized path: $FILE_PATH" >> "$LOGFILE"

# 6) Open directory or file; fallback if missing
if [ -d "$FILE_PATH" ]; then
  xdg-open "$FILE_PATH"
  echo "Opened directory: $FILE_PATH" >> "$LOGFILE"
elif [ -f "$FILE_PATH" ]; then
  xdg-open "$FILE_PATH"
  echo "Opened file: $FILE_PATH" >> "$LOGFILE"
else
  notify-send "TiddlyWiki File Opener" \
              "Invalid path: $FILE_PATH — opening fallback: $SAFE_FALLBACK"
  echo "Invalid path: $FILE_PATH — fallback to $SAFE_FALLBACK" >> "$LOGFILE"
  xdg-open "$SAFE_FALLBACK"
fi


Global macro

Steps

  1. Create the macro tiddler
  • Title: $:/macros/twfile
  • Type: text/vnd.tiddlywiki
  • Tag: $:/tags/Global
  1. Macro definition
\define twfile(path, label:"$path$")
<html><a href="twfile:///$path$">$label$</a></html>
\end

Important: Tag it $:/tags/Global

  1. Usage

Call the macro in any other tiddler:

Example:

<<twfile "/home/user/Downloads/example.txt" "example.txt">>

If you omit the label, it defaults to the path:

<<twfile "/home/example/Downloads/example.txt">>
  1. Behaviour
  • Renders as a clickable link labelled example.txt.
  • Points to twfile:///home/user/Downloads/example.txt, which is handled by the system protocol handler.

Lessons Learned

  • Macros defined with \define only become global if the tiddler is tagged $:/tags/Macro.
  • The defining tiddler won’t render its own macro; calls must be in other tiddlers. <== This is wrong. See pmario’s comment about it.
  • Wrapping the <a> in <html>…</html> ensures the parser doesn’t strip the custom scheme. <== This is wrong. See pmario’s comment about it.
3 Likes

Very good @wikster,

  • I note your examples are all below downloads, is this due to restictions?

I would of course love something similar in windows, and belive it would be possible. I did something similar with tiddlywiki files themself giving them a .tw extencion rather than .html allowing;

  • to tell them appart and search for them on disk
  • Double click to open in the prefered browser
  • Email the wikis to use them as smart documents

The browser was quite happy to discover .tw file were html and render them.

I think taking ownership of such protocols and field extensions to support tiddlywiki use outside the browser but for consumption inside the browser should be done and part of the projects core toolset. This includes Timimi for saving, and if nessasary host installed intermediatries to access any file/folder

  • Yes I know there will be variations accross operating systems and browsers, but hey Tiddlywiki can handle it.

You could be writting procedures, and also use the global tags SystemTag: $:/tags/Global
SystemTag: $:/tags/Global/View
and SystemTag: $:/tags/Global/View/Body

I am sure we could fix this, but is it needed?

Interesting, perhaps another approach is backticked ?

Thank you for sharing your interesting thoughts. The Downloads directory is used by my browser (Vivaldi) because it’s where I defined where downloads from Internet links should go.

thanks for posting

iv had similar ( proto-handler) method
to this under my hat / @ the back of my mind
for a while tbh
… make me wander if similar proto technique might have potential to do more** than just ‘open files/dirs’ ?

For anyone interested, I created a new macro that replaces the previous one as follows:

\define twfile(path, label:"//path//")
<$let tip="$path$">
<a href="twfile:///$path$" title=<<tip>> class="twfile-link-base">$label$</a>
</$let>
\end

\define twfile-styled(path, label:"//path//")
<$let tip="$path$">
<a href="twfile:///$path$" title=<<tip>> class="twfile-link-base twfile-highlight">$label$</a>
</$let>
\end

\define twfile-pill(path, label:"//path//")
<$let tip="$path$">
<a href="twfile:///$path$" title=<<tip>> class="twfile-link-base twfile-pill">$label$</a>
</$let>
\end

\define twfile-folder(path, label:"//path//")
<$let tip="$path$">
<a href="twfile:///$path$" title=<<tip>> class="twfile-link-base twfile-folder">$label$</a>
</$let>
\end

\define twfile-alert(path, label:"//path//")
<$let tip="$path$">
<a href="twfile:///$path$" title=<<tip>> class="twfile-link-base twfile-alert">$label$</a>
</$let>
\end

\define twfile-dotted(path, label:"//path//")
<$let tip="$path$">
<a href="twfile:///$path$" title=<<tip>> class="twfile-link-base twfile-dotted">$label$</a>
</$let>
\end

I also created a stylesheet:

/* — BASE CLASS (Unstyled Default) — */
.twfile-link-base {
    color: #4d93ff;
    text-decoration: underline;
    cursor: pointer;
    font-weight: normal;
}/* — 1. HIGHLIGHT CLASS — */
.twfile-highlight {
    background-color: #ffffe0;
    padding: 1px 3px;
    border-radius: 3px;
    color: #444444;
    text-decoration: none;
    font-weight: bold;
}/* — 2. PILL/TAG CLASS — */
.twfile-pill {
    border: 1px solid #005f99; /* Blue border */
    color: #005f99; /* Blue text */
    padding: 0 5px;
    border-radius: 12px; /* Makes the corners very round */
    text-decoration: none;
    font-size: 0.9em;
    cursor: pointer;
    display: inline-block;
}/* — 3. FOLDER CLASS — */
.twfile-folder {
    /* Subtle background and border */
    background: #e9e9e9;
    border: 1px solid #c0c0c0;
    border-bottom: 2px solid #a0a0a0; 
    
    color: #333333 !important; /* Dark gray text */
    padding: 2px 5px;
    border-radius: 4px;
    text-decoration: none;
    cursor: pointer;
    display: inline-block;
}/* — 4. ALERT CLASS — */
.twfile-alert {
    background-color: #ffcccc; /* Light Red/Pink */
    border: 1px solid #cc0000; /* Dark Red border */
    color: #cc0000; /* Dark Red text */
    padding: 1px 4px;
    border-radius: 2px;
    font-weight: bold;
    text-decoration: none;
    cursor: pointer;
    display: inline-block;
}/* — 5. DOTTED LINK CLASS — */
.twfile-dotted {
    font-weight: bold;
    color: #444444; /* Dark gray text */
    border-bottom: 2px dotted #808080; /* Dotted gray line */
    text-decoration: none; /* Removes default underline */
    cursor: pointer;
}

With these changes the linking to external files is more evident and more flexible.

There’s probably a way to simplify the macro, but the AI that I consulted couldn’t do it! :laughing:

USAGE for style selections:

Usage for Style Selection:

  • Default Link: <<twfile "path" "Label">>
  • Highlight Style: <<twfile-styled "path" "Label">>
  • Pill Style: <<twfile-pill "path" "Label">>
    etc.

EDIT: Apologies for saying that I did the above. It’s misleading. The AI did it all. What I did was to do a copy + paste. :wink:

EDIT 2: No need to use !important in the Stylesheet. Edited accordingly. See pmario’s comments below.

My Ai shortened the macro as shown below. Don’t try this at home! It took me all morning to get it work. :grinning_face_with_smiling_eyes:

\define twfile-base(path, label:"//path//", style:"")
<a href="twfile:///$path$" title="$path$" class="twfile-link-base $style$">$label$</a>
\end

\define twfile(path, label:"//path//") <<twfile-base "$path$" "$label$" "">>
\define twfile-pill(path, label) <<twfile-base "$path$" "$label$" "twfile-pill">>
\define twfile-folder(path, label) <<twfile-base "$path$" "$label$" "twfile-folder">>
\define twfile-dotted(path, label) <<twfile-base "$path$" "$label$" "twfile-dotted">>
\define twfile-alert(path, label) <<twfile-base "$path$" "$label$" "twfile-alert">>
\define twfile-highlight(path, label) <<twfile-base "$path$" "$label$" "twfile-highlight">>

EDITED to fix the syntax which was wrong.

Please do not blindly copy / paste AI created code here. Most of the time it’s wrong.
!important in a stylesheet is not needed and causes problems in the long run. It should still work, if you remove it. See: Why You Should Avoid Using !important in CSS

Those styles will not work properly, if the palettes are switched.

The OP and the other posts are interesting.

1 Like

That’s wrong. See: <html>: The HTML Document / Root element - HTML | MDN

The <html> HTML element represents the root (top-level element) of an HTML document, so it is also referred to as the root element. All other elements must be descendants of this element. There can be only one <html> element in a document.

The HTML element is a root element of a wiki. It does not make sense to cover an A element inside an HTML element.

That’s also not true. The following code works just fine in the same tiddler.

What is true: If procedures or macros should be used globally, they need to be tagged $:/tags/Global or (old) “$:/tags/Macro” – See: Procedure Definitions or Macro definitions

\procedure hello() Hello There!

<<hello>>

Please fix your code and the text.

Thank you for your comments and corrections.

There are a lot of features like this that I think would be possible to program into Tiddly Desktop, which uses nw.js.

Can any meaningful lessons be learned from this by people knowledgeable about tiddlywiki to create a standard solution in tiddlywiki script?

  • Clearly as there are external requirements can they at least be described in a general way that similar can be achived on other OS’s?

Otherwise I dont think it belongs in the Tips & Tricks area as it is more a exploration

@pmario kindly pointed out a number of mistakes in what AI had suggested to me. I finally changed the macro to a procedure and it’s working, but I’ll not post anything here about it. As it has rightly been mentioned AI can make mistakes and there’s a risk in propagating them.

Perhaps in future I could describe a possible solution for something in general terms and leave it up to more knowledgeable persons to mold it into a working solution.

it appears so

.learn.microsoft.com/en-us/windows/win32/search/-search-3x-wds-ph-install-registration

.https://stackoverflow.com/questions/1947209/registering-a-url-protocol-handler-in-a-multiple-platforms

.Build your own custom protocol handler for MacOS | jw bargsten

.URL protocol handler registration for PWAs  |  Web Platform  |  Chrome for Developers

1 Like

I moved this to the Discussion topic as its incomplete

Totaly agree, we are a helpful buch here and get it right much more often than LLM’s

I think the only place for LLM content here is once you have a working result, even if the LLM helped you get there. Always mention involvement of an LLM, it helps people be on gard for probelms that are very common.

  • LLM’s can come up with new and even good innovations but it must be guided carfully

See past comments of mine and others AI Generated Content on talk.tiddlywiki.org - #18 by TW_Tones

i thunk the point hear

is dont blindly copy
from ~unreliable-narrators~
to talk.tiddlywiki.org
(id guess*)
if you think whatever

and that is because you tested your assumptions
presumably in a wiki
that would constitute some insight in to whatever
and not be considered blindly copying & pasteing

*imho

If you tested it and it works, you can post it. – Tested stuff is very welcome by future readers of the thread. It may directly help them or give them new ideas for their own tests.

What should not be posted is untested AI output

1 Like

Here’s a tested procedure that works on my setup (TUXEDO OS), TiddlyWiki Node.js edition. A custom twfile:// URL scheme handler for TiddlyWiki asset links under Linux (KDE Plasma) was created as described earlier.

\procedure twfile-base(path, label:"//path//", style:"")
<$set name="finalUrl" value={{{ [<path>addprefix[twfile://]] }}}>
<$set name="finalClass" value={{{ [<style>addprefix[twfile-link-base ]trim[]] }}}>
<a href=<<finalUrl>> title=<<path>> class=<<finalClass>>>
  <$text text=<<label>>/>
</a>
</$set>
</$set>
\end

\procedure twfile-style(style, path, label)
<$macrocall $name="twfile-base" path=<<path>> label=<<label>> style=<<style>>/>
\end

\procedure twfile(path, label)
<$macrocall $name="twfile-style" style="" path=<<path>> label=<<label>>/>
\end

\procedure twfile-pill(path, label)
<$macrocall $name="twfile-style" style="twfile-pill" path=<<path>> label=<<label>>/>
\end

\procedure twfile-folder(path, label)
<$macrocall $name="twfile-style" style="twfile-folder" path=<<path>> label=<<label>>/>
\end

\procedure twfile-dotted(path, label)
<$macrocall $name="twfile-style" style="twfile-dotted" path=<<path>> label=<<label>>/>
\end

\procedure twfile-alert(path, label)
<$macrocall $name="twfile-style" style="twfile-alert" path=<<path>> label=<<label>>/>
\end

\procedure twfile-highlight(path, label)
<$macrocall $name="twfile-style" style="twfile-highlight" path=<<path>> label=<<label>>/>
\end

I can affirm that it works on my system, but with no guarantee that it will work for everyone else.

Did you create the CSS for this. I expect it is needed to be complete?

thanks for sharing, as it will come up in future searches.

Sometime in the future, as your skills develop, I think you may refine this tiddlywiki script down into a single function

I’m concerned that as @pmario rightly cautioned, wrong code could end up in search engines and this would be undesirable. Maybe the Forum could have a dedicated space where untested, unverified or otherwise possibly incorrect code could be posted, but that space would not be made accessible by search engines.

The CSS was built gradually by AI. I used ChatGPT initially and later added refinements using Copilot.

why ???

im not shore i understand what advantage (you think) would that bring to forum users ?