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/Macro
  1. Macro definition
\define twfile(path, label:"$path$")
<html><a href="twfile:///$path$">$label$</a></html>
\end

Important: Tag it $:/tags/Macro

  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.
  • Wrapping the <a> in <html>…</html> ensures the parser doesn’t strip the custom scheme.
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 !important;
    text-decoration: underline !important;
    cursor: pointer !important;
    font-weight: normal !important;
}/* — 1. HIGHLIGHT CLASS — */
.twfile-highlight {
    background-color: #ffffe0 !important;
    padding: 1px 3px !important;
    border-radius: 3px !important;
    color: #444444 !important;
    text-decoration: none !important;
    font-weight: bold !important;
}/* — 2. PILL/TAG CLASS — */
.twfile-pill {
    border: 1px solid #005f99 !important; /* Blue border */
    color: #005f99 !important; /* Blue text */
    padding: 0 5px !important;
    border-radius: 12px !important; /* Makes the corners very round */
    text-decoration: none !important;
    font-size: 0.9em !important;
    cursor: pointer !important;
    display: inline-block;
}/* — 3. FOLDER CLASS — */
.twfile-folder {
    /* Subtle background and border */
    background: #e9e9e9 !important;
    border: 1px solid #c0c0c0 !important;
    border-bottom: 2px solid #a0a0a0 !important; 
    
    color: #333333 !important; /* Dark gray text */
    padding: 2px 5px !important;
    border-radius: 4px !important;
    text-decoration: none !important;
    cursor: pointer !important;
    display: inline-block;
}/* — 4. ALERT CLASS — */
.twfile-alert {
    background-color: #ffcccc !important; /* Light Red/Pink */
    border: 1px solid #cc0000 !important; /* Dark Red border */
    color: #cc0000 !important; /* Dark Red text */
    padding: 1px 4px !important;
    border-radius: 2px !important;
    font-weight: bold !important;
    text-decoration: none !important;
    cursor: pointer !important;
    display: inline-block;
}/* — 5. DOTTED LINK CLASS — */
.twfile-dotted {
    font-weight: bold !important;
    color: #444444 !important; /* Dark gray text */
    border-bottom: 2px dotted #808080 !important; /* Dotted gray line */
    text-decoration: none !important; /* Removes default underline */
    cursor: pointer !important;
}

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: