TP-NOTE(1)

πŸ“¦
version 1.27.3 Β· 2 versions
, in nixpkgs unstable

πŸ’– TP-NOTE(1) πŸ’–

okay bestie, welcome to TP-NOTE(1) β€” the Tp-Note documentation page. buckle up, this is the whole technical reference and it's lowkey a lot, but we're gonna get through it together 🫢


✨ NAME ✨

Tp-Note: Markup-enhanced granular note-taking

it's a filename-synchronizing, template-driven note-taker β€” meaning it keeps your notes as plain Markdown files, with no database and no lock-in. iconic behaviour honestly πŸ’…


🌈 SYNOPSIS 🌈

       tpnote [-a ] [-b] [-c <FILE>] [-C <FILE>] [-d <LEVEL>] [-e]
              [-l <LANG>] [-p <NUM>] [-n] [-t] [-u] [-v] [-V]
              [-x <DIR>|''|'-']
              [<DIR>|<FILE>]

πŸ¦‹ DESCRIPTION πŸ¦‹

so. Tp-Note is a note-taking tool and a template system that synchronizes the note's metadata with its filename. that's the whole vibe. it analyzes its environment and the clipboard content, and stores the result in variables. new notes get created by filling those variables into predefined, customizable Tera-templates ✨

here's the situation breakdown, because context matters:

  • if the first positional parameter <FILE> points to an existing Tp-Note file (i.e. a Markdown file with a YAML front matter header), the note's metadata is parsed and, if necessary, its filename is adjusted.
  • when <FILE> points to a plain Markdown file without a YAML header, Tp-Note opens it directly for editing and viewing without modifying the file. passing the --add-header flag additionally prepends a YAML header, converting that plain Markdown file into a full Tp-Note note.
  • for all other file types, Tp-Note creates a new note in the same directory, annotating the file.
  • if the positional parameter <DIR> points to an existing directory (or, when omitted, the current working directory), a new note is created in that directory.

after creation, Tp-Note launches the system's file editor. and bestie, listen β€” although the configurable default templates are written for Markdown, Tp-Note is not tied to any specific markup language. however, it does come with an optional viewer feature that currently renders only Markdown, ReStructuredText, and HTML input. in addition, there's some limited support for Asciidoc and WikiText. finally, the note's rendition is live updated and displayed in the user's web browser πŸ”₯

once you finish editing, Tp-Note analyzes potential changes in the note's metadata and renames the file (if necessary) so its metadata and filename are back in sync. finally, the resulting path is printed to stdout, and log and error messages are dumped to stderr. periodt.

this document is Tp-Note's technical reference. more info lives in Tp-Note's user manual and at Tp-Note's project page 🌸


πŸŽ€ CREATING NOTE FILES πŸŽ€

Tp-Note operates in 5 different modes, depending on its command line arguments and the clipboard state. each mode is associated with one content template and one filename template. slay, let's go through them.

πŸ’œ Create a new note with empty clipboard

in case the clipboard is empty while starting, the new note is created with the templates: tmpl.from_dir_content and tmpl.from_dir_filename. by default, the new note's title is the parent's directory name. the newly created file is then opened with an external text editor, letting you change the proposed title and add other content. when the text editor closes, Tp-Note synchronizes the note's metadata and its filename. this operation is performed with the tmpl.sync_filename template ✨

use case: the clipboard is empty and <path> is a directory (or empty):

tpnote "./03-Favorite Readings/"

or

cd "./03-Favorite Readings"
tpnote

creates the document:

./03-Favorite Readings/20211031-Favorite Readings--Note.md

with the content:

---
title:      Favorite Readings
subtitle:   Note
author:     Getreu
date:       2021-10-31
lang:       en-GB
---

🌸 Create a new note based on clipboard data

when <path> is a directory and the clipboard is not empty, the clipboard's content is stored in the variables {{ txt_clipboard.body }} and {{ html_clipboard.body }}. the latter contains the HTML rich text version of the clipboard content. also, if the content contains a hyperlink, the first hyperlink's name can be accessed with {{ txt_clipboard.body | link_text }}, its URL with {{ txt_clipboard.body | link_dest }} and its title with {{ txt_clipboard.body | link_title }}. the new note is then created with the tmpl.from_dir_content and the tmpl.from_dir_filename templates. finally, the newly created note file is opened again with some external text editor, and when you close it, Tp-Note synchronizes the note's metadata and its filename with the template tmpl.sync_filename πŸ’«

note: this operation mode also empties the clipboard (configurable feature). just so you know πŸ‘€

🧚 HTML to Markdown conversion

in case the clipboard stream contains HTML, the internal filter

{{ html_clipboard.body
   | html_to_markup(extension=extension_default, default=txt_clipboard.body) }}

converts the stream into Markdown before being processed. if the conversion fails or results in an empty string, the fallback value is {{ txt_clipboard.body }} ✨

πŸ“ Clipboard simulation

when no mouse and clipboard is available, the clipboard feature can be simulated by feeding the clipboard data into stdin:

echo "[The Rust Book](<https://doc.rust-lang.org/book/>)" | tpnote

Tp-Note behaves here as if the clipboard contained the string: β€œ[The Rust Book](<https://doc.rust-lang.org/book/>)”.

when you pipe HTML into Tp-Note, make sure that the stream starts with either <!DOCTYPE html or <html, e.g.:

echo '<!DOCTYPE html><h1>Hello World</h1>'| tpnote

⭐ The clipboard contains some text

use case: while launching Tp-Note the clipboard contains the string: β€œWho Moved My Cheese?\n\nChapter 2” and <path> is a directory.

tpnote "./03-Favorite Readings/"

or:

cd "./03-Favorite Readings/"
tpnote

this creates the document:

./03-Favorite Readings/20211031-Who Moved My Cheese--Note.md

with the content:

---
title:      Who Moved My Cheese
subtitle:   Note
author:     Getreu
date:       2021-10-31
lang:       en-GB
---

Who Moved My Cheese?

Chapter 2

we see from the above example how the tmpl.from_dir_content content template extracts the first line of the clipboard's content and inserts it into the header's title: field. then it copies the entire clipboard content into the body of the document. but if desired or necessary, it's possible to modify all templates in Tp-Note's configuration file πŸ‘‘ and note that not only the note's content is created with a template β€” its filename is too: the tmpl.from_dir_filename filename template concatenates the current date, the note's title and subtitle. it's giving organized.

πŸ¦‹ The clipboard contains a heading

use case: open a web page in your web browser and select some text with its heading. then copy the content into your (HTML) clipboard:

<h1>Cinderella</h1>
by the Brothers Grimm

launch Tp-Note:

mkdir 'Fairy tales'
tpnote './Fairy tales'

Tp-Note's templates tmpl.from_dir_content and tmpl.from_dir_filename create the following document:

./Fairy tales/20250104-Cinderella--Notes.md

---
title:        Cinderella
subtitle:     Note
author:       Getreu
date:         2025-01-04
lang:         de-DE
---

# Cinderella

by the Brothers Grimm

Tp-Note searches for the first heading tag <h1>, <h2>, <h3> etc. in the HTML clipboard and uses the finding as the title for the new Tp-Note document πŸ”

NB: you can achieve the same result by piping the clipboard content into Tp-Note:
mkdir 'Fairy tales'
echo '<!DOCTYPE html><h1>Cinderella</h1>by the Brothers Grimm'\
   | tpnote './Fairy tales'

use case: <path> is a directory, the clipboard is not empty and contains the string: I recommend:\n[The Rust Book](https://doc.rust-lang.org/book/).

tpnote './doc/Lecture 1'

Tp-Note's templates tmpl.from_dir_content and tmpl.from_dir_filename create the following document:

./doc/Lecture 1/20211031-The Rust Book--Notes.md

---
title:      The Rust Book
subtitle:   URL
author:     Getreu
date:       2021-10-31
lang:       en-GB
---

I recommend:
[The Rust Book](<https://doc.rust-lang.org/book/>)

when analyzing the clipboard's content, Tp-Note searches for hyperlinks in Markdown, ReStructuredText, Asciidoc and HTML format. when successful, the content template uses the link text of the first hyperlink found as the document title 🎯

🌈 The clipboard contains a YAML header

use case: <path> is a directory, the clipboard is not empty and contains the string: ---\ntitle: Todo\nfile_ext: mdtxt\n---\nnothing.

tpnote

this creates the note: 20230915-Todo.mdtxt with the following content:

---
title:      Todo
subtitle:   Note
author:     Getreu
date:       2023-09-15
lang:       fr-FR

file_ext:   mdtxt
---

nothing

technically, the creation of the new note is performed using the YAML header variables: {{ fm.fm_title }}, {{ fm.fm_subtitle }}, {{ fm.fm_author }}, {{ fm.fm_date }}, {{ fm.fm_lang }}, {{ fm.fm_sort_tag }} and {{ fm.fm_file_ext }} which are evaluated with the tmpl.from_dir_filename templates πŸ”₯

note that the same result can also be achieved without clipboard input by typing in a terminal:

echo -e "---\ntitle: Todo\nfile_ext: mdtxt\n---\n\nnothing" | tpnote

furthermore, this operation mode is very handy with pipes in general, as shown by the following example: it downloads some webpage, converts it to Markdown and copies the result into a Tp-Note file. the procedure preserves the webpage's title in the note's title:

curl 'https://blog.getreu.net' \
| pandoc --standalone -f html -t markdown_strict+yaml_metadata_block \
| tpnote

creates the note file 20230919-Jens Getreu's blog--Note.md with the webpage's content converted to Markdown:

---
title:      Jens Getreu's blog
subtitle:   Note
author:     Getreu
date:       2023-09-15
lang:       en

viewport:   width=device-width, initial-scale=1.0, maximum-scale=1
---

<a href="/" class="logo">Jens Getreu's blog</a>

-   [Home](https://blog.getreu.net)
-   [Categories](https://blog.getreu.net/categories)

πŸŒ€ The clipboard or stdin contains an invalid YAML header

Okay so picture this: <path> is a directory, and the clipboard (or stdin) has text that starts with --- delimiters but the YAML inside is just… not valid. Like, ---\n<invalid\n---\nhallo β€” it’s giving β€œI tried” but the vibe check failed. 😭

In this case Tp-Note logs a warning and treats the entire input β€” including the --- delimiters and the malformed YAML β€” as plain body text. Note creation then proceeds as if no header had been present at all. No drama, no crash, just 🫢 β€œwe move.”

echo -e "---\ninvalid\n---\nhello" | tpnote

πŸ“Ž Create a new note annotating some non Tp-Note file

When <path> points to an existing file whose file extension is other than .md, a new note is created with a similar filename, and a reference to the original file is copied into the new note’s body. If the clipboard contains some text, it’s appended there also. The logic of this is implemented in the templates: tmpl.annotate_file_content and tmpl.annotate_file_filename. Once the file is created, it’s opened with an external text editor. After editing the file, it will be β€” if necessary β€” renamed to be in sync with the note’s metadata. Slay. ✨

For example:

:> "Classic Shell Scripting.pdf"

tpnote "Classic Shell Scripting.pdf"

creates the note:

Classic Shell Scripting.pdf--Note.md"

with the content:

---
title:      Classic Shell Scripting.pdf
subtitle:   Note
author:     Getreu
date:       2023-09-15
lang:       en-US
---

[Classic Shell Scripting.pdf](<Classic Shell Scripting.pdf>)

The configuration file variable filename.extensions lists all the file extensions that Tp-Note recognizes as its own file types. Only foreign file types can be annotated. Lowkey important. πŸ‘‘

Note that the file annotation mode also reads the clipboard’s (or stdin’s) content: when it’s not empty, its data is appended to the new note’s body. Furthermore, if that input carries its own YAML header, its fields override the corresponding header fields of the new note β€” title:, author: and date: (and keywords: under the zettel scheme). When a field is absent, the default derived from the filename, the username and the current date is used instead. The way that works is honestly iconic. πŸ’…


πŸͺ„ Convert a text file into a Tp-Note file

Consider the content of the following text file Ascii-Hangman--A game for children.md whose creation date is 13 March 2022:

A little game designed for primary kids to revise vocabulary in classroom.

To convert the plain Markdown file into a Tp-Note file, the --add-header flag is required (it defaults to false via arg_default.add_header):

tpnote --add-header --batch "Ascii-Hangman--A game for children.md"

Without --add-header, Tp-Note opens the file directly for editing and viewing without prepending a header. No cap.

As a result of the above command, Tp-Note converts the filename into:

20220313-Ascii-Hangman--A game for children.md

and prepends a YAML header to the file’s content:

---
title:      Ascii-Hangman
subtitle:   A game for children
author:     Getreu
date:       2022-03-13
lang:       en-US

orig_name: Ascii-Hangman--A game for children.md
---
A little game designed for primary kids to revise vocabulary in classroom.

🐚 Use Tp-Note in shell scripts

  • Use case: download a webpage and store it as Tp-Note file

Using the method displayed above you can save time and create a script with:

sudo nano /usr/local/bin/download

Insert the following content:

#!/bin/sh
curl "$1" | tpnote

Instead of Tp-Note’s internal HTML to Markdown converter, you can alternatively use the external pandoc converter. This method offers the advantage to also convert the HTML page’s metadata. Currently, Tp-Note’s internal converter lacks this feature. F in the chat for that missing feature, but we ball. 😀

#!/bin/sh
curl "$1" | pandoc --standalone -f html -t markdown_strict+yaml_metadata_block | tpnote

Do not forget to make it runnable:

sudo chmod a+x /usr/local/bin/download

To execute the script type:

download 'https://blog.getreu.net'

Periodt. 🫢

πŸ’… NOTE FILE MANIPULATION πŸ’…

Ok bestie, this is the part where things get interactive β€” we're talking editing, viewing, renaming, printing, and scripting. Strap in. πŸŽ€


✏️ Editing notes

So unless you invoke Tp-Note with --batch or --view, it launches an external text editor after creating a new note. Simple. And that also happens when <path> points to an existing .md-file, regardless of whether the file has a YAML header or not. No cap, it just opens. 🫢

Now here's the tea: when <path> points to a plain Markdown file without a YAML header, Tp-Note opens it for editing and viewing without modifying the file. Like, it's just gonna sit there respectfully. But if you also want to prepend a YAML header and convert that file into a real Tp-Note note, you add --add-header. That's the glow-up. ✨

For example, open and edit an existing note file:

cd "./03-Favorite Readings"
tpnote 20211031-Favorite Readings--Note.md

πŸ‘€ Viewing notes

Once Tp-Note has launched your file editor, it opens the note file, renders its content to HTML, launches your web browser and connects it to Tp-Note's internal web server. Then β€” and this is the iconic part β€” Tp-Note watches the note file and re-renders the viewed HTML when the content changes. Live. Constantly. Main character energy. πŸ’…

The note's file extension determines which internal renderer is activated.

Tp-Note's built-in note viewer comprises three markup language renderers:

  1. Markdown _(file extension .md)_

This renderer is CommonMark compatible and feature complete. 🌈

Source code is highlighted when you annotate the programming language (see also tmpl_html.viewer_highlighting_theme and tmpl_html.exporter_highlighting_theme):

pub fn main(){
  let w = "world!";
  println!("Hallo {:?}", w);
}

The renderer understands heading attributes, inline images, tables, task lists, footnotes, strike-through, LaTeX formulas and Mermaid diagrams. The following is an example of a LaTeX formula:

x^n + y^n = z^n

Or:

$$ x^n + y^n = z^n $$

Inline formulas are enclosed between Dollar characters, e.g. $\alpha$ becomes α. 🧚

A formula that fails to parse is governed by the tmpl_html.viewer_embedded_content_error_policy and tmpl_html.exporter_embedded_content_error_policy options (both default to Inline). Each option accepts one of two values:

  • Inline replaces the faulty formula with an inline error box and logs a warning, so the rest of the note still renders and --export still produces a file.
  • HardError aborts the whole rendition instead, so the viewer shows its full-page error template and --export fails without writing an output file.
Batch export (--export together with --batch) always uses HardError, overriding the configured exporter value, so a broken formula or diagram fails an automated or scripted export. 😭 That's on purpose, bestie β€” you want it to fail loudly there.

The same policies apply to broken Mermaid diagrams (see below).

Fenced mermaid blocks are rendered to inline SVG diagrams β€” in both the viewer and the exported HTML β€” by a pure-Rust renderer, so the export stays fully self-contained (no JavaScript or extra assets):

graph TD
    A --> B

When a diagram fails to render, the behaviour is controlled by the same tmpl_html.viewer_embedded_content_error_policy and tmpl_html.exporter_embedded_content_error_policy options described above for formulas.

Heading attributes β€” ok this is lowkey the coolest bit:

# text {#id .class1 .class2 myattr other_attr=myvalue}

is interpreted as a level 1 heading with the content β€œtext”, ID id, classes class1 and class2 and custom attributes myattr (without value) and other_attr with value myvalue. Note that ID, classes, and custom attributes should be space-separated. The space after the leading # is required, as it marks the # as a heading marker. This is unrelated to the # inside the braces, which attaches directly to the ID. πŸ”₯

For example:

## Installation {#install .collapsible data-level=2}

is a level 2 heading with the content β€œInstallation”, ID install, class collapsible and custom attribute data-level with value 2.

Every heading that does not carry an explicit ID this way gets one generated automatically from its text, so a table of contents or a cross-reference to another section works without hand-written heading attributes, and the same note keeps navigating correctly when rendered by a forge like GitHub or GitLab. This is controlled by tmpl_html.auto_heading_ids (default Gfm):

  • Gfm reproduces the GitHub/GitLab slug (lowercase, spaces become hyphens, punctuation dropped).
  • Pandoc reproduces Pandoc's auto_identifiers algorithm instead, which additionally drops a leading run of non-letter characters (a heading starting with 2. loses the 2. entirely, unlike Gfm, which keeps the digit).
  • Off disables this and restores the previous behaviour, where a heading without an explicit ID has none.

An explicit {#id} always wins over the generated one, regardless of this setting. Periodt. πŸ‘‘

  1. ReStructuredText _(file extension .rst)_

This renderer is experimental and covers only basic markup. πŸ™ˆ Like, it's trying its best.

  1. PlainText _(link only renderer, file extension .txtnote)_

The purpose of this renderer is to make hyperlinks written in Markdown, ReStructuredText, Asciidoc, HTML, Wikitext syntax clickable. Only hyperlinks are rendered, all other text is shown verbatim. Ngl that's kind of slay β€” clickable links, everything else untouched. πŸ’–

Tp-Note's web server streams large media files without loading them into memory. Just refer to the media file as local link: [my video](<dir/my video.mp4>). Make sure that the file extension of the video file is registered with viewer.served_mime_types. πŸ“


πŸ¦‹ Automatic filename synchronization before and after editing

Before launching the text editor and after closing it, Tp-Note synchronizes the filename with the note's metadata. So when you change the metadata of a note, Tp-Note will replicate that change in the note's filename. As a result, all your note's filenames always correspond to their metadata, which helps to retrieve your notes in large data pools. Literally the organization dream. ✨

For example:

tpnote "20200306-Favorite Readings--Note.md"

The way how Tp-Note synchronizes the note's metadata and filename is defined in the template tmpl.sync_filename.

Once Tp-Note opens the file in your text editor, let's assume you decide to change the title in the note's YAML metadata section from title: Favorite Readings to title: Introduction to bookkeeping. After closing the text editor, Tp-Note updates the filename automatically:

20200306-Introduction to bookkeeping--Note.md
Note: the sort tag 20200306 has not changed. The filename synchronization mechanism by default never does. (See below for more details about filename synchronization.) 🧚

πŸ–¨οΈ Printing note files

Tp-Note renders note files to HTML. The latter is either shown in the browser or can be exported with --export. When exporting to HTML, hyperlinks are passed through an internal link rewriting engine that can be parametrized with --export-link-rewriting. The easiest way to print the resulting HTML, is to pipe it through an HTML to PDF converter, e.g. Weasyprint or Wkhtmktopdf.

tpnote --export=- mynote.md | weasyprint - mynote.md.pdf

Note that this example runs without --batch, so a failed embedded renderer (a Mermaid diagram or LaTeX formula) follows the configured tmpl_html.exporter_embedded_content_error_policy (default Inline): the error is rendered inline and the PDF is still produced. Add --batch to make such a failure abort the export instead, so a broken diagram or formula can never slip silently into the PDF (batch export always uses HardError). πŸ’€

Weasyprint supports the CSS Paged Media standard allowing to include page layout directives into HTML. You can change the default page layout by modifying the HTML template with the tmpl_html.exporter_doc_css configuration file variable.

While tmpl_html.exporter_doc_css sets the page layout for all your notes, a single note can carry its own layout: as Markdown passes inline HTML through to the rendered document, a <style> block placed in the note's body reaches Weasyprint unchanged. The following example switches the page to A4 landscape with narrow margins and starts every chapter on a new page:

---
title:        Documentation
subtitle:     Overview
---

<style>
@page { size: A4 landscape; margin: 10mm; }
h1:not(:first-of-type) { break-before: page; }
</style>

# Documentation

Such a <style> block also affects the note's live preview in the viewer, where the @page rules are simply ignored. πŸ‘€

Independently of any style sheet, a page break can also be forced at one single spot by placing an empty HTML element carrying the break directive directly into the note's body. Surround it with blank lines, so that Markdown treats it as a block of its own:

The last paragraph before the break.

<div style="break-after:page"></div>

This paragraph starts on a new page.

🐚 Use Tp-Note in shell scripts

  • Use case: synchronize recursively filenames and metadata πŸ”₯

The following synchronizes bidirectionally all filenames with the note's YAML header data.

TPNOTE_USER="John" find . -type f -name '*.md' -exec tpnote -a -b {} > /dev/null \;

The direction of the synchronization depends on whether the .md file has a valid YAML header or not:

  • A YAML header is present and valid: the header fields might update the filename (see template tmpl.sync_filename). A possible sort-tag at the beginning of the filename remains untouched. πŸŽ€
  • No YAML header: a new header is prepended (see template from_text_file_content) and the filename might change slightly (see template from_text_file_filename). A possible sort-tag at the beginning of the filename remains untouched. If the filename does not start with a sort tag, the file's creation date is prepended. ⭐

iykyk β€” that's the whole sync vibe. πŸ’œ

✨ OPTIONS ✨

Okay bestie, this is the big one β€” every flag, every vibe. Buckle up πŸŽ€

  • -a, --add-header β€” Prepends a YAML header in case the text file does not have one. The default template deduces the title:' and subtitle:' header fields from the filename. The sort-tag and file extension remain untouched. In case the filename is lacking a sort-tag, the file creation date in numerical format is prepended. Without this flag, Tp-Note opens a plain Markdown file directly for editing and viewing without modifying it. This flag corresponds to the configuration file variable arg_default.add_header', which defaults to false'.
  • -b, --batch β€” Do not launch the external text editor or viewer. All other operations are available and are executed in the same way. In batch mode, error messages are dumped on the console only and no alert windows pop up.

Lowkey important: Tp-Note ignores the clipboard when run in batch mode with `--batch'. Instead, if available, it reads the stdin stream as if the data came from the clipboard. 🫢

  • -c FILE, --config=FILE β€” Loads an additional configuration from the TOML formatted FILE and merges it into the default configuration.
  • -C FILE, --config-defaults=FILE β€” Dumps the internal default configuration in TOML format into FILE or stdout if FILE equals to -', e.g. tpnote -C - | less`.
  • -d LEVEL, --debug=LEVEL β€” Prints additional log messages. The debug level LEVEL must be one out of trace', debug', info', warn', error' (default) or off'. The level trace' reports the most detailed information, while error' informs you only about failures. A `warn' level message means that not all functionality might be available or work as expected.

Here's the cheat sheet for debugging, literally keep this:

  • Use `-b -d trace' for debugging templates
  • and `-V -b -d trace' for debugging configuration files.
  • If the HTTP server (viewer) does not work as expected: `-n -d debug'.
  • If your text editor does not open as expected: `-n -d info --edit'.
  • Or, to observe the launch of the web browser: `-n -d info --view'.
  • The option `-d trace' shows all available template variables, the templates used and the rendered result of the substitution. This is particularly useful for debugging new templates.
  • The option `-d off' silences all error message reporting and also suppresses the error pop-up windows.
Note, under Linux, when -d trace is given, no pop-up messages appear. Instead, the logs are dumped to the console from where you started Tp-Note.

All error messages are dumped in the error stream stderr and appear on the console from where Tp-Note was launched:

tpnote.exe --debug info my_note.md

Under Windows the output must be redirected into a file to see it:

tpnote.exe --debug info my_note.md >debug.md 2>&1

Alternatively, you can redirect all log file entries into popup alert windows:

tpnote.exe --popup --debug info my_note.md

The same can be achieved by setting following configuration file variables (especially useful with Windows):

[arg_default]
debug = 'info'
popup = true

The value for arg_default.debug' must be one out of trace', debug', info', warn', error' (default) and `off'. They have the same meaning as the corresponding command line options. πŸ’…

  • -e, --edit β€” Edit only mode: opens the external text editor, but not the file viewer. This disables Tp-Note's internal file watcher and web server, unless -v' is given. Alternatively you can set the environment variable TPNOTE_BROWSER=""' to the empty string. Another way to permanently disable the web server is to set the configuration variable arg_default.edit=true'. When --edit --view' appear together, both the editor and the viewer will open and the arg_default.edit variable is ignored.
  • -l LANGUAGE_TAG, --force-lang=LANGUAGE_TAG β€” Disables the automatic language detection while creating a new note file and uses LANGUAGE_TAG instead. LANGUAGE_TAG is formatted as IETF BCP 47 language tag, e.g. en-US'. If LANGUAGE_TAG equals -', the environment variable TPNOTE_LANG' determines the language instead; or, if the latter is not defined, the user's default language, as reported from the operating system's locale setting, is decisive. The configuration file variable arg_default.force_lang' has the same effect as the above when set to a non-empty LANGUAGE_TAG string.
  • -p PORT, --port=PORT β€” Sets the server port that the web browser connects to, to the specified value PORT. If not given, a random available port is chosen automatically. ✨
  • -n, --no-filename-sync β€” Whenever Tp-Note opens a note file, it synchronizes its YAML-metadata with its filename. --no-filename-sync' disables this synchronization. In addition, in scripts this flag can be especially useful for validating the syntax of .md'-files. See section EXIT STATUS for more details. The section METADATA FILENAME SYNCHRONIZATION shows alternative ways to disable synchronization.
  • -s SCHEME_NAME, --scheme=SCHEME_NAME β€” Sets the filename scheme for creating a new note file. This overwrites the arg_default.scheme' value in the configuration file. Under [[scheme]]' follows the definition of the schemes. The default configuration ships two schemes with the SCHEME_NAMES default' and zettel' (for Zettelkasten).
  • -t, --tty β€” Tp-Note tries different heuristics to detect whether a graphic environment is available or not. For example, under Linux, the DISPLAY' environment variable is evaluated. The --tty' flag disables the automatic detection and sets Tp-Note into "console only" mode: now only the non GUI editor (see configuration variable: `app_args.editor_console') and no viewer is launched. πŸ–₯️
  • -u, --popup β€” Redirects log file entries into pop-up alert windows. Must be used together with the --debug option to have an effect. Note, that debug level error' conditions will always trigger pop-up messages, regardless of --popup and --debug (unless --debug off'). Pop-up alert windows are queued and will never interrupt Tp-Note. To better associate a particular action with its log events, read through all upcoming pop-up alert windows until they fail to appear. As this option is activated by default, it has no effect unless you set `arg_default.popup = false' in the configuration file.
  • -v, --view β€” View only mode: do not open the external text editor. This flag instructs Tp-Note to start an internal file watcher and web server and connect the system's default web browser to view the note file and to observe live file modifications. The configuration setting arg_default.edit=true' or the environment variable TPNOTE_EDITOR=""' disables the viewer. However, with --view' given at the command line, the viewer appears, regardless of the value of arg_default.edit'.
NB: By default, Tp-Note tries to synchronize every file it opens. To prevent the viewed filename from changing, --view' can be used together with --no-filename-sync'.
  • -V, --version β€” Print Tp-Note's version, its built-in features and the path to the sourced configuration file. The output is YAML formatted for further automatic processing. In addition, use `-V -b -d trace' for configuration file debugging.
  • -x DIRECTORY, --export=DIRECTORY β€” Prints the note as HTML rendition into DIRECTORY. -x -' prints to stdout. If DIRECTORY is a relative path, DIRECTORY is appended to the document directory. For example, e.g. --export=.' or -x ./', prints the HTML rendition in the directory where the note file resides. This option prevents the launch of the external text editor and viewer and can be combined with --batch' to avoid pop-up error alert windows.
  • --export-link-rewriting=MODE β€” Chooses how local links in the exported HTML file are written out: off', short' or long' (default). No link rewriting occurs, for the MODE off'. The MODE short' rewrites all local relative links to absolute links, whose base is the first parent directory containing a project configuration file tpnote.toml'. NB, the directory of that file defines the base for all absolute local links in your Tp-Note file! The mode long' rewrites all local links to absolute links whose base is the system's root directory /' . For relative local links this is performed by prepending the path to the note file. Absolute local links get the path to the project configuration file tpnote.toml' prepended. In case you do not place a tpnote.toml' file in a parent directory, the base for absolute local links in your note file is interpreted as `/' .

The right MODE choice depends on how you view the resulting HTML: if you publish on a web server, then short' might be a good choice. Do not forget to place a project configuration file tpnote.toml' somewhere in the document's path. If you view the HTML file directly in your web browser, better choose `long'. In this case, the presence of a project configuration file will not affect the output.

NB: You can also set this option via Tp-Note's configuration file with the key `arg_default.export_link_rewriting'.

🌸 THE NOTE'S DOCUMENT STRUCTURE 🌸

Okay so here's how Tp-Note decides a text file is a valid note file β€” it checks if its:

  • file extension is listed in one of the configuration file variable `filename.extensions'; if its
  • content has a valid YAML header and
  • the YAML header contains a key whose name is defined in the configuration file variable tmpl.compulsory_header_field' (default title').

A Tp-Note note file is always UTF-8 encoded. As newline, either the Unix standard \n' or the Windows standard \r\n' is accepted. Tp-Note writes out newlines according the operating system it runs on. πŸ¦‹

✨ The document's header and body ✨

Okay bestie, so Tp-Note is literally designed to be compatible with Pandoc's and RMarkdown's document structure, like the figure below shows. And just so we're speaking the same language: in this whole doc, the terms β€œYAML header”, β€œheader” and β€œfront matter” are all synonyms, okay? They all mean the same metadata block sitting at the very beginning of the text file 🫢

---
<YAML-front-matter>
---

<document-body>

The YAML front-matter starts right at the beginning of the document with --- and ends with ... or ---. Heads up: according to the YAML standard, string literals are always encoded as JSON strings β€” that's just the rules, no cap. And by convention, a valid Tp-Note file has at least one YAML field named title: (the name of this compulsory field is defined by the tmpl.compulsory_header_field variable in the configuration file, so yes, you can change it there ✨).

Now, lowkey important: prepended text placed before the YAML front-matter is ignored. But if it's there, the skipped text must not be longer than 1024 Unicode characters (cf. the constant BEFORE_HEADER_MAX_IGNORED_CHARS in Tp-Note's source code file content.rs) and it must be followed by at least one blank line:

Prepended text is ignored.

---
<YAML-front-matter>
---

<document-body>

There's no restriction about the markup language used in the note's text body β€” go off, write whatever 🌈. However, the default templates assume Markdown or ReStructuredText and the file extensions .md and .rst. Both can be changed easily by adapting Tp-Note's configuration file. And besides the requirements concerning its header, a valid Tp-Note file must have a filename extension that's listed in the configuration file variable filename.extensions. That variable also decides which internal markup language render gets called for Tp-Note's internal viewer πŸ‘‘


The document's body often contains (inline) links to resources β€” like images β€” and links to other documents. This section describes how the automatic path rewriting of local links works πŸ’…

In general, the link syntax depends on the markup language used in the Tp-Note file. The following examples illustrate the different link types Tp-Note understands:

  • Absolute URL β€” [blog](<https://blog.getreu.net>)
  • Relative URL (=local link) β€” ![Alt text](<images/my logo.png>)
  • Absolute local link β€” ![Alt text](</images/my logo.png>)
  • Relative local link β€” ![Alt text](<images/my logo.png>)
  • Relative local link β€” [my doc](<../../notes/31-my doc.md>)
  • Relative local autolink β€” <tpnote:../../notes/31-my%20doc.md>
  • Shorthand link β€” [my doc](<../../notes/31>)
  • Shorthand autolink β€” <tpnote:../../notes/31>
  • Formatted shorthand link β€” <tpnote:../../notes/31?-->)

Remarks, bestie πŸ“:

  • The base for absolute local links is the first parent directory containing a project configuration file tpnote.toml. If absent, absolute local links refer to the root directory /.
  • Shorthand link: instead of writing out the full link destination, e.g. [my doc](<./docs/20230508-my note.md>), you can shorten the link to [my doc](<docs/20230508>), indicating only the destination's sort-tag. Alternatively, the same shorthand link can be expressed as autolink as well: <http:docs/20230508>. NB: if more than one document with the same sort-tag exists in a directory, the viewer only displays the first in alphabetical order. To set up a different order, you can extend the sort-tag until it becomes unique, e.g. by renaming the destination document in the above example to ./docs/20230508a-my note.md. This way you obtain the unique sort-tag 20230508a ✨

Although Tp-Note's built in viewer follows absolute and relative local links, usually the latter are preferred. They make moving documents easier, as relative links do not break when the source and the destination documents are moved together πŸŽ€

As mentioned above, the shortest way to refer to other Tp-Note documents is indicating their sort-tag only, e.g. <tpnote:dir/123> and [my file](<tpnote:dir/123>). And if the other document is located in the same directory, the links are even shorter: <tpnote:123> and [my file](<tpnote:123>) β€” iconic, honestly πŸ’…

Tp-Note's exporter function --export converts a given Tp-Note file into HTML and adds .html to the output filename. Links in the document's content to other Tp-Note files are hereby rewritten by appending .html to their URLs. This way you can convert groups of documents to HTML and later jump from document to document in your web browser πŸ¦‹. The option --export-link-rewriting allows you to fine-tune how local links are written out. Valid values are: off, short and long.

In order to achieve this, the user must respect the following convention concerning absolute paths in local links in Tp-Note documents: when a document contains a local link with an absolute path, the base of this path is considered to be the directory where the project configuration file tpnote.toml resides (or / if none exists). The option --export-link-rewriting decides how local links in the Tp-Note document are converted when the HTML is generated. If its value is short, then local links with relative paths are converted to absolute paths. The base of the resulting path is where the tpnote.toml file resides (or / if none exists). Consider the following example --export-link-rewriting=short:

  • The Tp-Note file /my/docs/car/bill.md contains
  • an absolute local link: /car/scan.jpg,
  • and another relative local link: ./photo.jpg.
  • The project configuration file is: /my/docs/tpnote.toml.

The images in the resulting HTML will appear as

  • /car/scan.jpg.
  • /car/photo.jpg.

For --export-link-rewriting=long, in addition to the above, all absolute paths in local links are prepended with the project configuration file's directory. Consider the following example:

  • The Tp-Note file /my/docs/car/bill.md contains
  • an absolute local link: /car/scan.jpg,
  • and another relative local link: ./photo.jpg.
  • The project configuration file is: /my/docs/tpnote.toml.

The images in the resulting HTML will appear as

  • /my/docs/car/scan.jpg.
  • /my/docs/car/photo.jpg.

Summary: the right --export-link-rewriting choice depends on how you view the resulting HTML. If you publish on a web server, then short might be a good choice (do not forget to place a project configuration file tpnote.toml somewhere in the document's path 🌸). If you view the HTML file directly in your web browser, better choose long πŸ’–.

So far, we've seen how Tp-Note's viewer and HTML exporter converts the destination of local links [text](destination). Concerning the local link's text property, the situation is simpler, as the text property never changes during the above discussed rewriting process. However, it is possible to overwrite the displayed text property by appending a format string to the destination: [formatted destination](destination?format string) ✨

All local links in the following tables have the same link destination dir/01ac-Tulips--red, yellow.md. The examples differ only in the way the link is displayed in the browser πŸ‘€

Local link:

  • [matters](<dir/01ac-Tulips--red, yellow.md>) β€” what you see: matters
  • [matters](<dir/01ac>) β€” what you see: matters

Formatted local link:

  • [whatever](<dir/01ac-Tulips--red, yellow.md?>) β€” what you see: Tulips–red, yellow
  • [whatever](<dir/01ac?>) β€” what you see: Tulips–red, yellow
  • [whatever](<dir/01ac?,>) β€” what you see: Tulips–red
  • [whatever](<dir/01ac?-->) β€” what you see: Tulips
  • [whatever](<dir/01ac?--:,>) β€” what you see: red
  • [whatever](<dir/01ac?#>) β€” what you see: 01ac
  • [whatever](<dir/01ac??>) β€” what you see: 01ac-Tulips–red, yellow.md

Observations, because these are so worth knowing 🧚:

  1. The format operator ? (not followed by a #) strips the path, the sort-tag, the copy-counter and the filename extension. In other words, it keeps only the file stem.
  2. The string following the question mark is the to pattern: ?<to>. It marks the exclusive end of the matching.
  3. Consider the pattern ?<from>:<to>: the string before the colon is the from pattern, the string after the colon is the to pattern. Patterns are always searched from the start of the string, e.g. in Latin scripts from the left to the right.
  4. The format operator ?# prints the sort-tag.
  5. The format operator ?? prints the whole filename.
  6. All format operators can be optionally followed by a search pattern.

Local autolink:

  • <tpnote:dir/01ac-Tulips--red,%20yellow.md> β€” what you see: dir/01ac-Tulips–red,%20yellow.md
  • <tpnote:dir/01ac> β€” what you see: dir/01ac

Formatted local autolink:

  • <tpnote:dir/01ac-Tulips--red,%20yellow.md?> β€” what you see: Tulips–red, yellow
  • <tpnote:dir/01ac?> β€” what you see: Tulips–red, yellow
  • <tpnote:dir/01ac??> β€” what you see: 01ac-Tulips–red, yellow.md
  • <tpnote:dir/01ac??.> β€” what you see: 01ac-Tulips–red, yellow
  • <tpnote:dir/01ac??:.> β€” what you see: 01ac-Tulips–red, yellow
  • <tpnote:dir/01ac??-:,> β€” what you see: Tulips–red
  • <tpnote:dir/01ac??--:,> β€” what you see: red

πŸ’– METADATA FILENAME SYNCHRONIZATION πŸ’–

Okay so bestie, this is the part where Tp-Note gets lowkey obsessed with making your filename and your metadata match. Consider this Tp-Note filename generated with the default filename scheme (cf section β€œFilename synchronization schemes” for other schemes):

20151208-Make this world a better place--Suggestions.md

The filename has 4 parts:

{{ fm.fm_sort_tag }}-{{ fm.fm_title }}--{{ fm.fm_subtitle }}.{{ fm.fm_file_ext }}

The - between {{ fm.fm_sort_tag }} and {{ fm.fm_title }} is hereafter referred to as sort-tag separator (cf. filename.sort_tag.separator).

A so-called sort tag is an alphanumeric prefix at the beginning of the filename. It is used to order files and notes in the file system. Besides numerical digits and lowercase letters, a sort tag may contain any combination of _, -, = and . (cf. filename.sort_tag.extra_chars). If a sort-tag contains lowercase letters, only 2 in a row are allowed (cf. filename.sort_tag.letters_in_succession_max). For example:

  • Chronological sort tag
20140211-Reminder.doc
20151208-Manual.pdf
2015-12-08-Manual.pdf
NB: All chronological sort-tags must have at least one counter with 4 digits or more, e.g. 2015. The character - between the counters is optional.
Tip: Always include the year with 4 digits in chronological sort-tags.
  • Sequence number sort tag
02-Invoices/
08-Tax documents/
09_2_144-Manual.pdf
09.9.1-Notes.md
NB: None of the counters exceeds 3 digits (cf. filename.sort_tag.sequential.digits_in_succession_max) which is the criterion to recognize a sequence number sort-tag. The largest counter in these examples is 144, so all sort_tags are sequence numbers.
  • Alphanumeric sequence number sort tag
02-Invoices/
08-Tax documents/
09b144-Manual.pdf
09i1-Notes.md
NB: The example is equivalent to the previous one. The only difference is, that the separators are expressed through the alternation of digits and letters.

Summary:

  1. A sort-tag is composed of a number of counters, which can be numerical, e.g. 123.28 or combined numerical/letter based, e.g. 123ab.
  2. A counter is a set of digits (base 10) 123 or a set of lowercase letters (base 26) ab.
  3. A letter-based counter can be maximal 2 letters wide. Its maximum is zz (cf. filename.sort_tag.letters_in_succession_max).
  4. A sequential sort-tag is a sort-tag whose counters are at most 3 digits wide (cf. sort_tag.sequential.digits_in_succession_max).
  5. The filter incr_sort_tag increments only sequential sort-tags.
  6. In order not to confuse sequential and chronological sort-tags, it is recommended to always write out the year in chronological sort-tags with 4 digits, e.g. 2013-08-10 or 20130810.

Before Tp-Note creates a new note file, it searches the current directory for the latest existing Tp-Note file. If that file starts with a sequence number sort-tag, Tp-Note increments that number and uses the result as sort-tag for the new note file. Otherwise, the new note gets a chronological sort tag of today.

A note’s filename is said to be in sync with its metadata, when the following holds (slightly simplified, see tmpl.sync_filename):

filename on disk without sort tag == `{{ fm.fm_title }}--{{
fm.fm_subtitle }}.md'

[1]

For example, consider the following document with the filename:

20211031-My file.md

and the content:

---
title:      1. The Beginning
subtitle:   Note
author:     Getreu
date:       2021-10-31
lang:       en-GB

remainder:  false
---

As My file.md is not equal to 1. The Beginning--Note.md, Tp-Note will rename the file to 20211031-1. The Beginning--Note.md. If the filename had been 05_02-My file.md, it would rename it to 05_02-1. The Beginning--Note.md.

Note: When the YAML front-matter does not contain the optional sort_tag variable, Tp-Note will never change a sort tag. Nevertheless, it might change the rest of the filename!

The reason why by default Tp-Note does not change sort tags is, that they define their order in the file listing. In general this order is independent of the notes content. The simplest way to organize the sort tags of your files is by renaming them directly in your file system. Nevertheless, in some cases you might want to have full control over the whole filename through the note’s YAML front-matter. For example, if β€” for some reason β€” you have changed the document’s date in the front-matter and you want to change the chronological sort tag in one go. In order to overwrite the note’s sort tag on disk, you can add a sort_tag string-variable to its front-matter:

---
title:      1. The Beginning
date:       2021-10-31

sort_tag:   '20211101'
---

Note, the above sort-tag value - here a number - must be enclosed with quotes in order to label it as a string type. When Tp-Note synchronizes the note’s metadata with its filename, it will also change the sort tag from 20211031 to 20211101. The resulting filename becomes 20211101-1. The Beginning--Note.md.

The sort_tag variable also becomes handy, when you want to create one single note without any sort tag:

---
title:      1. The Beginning

sort_tag:   ''
---

In the same way, how it is possible to pin the sort tag of the note from within the note’s metadata, you can also change the file extension by adding the optional file_ext variable into the note’s front-matter:

---
title:      1. The Beginning

file_ext:   rst
---

This will change the file extension from .md to .rst. The resulting filename becomes 20211101-1. The Beginning--Note.rst.

Important: rst must be one of the registered file extensions listed in the filename.extensions variable in Tp-Note’s configuration file. If needed you can add more extensions there. If the new filename extension is not listed in one of these variables, Tp-Note will not be able to recognize the note file as such and will not open it in the external text editor and viewer.
Note: When a sort_tag variable is defined in the note’s YAML header, you should not change the sort tag string in the note’s file name manually by renaming the file, as your change will be overwritten next time you open the note with Tp-Note. However, you can switch back to Tp-Note’s default behaviour any time by deleting the sort_tag line in the note’s metadata. The same applies to the file_ext variable.

The metadata filename synchronization feature can be disabled permanently by setting the configuration file variable arg_default.no_filename_sync = true. To disable this feature for one time only, invoke Tp-Note with --no-filename-sync. To exclude a particular note from filename synchronization, add the YAML header field filename_sync: false.

---
title:         1. The Beginning

filename_sync: false
---

Note, that in the above described examples, the information flow always goes from the YAML note header towards the note’s filename. However, when Tp-Note opens a text file without a YAML header, a new header is added automatically. In this case the information flow goes from the filename towards the header, namely in the opposite direction. Once the new header is prepended to the text file, a regular filename synchronization - as described above - is triggered and executed as described above.


πŸŽ€ Filename synchronization schemes πŸŽ€

Technically, the rules how the note’s header relates to its filename are encoded in customizable so-called filename templates (cf. section Templates). These templates exist in two different variants referred to as default scheme and zettel scheme:

  1. The 'default' scheme:

For example:

---
title:      1. The Beginning
subtitle:   Note
---

The filename synchronization template from the default scheme set looks like (simplified):

{{ fm.fm_sort_tag }}-{{ fm.fm_title }}--{{ fm.fm_subtitle }}.{{ fm.fm_file_ext }}

It generates from the above example the filename:

20211031-1. The Beginning--Note.md
  1. The zettel scheme:

Although the default scheme covers most of the daily note-taking, Luhmann’s Zettelkasten knowledge management system requires slightly different templates, hence the name zettel scheme.

The following example illustrates the header fields of a typical zettel scheme note file:

---
title:    Lemon
keywords:
          - fruit
          - round
          - sour taste
scheme:   zettel
sort_tag: 2b3
---

The [lemon] belongs to the Rutaceae family.
[lemon]: https://en.wikipedia.org/wiki/Lemon

The filename synchronization template from the zettel scheme set looks like (simplified):

{{ fm.fm_sort_tag }}--{{ fm.fm_title }}__{{ fm.fm_keywords }}.{{ fm.fm_file_ext }}

It generates the following filename:

2b3--Lemon__fruit_round_sour taste.md

πŸ› οΈ CUSTOMIZATION πŸ› οΈ

Tp-Note is shipped with a default internal configuration that can be customized by merging a series of configuration files from various locations into the default values. This happens in the following order:

  1. Unix and macOS only: /etc/tpnote/tpnote.toml
  2. The user’s configuration file:
    • Unix: ~/.config/tpnote/tpnote.toml
    • Windows: C:\Users\<LOGIN>\AppData\Roaming\tpnote\config\tpnote.toml>
    • macOS: /Users/<LOGIN>/Library/Application Support/tpnote

If the environment variable TPNOTE_CONFIG is set, its value is used as the path to this file instead of the standard location above.

  1. At startup all parent directories of the note file path <PATH> are searched for a file named tpnote.toml. If present and its content is not empty, Tp-Note interprets the file’s content as configuration file. Such a file can additionally set the two root-level variables is_root_path_marker and merge_parent_config (cf. β€œSearching for project configuration files” below).
  2. The file indicated by the command line parameter --config <FIlE>.

Okay so real quick on names: this manual calls these locations by their scope β€” the file from 1. is the system configuration file, the one from 2. is the user configuration file, and every tpnote.toml that the upward search of 3. turns up is a project configuration file. They are all tpnote.toml files with the same syntax and the same variables; only their location differs, and with it their precedence and reach β€” a project configuration file applies to the notes below its own directory alone. Two variables are meaningful in a project configuration file only, because they steer the very search that finds it: is_root_path_marker and merge_parent_config.

When Tp-Note starts, it merges all available configuration files, in the order above, into the default configuration. Each file is applied one at a time and kept only if the result still parses into a valid configuration; an invalid file (bad syntax, wrong value, unknown key) is skipped with a logged warning instead of discarding every other, correctly loaded file. For debugging, you can print out the merged result with -V -b -d trace.

tpnote -V -b -d trace  |less

To write a custom configuration file, generate a template with -C:

tpnote -C ~/.config/tpnote/tpnote.toml

The template shows all variables with their default values. When you change a value, do not forget to uncomment the modified line to activate your change. Also make sure to keep the version variable at the beginning of the file commented out. As any Tp-Note upgrade might include a breaking change in the configuration file structure, try to keep your custom configuration small.

πŸ”Ž Searching for project configuration files πŸ”Ž

This single search decides two things at once: where the document root lies, and which project configuration files’ settings get merged into the configuration. Two variables in a project configuration file control both:

  • is_root_path_marker (default: true) β€” the first project configuration file found, starting with the one closest to <PATH>, whose value is true (or that leaves the variable unset, the default for every project configuration file written before this option existed) fixes the document root at its directory, and the search for the document root stops there.
  • merge_parent_config (default: false) β€” if true, Tp-Note keeps searching further up after finding this project configuration file, merging any additional tpnote.toml files it finds underneath the current configuration (settings closer to the note file take precedence). This does not move the document root, which stays fixed wherever is_root_path_marker stopped it. The search keeps climbing only as long as each successive project configuration file it finds also sets merge_parent_config = true; the first ancestor that leaves it at the default false ends the search.

Both are root-level variables, so β€” as TOML has no syntax to leave a table once it is open β€” they must appear at the top of the file, before its first [table] header. Placed after one, they silently become that table’s variables instead, where they are unknown: the file is then skipped with a warning (cf. β€œEXIT STATUS”, status 5).

Security note: enabling merge_parent_config means Tp-Note will read and merge configuration from every ancestor directory up to wherever the search stops, including its editor and browser launch commands. Only enable this pointing through directories you trust β€” for example, a directory shared with other users. If a subproject should inherit settings from one specific ancestor only, place a plain tpnote.toml at that ancestor without merge_parent_config set: its default false value stops the search there, rather than leaving it open-ended all the way to the filesystem root.

Example with one project configuration file. Say ~/notes/project/ holds both the note being edited and the following tpnote.toml:

[arg_default]
scheme = "zettel"

Neither of the two variables is set here, so both stay at their defaults. Climbing upward from ~/notes/project/, Tp-Note finds this one project configuration file and stops there: with is_root_path_marker at its default true, that directory becomes the document root, and with merge_parent_config at its default false, nothing above it is ever consulted. Only this file’s own settings apply.

Example with two project configuration files. Now add a second, vault-wide tpnote.toml one level up, in ~/notes/:

[viewer]
startup_delay = 500

and change ~/notes/project/tpnote.toml to:

merge_parent_config = true

[arg_default]
scheme = "zettel"

Climbing from ~/notes/project/ still finds the project’s own configuration file first, and it still fixes the document root there: setting merge_parent_config does not change is_root_path_marker, which is still left at its default true. But because merge_parent_config is now true, the search keeps going, finds ~/notes/tpnote.toml, and merges its [viewer] setting in underneath the project’s own β€” so a note in ~/notes/project/ ends up with both arg_default.scheme = "zettel" and viewer.startup_delay = 500, while the project’s own configuration file only had to state the one setting it actually overrides. The document root stays ~/notes/project/ throughout.

Example moving the document root. Change ~/notes/project/tpnote.toml once more, adding is_root_path_marker = false:

is_root_path_marker = false
merge_parent_config = true

[arg_default]
scheme = "zettel"

Now the project’s own configuration file no longer fixes the document root there, so the search for the document root keeps going, reaches ~/notes/tpnote.toml (which leaves is_root_path_marker at its default true), and the document root becomes ~/notes/ instead of ~/notes/project/. merge_parent_config is still true, so ~/notes/tpnote.toml’s settings are still merged in exactly as in the previous example. Moving the document root and cascading the configuration are independent effects of the same climb, each controlled by its own variable.


Some filename and template related variables are grouped into a scheme. The shipped configuration file lists two schemes: default and zettel. The scheme used when creating a new note is selected by the command line option --scheme, the environment variable TPNOTE_SCHEME or the configuration variable arg_default.scheme. The scheme selected when synchronizing a Tp-Note header with its filename depends on the value of the header variable scheme: which defaults to default (cf. scheme_sync_default).

Note, that the merging algorithm merges all values, except arrays. These are usually replaced by the subsequent configuration file. There is one exception though: top level arrays are also merged. An example of this is the top level array [[scheme]]. In the following example we overwrite the variable extension_default in the scheme default. All other variables remain untouched.

[[scheme]]
name="default"
[scheme.filename]
extension_default = "txt"

When your changes apply to all schemes, the best way is to modify the base scheme (base_scheme) instead, since all schemes inherit their (default) values from the base scheme:

[base_scheme.filename]
extension_default = "txt"

Make sure, that your change is not explicitly overwritten in a scheme definition, as the latter takes precedence.

To add a custom scheme you must explicitly overwrite all variables that differ from the base scheme base_scheme:

[[scheme]]
name="my-custom-scheme"
[scheme.filename]
# Insert all variables here, that differ from `base_scheme`.
[scheme.tmpl]
# Insert all variables here, that differ from `base_scheme`.

The following example illustrates how non-top-level arrays are overwritten by the subsequent configuration file. The default configuration lists about 20 MIME types. After merging the following example, the configuration lists only the two MIME types jpeg and jpg in served_mime_types.

[viewer]
served_mime_types = [
    ["jpeg", "image/jpeg"],
    ["jpg", "image/jpeg"],
]

πŸ“ Register your own text editor πŸ“

There are two ways to modify the default file editor, Tp-Note launches when it starts: either you can modify the configuration file variables app_args.*.editor and app_args.*.editor_console, or alternatively, you can set the TPNOTE_EDITOR environment variable (cf. examples in the chapter ENVIRONMENT_VARIABLES below).

The configuration file variables app_args.unix.editor and app_args.unix.editor_console define lists of external text editors to be launched for editing. The lists contain by default well-known text editor names and their command line arguments for Unix-like operating systems. For other systems consult: app_args.windows.editor, app_args.windows.editor_console, app_args.macos.editor and app_args.macos.editor_console. Tp-Note tries to launch every text editor in app_args.*.editor from the beginning of the list until it finds an installed text editor. When Tp-Note is started on a Linux console, the list app_args.*.editor_console is used instead. Here you can register text editors that do not require a graphical environment, e.g. vim or nano. In order to use your own text editor, just place it at the top of the list. To debug your changes invoke Tp-Note with tpnote --debug debug --popup --edit.

The following example showcases the configuration for the Kate file editor. The entry kate launches the binary, while the command line parameter --block guarantees, that the launched process blocks until the user closes the editor. Tp-Note detects the end of the process, checks if the title of the note files has changed in its YAML header and renames the note file if necessary.

[app_args]
unix.editor = [
  [
    "kate",
    "--block"
  ]
]

The equivalent configuration with environment variable:

TPNOTE_EDITOR="kate --block" tpnote

All items in the above list are subject to limited template expansion allowing to insert the value of environment variables. Consider the following example:

[app_args]
windows.editor = [
    [
        "{{get_env(name=\"LOCALAPPDATA\")}}\\Programs\\Microsoft VS Code\\Code.exe",
        "-n",
        "-w",
    ]
]

When the config file loads, that expression {{ get_env(name="LOCALAPPDATA") }} expands on Windows for a user named Joe into C:\User\Joe\AppData\Local, which gives you:

[app_args]
windows.editor = [
    [
    "C:\\User\\Joe\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe",
    "--new-window", "--wait",
    ]
]

Okay so the general rule when you're wiring Tp-Note up to your text editor: make sure your editor does not fork. 🫢 How do you check? Launch it from the command line, bestie. If the prompt comes right back at you, that editor forks the process (rude). If the prompt only reappears when you actually close the editor β€” everything's fine, slay. πŸ’… Lots of editors give you a flag to stop the forking: --wait for Visual Studio Code, --nofork for Vim, --block for Kate.

Tp-Note does still work with forking editors, but like… you should avoid it, and there's a workaround. Peep this:

$ TPNOTE_EDITOR="kate" tpnote
/home/getreu/20230714-getreu--Note.md
$

In the example above, Tp-Note launches kate in forking mode because the --block flag is missing. Internally the editor-process launch returns immediately, so Tp-Note has no way to know when exactly you closed the editor. Which means: it can't check whether you changed the note's header, and no filename synchronization happens afterwards. 😭

Bestie workaround: manually trigger the filename sync after editing with tpnote --batch "$FILE":
FILE=$(tpnote --batch) # Create the new note.
tpnote --view "$FILE"& # Launch Tp-Note's viewer.
kate "$FILE"           # Note, the prompt returns immediatly as the editor forks.
                       # After closing the editor when editing is done...
tpnote --batch "$FILE" # Synchronize the note's filename again.

Where FILE=$(tpnote --batch) creates the note file, kate "$FILE" opens the text editor, and tpnote --batch "$FILE" synchronizes the filename after editing.

NB: try to avoid forking at all cost. As mentioned above, most text editors have a command line flag to prevent the process from forking:
TPNOTE_EDITOR="kate --block" tpnote

πŸŽ€ Register a Flatpak Markdown editor

Flathub for Linux is a cross-platform application repository that works so well with Tp-Note, fr fr. To show you how it goes, we're adding a Tp-Note launcher for the Mark Text Markdown text editor, which is available as a Flatpak package. Before installing, make sure you've set up Flatpak correctly. Then install the app with:

sudo flatpak install flathub com.github.marktext.marktext

To test it, run Mark Text from the command line:

flatpak run com.github.marktext.marktext

Then drop a Tp-Note config in its search path (e.g. ~/.config/tpnote/tpnote.toml) with this inside:

[app_args]
unix.editor = [ [ "flatpak", "run", "com.github.marktext.marktext", ] ]

The structure of this variable is a list of lists. Every item in the outer list is one entire command line launching a different text editor β€” here, Marktext. When launching, Tp-Note searches through the list until it finds an installed text editor on the system. πŸ‘‘

Save the modified config file. Next time you launch Tp-Note, the Mark Text editor opens. Obsessed.

πŸ¦‹ Register a console text editor running in a terminal emulator

In this setup, Tp-Note launches the terminal emulator, which is configured to launch the text editor as a child process. Neither process should fork when they start (see above, bestie πŸ’–).

Here are some examples you can adjust to your needs and taste:

  • Neovim in Xfce4-Terminal:
[app_args]
unix.editor = [
  [
    "xfce4-terminal",
    "--disable-server",
    "-x",
    "nvim",
    "+colorscheme pablo",
    "+set syntax=markdown",
  ],
]
  • Helix-editor in XFCE4-Terminal:
[app_args]
unix.editor = [
  [
    "xfce4-terminal",
    "--disable-server",
    "-x",
    "hx",
  ],
]
  • Helix in LXTerminal:
[app_args]
unix.editor = [
  [
    "lxterminal",
    "--no-remote",
    "-e",
    "hx",
  ],
]
  • Helix in Xterm:
[app_args]
unix.editor = [
  [
    "xterm",
    "-fa",
    "DejaVu Sans Mono",
    "-fs",
    "12",
    "-e",
    "hx",
  ],
]
  • Helix in Alacritty:
[app_args]
unix.editor = [
  [
    "alacritty",
    "-e",
    "hx",
  ],
]
  • Flatpack Helix in XFCE4 terminal:
[app_args]
unix.editor = [
  [
    "xfce4-terminal", "--disable-server", "-x",
    "flatpak", "run", "com.helix_editor.Helix",
  ],
]

unix.editor_console = [
  [
    "flatpak", "run", "com.helix_editor.Helix"
  ],
]

🌸 Change the file extension for new note files

Tp-Note figures out a note's markup language from its file extension and renders the content accordingly (see the filename.extensions variable). For example, the filename.extensions variable lists some extensions that are regarded as Markdown files:

[[scheme]]
name = "default"
[scheme.filename]
extensions = [
  ["txt", "ToMarkdown", "Markdown"],
  ["md", "ToMarkdown", "Markdown"],
  ["rst", "Disabled", "ReStructuredText"],
  ["htmlnote", "PassThrough", "Html"],
  ["txtnote", "Disabled", "PlainText"],
  ["adoc", "Disabled", "PlainText"],
  ["text", "ToMarkdown", "Markdown"],
  ["markdn", "ToMarkdown", "Markdown"],
  ["markdown", "ToMarkdown", "Markdown"],
]

The default file extension for new note files is defined as:

[base_scheme.filename]
extension_default = "md"

If you'd rather have the extension .markdown for new notes, write a config file with:

[base_scheme.filename]
extension_default = "markdown"

This modification does not change how the note file's content is interpreted β€” in this case as Markdown β€” because both .md and .markdown are rendered as Markdown according to filename.extensions. πŸ’…

✨ Configure the natural language detection algorithm

When creating a new header for a new or an existing note file, a linguistic language detection algorithm tries to figure out in what natural language the note file is authored. Depending on the context, the algorithm processes as input: the header field title: or the first sentence of the text body. The algorithm is implemented as a template filter named get_lang, which is used in various Tera content templates tmpl.*_content in Tp-Note's configuration file. The filter get_lang is parametrized by the configuration variables tmpl.filter.get_lang.*, containing e.g. a list of ISO 639-1 encoded languages that the algorithm considers potential detection candidates, e.g.:

[base_scheme.tmpl]
filter.get_lang.language_candidates = [ "en", "fr", "de", "et" ]

Since natural language detection is CPU intensive, it's advised to limit the number of detection candidates to 5 or 6, depending on how fast your computer is. The more language candidates you include, the longer the note file creation takes. As a rule of thumb, with all languages enabled the creation of new notes can take up to 4 seconds on my computer. 😭 Nevertheless, it is possible to enable all available detection candidates with the empty array:

[base_scheme.tmpl]
filter.get_lang.language_candidates = []

If the input text is usually written in one language only, set:

[base_scheme.tmpl]
filter.get_lang.mode = "Monolingual"

The algorithm can also search for multiple languages in the input text:

[base_scheme.tmpl]
filter.get_lang.mode = "Multilingual"

The language detection algorithm can be fine-tuned with:

[base_scheme.tmpl]
filter.get_lang.minimum_relative_distance = 0.3
filter.get_lang.consecutive_words_min  = 7
filter.get_lang.words_total_percentage_min = 10

These parameters can reduce false positives when determining the natural language with the get_lang filter. Setting them to 0 finds more languages but leads to more false positives. A higher value enforces criteria to ignore some words when guessing the languages. The minimum_relative_distance excludes words that appear in more than one language β€” valid values are between 0.0 and 0.99. consecutive_words_min sets the minimum of consecutive words in one language to be considered. words_total_percentage_min sets the minimum word count ratio in percent a language must appear in the input text to be reported.

Once the language is detected with the get_lang filter, it passes another filter called map_lang. 🧚 This one maps the result of get_lang β€” encoded as ISO 639-1 codes β€” to IETF language tags. For example, en is replaced with en-US, or de with de-DE. This extra filtering is useful because the detection algorithm cannot figure out the region code (e.g. -US or -DE) by itself. Instead, the region code is appended in a separate processing step. Spell checker or grammar checker like [LTeX] rely on this region information to work properly.

The corresponding configuration looks like this:

[base_scheme.tmpl]
filter.get_lang.language_candidates = [ "en", "fr", "de", "et" ]
filter.map_lang = [
    [ "en", "en-US", ],
    [ "de", "de-DE", ],
]

So when the user's region setting β€” the one the operating system's locale reports β€” isn't in that list above, Tp-Note just automatically appends it as an extra internal mapping. πŸ’… And when the map_lang filter bumps into a language code it has no mapping for, it forwards the input language code as-is, no modifications, no drama: input fr gives you output fr. And heads up β€” subsequent entries that differ only in the region subtag, like ['en', 'en- GB'], ['en', 'en-US'], are ignored. πŸ™ˆ

Lowkey important note: the environment variable TPNOTE_LANG_DETECTION β€” if it's set β€” takes precedence over tmpl.filter.get_lang.language_candidates and tmpl.filter.map_lang. Which means you can configure language detection system-wide without ever touching Tp-Note's configuration file. ✨ This example does the exact same thing as the config above:

TPNOTE_LANG_DETECTION="en-US, fr, de-DE, et" tpnote

Want all the language detection candidates? Drop the pseudo tag +all somewhere in the list:

TPNOTE_LANG_DETECTION="en-US, de-DE, +all" tpnote

In that example the IETF language tags en-US and de-DE are kept so that the region codes US and DE used by the map_lang template filter stay configured. 🫢

For debugging, go stare at the value of SETTINGS in the debug log with:

tpnote -d trace -b

And if you'd rather not have Tp-Note detecting languages at all:

[base_scheme.tmpl]
filter.get_lang.mode = "Disabled"

Same energy, different route:

TPNOTE_LANG_DETECTION="" tpnote

πŸŽ€ Localize the note's front matter πŸŽ€

By default, the front matter variable names get printed in English when new note files are created from templates. Like, the header variable fm.fm_subtitle shows up as subtitle: in the note's header. That's the default vibe.

This translation relation lives in the configuration file variable scheme.tmpl.fm_var.localization. Here's a simplified example, bestie:

[base_scheme.tmpl]
fm_var.localization = [
    ["fm_title", "title"],
    ["fm_subtitle", "subtitle"],
    ["fm_author", "author"],
    ["fm_date", "date"],
    ["fm_lang", "lang"],
    ["fm_languages", "languages"],
    ["fm_sort_tag", "sort_tag"],
    ["fm_file_ext", "file_ext"],
    ["fm_no_filename_sync", "no_filename_sync"],
    ["fm_filename_sync", "filename_sync"],
    ["fm_scheme", "scheme"],
]

To change the natural language of the displayed header variable names, you just edit the second column of that table. For example:

[base_scheme.tmpl]
fm_var.localization = [
    ["fm_title", "Titel"],
    ["fm_subtitle", "Untertitel"],
    ["fm_author", "Autor"],
    ["fm_date", "Datum"],
    ["fm_lang", "Sprache"],
    ["fm_languages", "Sprachen"],
    ["fm_sort_tag", "Kennzeichen"],
    ["fm_file_ext", "Dateierweiterung"],
    ["fm_no_filename_sync", "Keine_Sync"],
    ["fm_filename_sync", "Dateinamensync"],
    ["fm_scheme", "Schema"],
]
⚠️ Keep in mind, the templates do not change! πŸ”₯ Header variables in templates always start with the identifier fm.fm_ (peep the first column of the table above).

The config above changes the localization default for all note files within the scope of the configuration file tpnote.toml. But you can also run header variables in different languages side by side, using schemes:

[[scheme]]
name = 'Deutsch'
[scheme.tmpl]
fm_var.localization = [
    ["fm_title", "Titel"],
    ["fm_subtitle", "Untertitel"],
    ["fm_author", "Autor"],
    ["fm_date", "Datum"],
    ["fm_lang", "Sprache"],
    ["fm_languages", "Sprachen"],
    ["fm_sort_tag", "Kennzeichen"],
    ["fm_file_ext", "Dateierweiterung"],
    ["fm_no_filename_sync", "Keine_Sync"],
    ["fm_filename_sync", "Dateinamensync"],
    ["fm_scheme", "Schema"],
]

That defines an extra scheme called β€œDeutsch”. To use it, invoke Tp-Note with tpnote --scheme Deutsch. It creates this note file:

---
Titel:        getreu
Untertitel:   Note
Autor:        Getreu
Datum:        2025-04-23
Sprache:      de-DE
Schema:       Deutsch
---

And when you later reopen that note file with tpnote 20250423-getreu--Note.md, Tp-Note figures out its scheme Deutsch from the line Schema: Deutsch. Then, using the associated localization table above, it translates the keys back to the English fm.fm_* versions. Iconic behavior, honestly. πŸ‘‘

πŸ’œ Note that the (de-)localization only applies to root level keys (like fm_foo in fm.fm_foo). All nested key names (like baz in fm.fm_foo.baz) are never translated. Periodt.

🌈 Change the default markup language 🌈

Tp-Note's core functionality β€” managing note file headers and filenames β€” is markup language agnostic. But among the shipped templates there's exactly one content template, tmpl.annotate_file_content, that generates a hyperlink. And since hyperlink syntax varies by markup language, Tp-Note's default internal tmpl.annotate_file_content template handles β€œMarkdown” and β€œReStructuredText” syntax.

So if you want to switch the default markup language with filename.extension_default to something other than md or rst, do not forget to modify the tmpl.annotate_file_content content template too. πŸ™

😭 Heads up: Tp-Note's built-in viewer is not markup language agnostic. It's got three different markup renderers (cf. the section Customize the built-in note viewer):
  • Markdown (file extension .md)
  • ReStructuredText (file extension .rst) and
  • PlainText (Link only renderer, file extension .txtnote)

πŸ¦‹ Change the default markup language to ReStructuredText πŸ¦‹

Tp-Note's core function is a template system, so it depends very little on the markup language you're using. The default templates are designed so they contain almost no markup-specific code. There's one little exception though, in the annotate_file_content template (see the previous section, bestie).

When you open an existing note file, Tp-Note detects the note file's markup language from its file extension. To open a note written in ReStructuredText, just type:

tpnote mynote.rst

To create a new ReStructuredText note, invoke Tp-Note with:

TPNOTE_EXTENSION_DEFAULT=rst tpnote

You can also change the markup language of one specific note file by adding the variable file_ext: to its YAML header. For ReStructuredText, add:

---
title:    some note
file_ext: rst
---

To make ReStructuredText the default markup language for all future new notes, write a configuration file ~/.config/tpnote/tpnote.toml with this content:

[base_scheme.filename]
extension_default="rst"

Or, if you want the change to only apply to notes created with the zettel scheme set:

[[scheme]]
name = 'zettel'
[scheme.filename]
extension_default="rst"

That last one overwrites the extension_default variable of the zettel scheme but leaves the default scheme completely untouched. πŸ’…


⭐ Change the sort tag character set ⭐

Sort-tags for new notes are generated with the tmpl.*_filename templates. Before you go changing the sort-tag generation scheme in those templates, make sure you've enabled the right set of potential sort-tag characters.

In the default scheme, the digits 0-9, all lower case letters, and the characters _, -, . are recognized as being part of a sort tag when they appear at the beginning of a filename. This character set can be modified with the filename.sort_tag.extra_chars configuration variable.

If defined, the filename.sort_tag.separator (by default -) marks the end of a sort tag without being part of it. On top of that, one special character filename.sort_tag.extra_separator (by default '') might be inserted by the filename template directly after the - to avoid ambiguity. πŸŽ€


πŸ’« Customize the filename synchronization scheme πŸ’«

The filename synchronization scheme is fully customizable through Tp-Note's filename templates. To design a custom one, start by setting up your synchronization rules in the tmpl.sync_filename template. Then adjust all tmpl.*_filename templates to comply with those rules.

To verify your design, check that this holds for any sequential application of one tmpl.*_filename template followed directly by the tmpl.sync_filename template: the latter should never change the filename that was initially set up by any tmpl.*_filename template. ✨

Secondly, make sure that in filename templates tmpl.*_filename, sort-tags {{ path | file_sort_tag }} are never inserted directly. Instead, prepend the sort_tag with prepend(with_sort_tag=path|file_sort_tag) to the following expression, like:

{{ fm.fm_title | sanit | prepend(with_sort_tag=path|file_sort_tag) }}

The filter prepend(with_sort_tag=<...>) decides whether to insert the sort_tag.separator="-" and/or the sort_tag.extra_separator="'" characters. These heuristics let Tp-Note identify sort-tags in filenames unequivocally, which avoids potential cyclic filename changes. Or, to say it differently: the tmpl.sync_filname template must always give the same result, even after repeated application. πŸ”

To debug your tmpl.sync_filename template, create a test note file test.md and invoke Tp-Note with --debug trace and --batch:

tpnote --batch --debug trace test.md

πŸ“ Store new note files by default in a subdirectory πŸ“

When you're annotating an existing file on disk, the new note file goes in the same directory by default. To configure Tp-Note to store the new note file in a subdirectory instead β€” let's say Notes/ β€” you need to modify the templates scheme.tmpl.annotate_file_filename and scheme.tmpl.annotate_file_content.

First, create a configuration file ~/.config/tpnote/tpnote.toml with:

[base_scheme.tmpl]
annotate_file_content = """
COMPLETE HERE
"""

annotate_file_filename = """
COMPLETE HERE
"""

In the above, replace the string COMPLETE HERE with the default values for the variables you get with tpnote -C - | less. 🫢

Then, replace in annotate_file_filename the string:

{{ fm.fm_title | sanit | prepend(with_sort_tag=tag) }}\

with:

Notes/{{ fm.fm_title | sanit | prepend(with_sort_tag=tag) }}\

and inside annotate_file_content:

[{{ path | file_name }}](<{{ path | file_name }}>)

swap it for:

[{{ path | file_name }}](<../{{ path | file_name }}>)

Okay so important vibe check: the zettel scheme overwrites the base annotate_file_filename template. So if you're also running the zettel scheme, you gotta repeat the whole section up there πŸ’… and then tack this onto your config file β€” and yeah, repeat the above again:

[[scheme]]
name = 'zettel'
[scheme.tmpl]
annotate_file_content = """
COMPLETE HERE
"""

annotate_file_filename = """
COMPLETE HERE
"""

Wanna test your config? Just drop a test.pdf file in the current directory and annotate it like:

tpnote test.pdf

This should create a brand-new note file at ./Notes/test.pdf--Note.md and open your web browser with a link to test.pdf ✨. Click that link and the PDF page should show up. FYI the default behaviour, like without any of this customization, is to create the new note file ./test.pdf--Note.md right in the current directory.

And to test the zettel scheme config, invoke Tp-Note with:

tpnote -s zettel test.pdf

πŸŽ€ Customize the built-in note viewer πŸŽ€

Change the way how note files are rendered for viewing

So rn there are three markup renderers available: Markdown, ReStructuredText and PlainText. The config file variable filename.extensions links up several note file extensions with one of these renderers. If none of them are your vibe, you can totally disable the viewer feature for one particular note file extension by hooking it up to the pseudo RendererDisabled renderer πŸ‘‘. And if you wanna disable the viewer feature entirely (like, for all file extensions), just set the variable arg_default.edit = true.

Delay the launch of the web browser

By default, Tp-Note launches two external programs: some text editor and a web browser. If you're into it, the config variable viewer.startup_delay lets you delay the web browser launch by some milliseconds ⏱️. That way the browser window will always pop up on top of the editor window, iconic. A negative value delays the start of the text editor instead. Slay.

Change the HTML rendition template

After the markup rendition process, Tp-Note's built-in viewer generates its final HTML rendition through the customizable HTML templates tmpl_html.viewer, tmpl_html.viewer_error and tmpl_html.exporter. Unlike content templates and filename templates, all HTML templates escape HTML-critical characters in variables by default. To disable escaping for a specific variable, add the safe filter in last position of the filter chain 🫢. But pls note: in general the safe filter is only recommended directly after the to_html and the markup_to_html filters, because those handle critical input by themselves. Ngl, this simplified code sample, inspired by the tmpl_html.viewer template, shows off the available variables:

[tmpl_html]
viewer = '''
{%- set ext = fm.fm_file_ext | default(value=extension_default ) -%}
<!DOCTYPE html>
<html lang="{{ fm.fm_lang | default(value='en') }}">
<head>
<meta charset="utf-8">
<title>{{ fm.fm_title }}</title>
<link rel="stylesheet" href="{{ viewer_doc_css_path }}">
<link rel="stylesheet" href="{{ viewer_highlighting_css_path }}">
  </head>
  <body>
  <pre class="doc-header">{{ doc_fm_text }}</pre>
  <hr>
  <div class="doc-body">
    {{ doc.body | markup_to_html(extension=ext) | safe }}
  </div>
  <script>{{ viewer_doc_js | safe }}</script>
</body>
</html>
'''

Specifically πŸ‘‡

  • {{ fm.fm_* }} are the deserialized header variables. Heads up: the header variables may be localized, e.g. Untertitel. Nevertheless, in templates always use the English version, e.g. fm.fm_subtitle. All content template variables and filters are available β€” see the section Template variables above.
  • {{ viewer_doc_css_path }} is the CSS stylesheet path required to format an HTML rendition of a Tp-Note document. This path is an internal constant and understood by Tp-Note's internal web server πŸ’….
  • {{ viewer_highlighting_css_path }} is the CSS stylesheet path required to highlight embedded source code. This path is an internal constant and it's understood by Tp-Note's internal web server ✨.
  • {{ doc.header }} is the raw UTF-8 copy of the header. Not to be confounded with the dictionary variable {{ fm }}.
  • {{ doc.body | markup_to_html(extension=ext) | safe }} is the note's body as an HTML rendition. The parameter extension designates the markup language as specified in the filename.extensions-* variables.
  • {{ viewer_doc_js | safe }} is the JavaScript browser code for live updates πŸ”₯.
  • {{ extension_default }} (c.f. section Template variables).
  • {{ username }} (c.f. section Template variables).
  • {{ lang }} (c.f. section Template variables).
  • {{ my_val | to_html | safe }} is the HTML rendition of the my_val variable (c.f. section Template filter).
  • {{'fm.fm_title' | name}} prints the localized name of the fm.fm_title variable, e.g. title in English or Titel in German πŸŽ€.

Alternatively, the header enclosed by <pre>...</pre> can also be rendered as a table:

<table class="fm">
  <tr>
    <th class="fmkey">{{'fm.fm_title' | name}}:</th>
    <th class="fmval">
      <b> {{ fm.fm_title| default(value='') | to_html | safe }}</b>
    </th>
  </tr>
  <tr>
    <th class="fmkey">{{'fm.fm_subtitle' | name}}:</th>
    <th class="fmval">
      {{ fm.fm_subtitle | default(value='') | to_html | safe }}
    </th>
  </tr>
  {% for k, v in fm | remove(key='fm_title')| remove(key='fm_subtitle')| %}
  <tr>
    <th class="fmkeygrey">{{ k | name }}:</th>
    <th class="fmvalgrey">{{ v | to_html | safe }}</th>
  </tr>
  {% endfor %}
</table>

The error page template tmpl_html.viewer_error (see below) does NOT provide fm.fm_* variables β€” because of possible header syntax errors, bestie 😭. Instead, the variable {{ doc_error }} contains the error message as raw UTF-8. The body of the erroneous document is rendered using the file's own markup language renderer (determined by {{ path | file_ext }}). If that renderer produces no output β€” e.g. because the renderer feature is not compiled in β€” the fallback PlainText renderer (same as txtnote) is used, which renders the text verbatim with clickable hyperlinks ✨:

[tmpl_html]
viewer_error = '''
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<title>Syntax error</title>
</head>
<body>
<h3>Syntax error</h3>
<p> in note file: <pre>{{ path }}</pre><p>
<div class=\"note-error\">
<hr>
<pre>{{ doc_error }}</pre>
<hr>
</div>
{%- set ext = path | file_ext -%}
{%- set doc_body_html = doc_text | markup_to_html(extension=ext) -%}
{%- if doc_body_html -%}
{{ doc_body_html | safe }}
{%- else -%}
{{ doc_text | markup_to_html(extension='txtnote') | safe }}
{%- endif -%}
<script>{{ viewer_doc_js | safe }}</script>
</body>
</html>
'''

Customize the built-in HTML exporter

Customizing Tp-Note's HTML export function works the exact same way as customizing the built-in viewer πŸ’…. There are some slight differences though: the role of the tmpl_html.viewer template β€” discussed up above β€” is taken over by the tmpl_html.exporter template:

[tmpl_html]
exporter = '''
{%- set ext = fm.fm_file_ext | default(value=extension_default ) -%}
<!DOCTYPE html>
<html lang="{{ fm.fm_lang | default(value='en') }}">
<head>
<meta charset="utf-8">
<title>{{ fm.fm_title }}</title>
<style>
{{ exporter_doc_css | safe }}
{{ exporter_highlighting_css | safe }}
</style>
</head>
<body>
  <pre class="doc-header">{{ doc_fm_text }}</pre>
  <hr>
  <div class="doc-body">
    {{ doc.body | markup_to_html(extension=ext) | safe }}
  </div>
</body>
</html>
'''

In this template the same Tera variables as in tmpl_html.viewer are available β€” with one exception: {{ note_js }}, which doesn't make sense in this context πŸ™ˆ. Since the exporter prints any possible rendition error messages on the console, there's no equivalent to the tmpl_html.viewer_error template. Note: in contrast to the previous tmpl_html.viewer example, the source code highlighting CSS code is now embedded into the HTML output with:

<style>
  {{ exporter_doc_css | safe }}
  {{ exporter_highlighting_css | safe }}
</style>

Heads up: the safe filter disables the escaping of critical characters in the CSS input. We have no security concerns in this context, because we have full control over the CSS input coming from the config file variables tmpl_html.exporter_doc_css and tmpl_html.exporter_highlighting_theme. Periodt.

Choose your favourite web browser as note viewer

Once the note is rendered into HTML, Tp-Note's internal HTTP server connects to a random port at the localhost interface where the rendition is served so you can view it with a web browser 🌈. Tp-Note's config file contains a list app_args.unix.browser with common web browsers and their usual location on Unix-like operating systems. For other systems, consult app_args.windows.browser and app_args.macos.browser. This list is executed top down until a web browser is found and launched πŸ”₯. If you wanna view your notes with a different web browser, just overwrite the internal app_args.unix.browser list and put your favourite browser on top:

[app_args]
unix.browser = [[ "chromium", "--new-window", "--incognito"]]

A sandboxed Flatpak browser works too, for example Chrome πŸ’–. But heads up: a Flatpak browser's OS user cannot be determined by the viewer, so by default (viewer.same_user_policy = "Enforce") its connection is refused 😭 β€” set viewer.same_user_policy = "Off" as well (cf. SECURITY AND PRIVACY CONSIDERATIONS below):

[app_args]
unix.browser = [
  [ "flatpak", "run", "com.google.Chrome", "--new-window", "--incognito" ],
]

[viewer]
same_user_policy = "Off"

Orrr you can set the TPNOTE_BROWSER environment variable instead β€” the examples for that live down in the ENVIRONMENT_VARIABLES chapter below πŸ’–.

So what happens if none of the browsers you listed can be found? Tp-Note drops into a fallback mode with limited functionality, where it tries to open the system's default web browser. The catch, bestie: in fallback mode Tp-Note cannot detect when you close the web browser 😭, and that can lead to situations where Tp-Note's internal HTTP server shuts down too early. So if you wanna check that Tp-Note actually finds the web browser you picked, run it like this:

tpnote --debug debug --popup --view

🌸 TEMPLATES 🌸

Every single one of Tp-Note's workflows is customizable through its templates, which are grouped into the [scheme.tmpl] and [scheme.tmpl_html] sections of Tp-Note's configuration file ✨. This chapter is all about the [scheme.tmpl] templates β€” the ones responsible for generating Tp-Note files. The [scheme.tmpl_html] templates only concern Tp-Note's viewer feature, and those get their own discussion in the chapters Customize the built-in note viewer and Choose your favourite web browser as note viewer 🫢.

Tp-Note captures and stores its environment in Tera variables. For example, the variable {{ dir_path }} is initialized with the note's target directory, and {{ clipboard }} contains the content of the clipboard. Wanna learn more about Tera variables? Launch Tp-Note with the --debug trace option and just watch what info it scoops up from its environment πŸ‘€.

Template types

The content of a new note is composed by one of Tp-Note's internal customizable templates β€” hence the name Tp-Note, where Tp stands for "template" πŸ’…. Which internal template gets applied depends on the context in which Tp-Note is invoked: e.g. the template used when creating a new note file in a given directory is called tmpl.from_dir_content.

All in all, there are 3 different tmpl.*_content templates:

  • tmpl.from_dir_content
  • tmpl.from_text_file_content
  • tmpl.annotate_file_content

In general, the templates are designed so that the text input stream β€” usually coming from the clipboard β€” ends up in the body of the note file, while the environment β€” like the username β€” ends up in the header of the note file πŸŽ€.

Once the content of the new note is set by one of the above content templates, another template type steps onto the scene: the so-called filename template. Each content template has a matching filename template, like:

  • tmpl.from_dir_filename
  • tmpl.from_text_file_filename
  • tmpl.annotate_file_filename
  • tmpl.sync_filename (no corresponding content template)

As the name suggests, a filename template's whole job is to determine the filename of the new note. It does this by evaluating (deserializing) its YAML header. The values of the note's YAML header fields are accessible in filename templates through various dynamically created {{ fm.fm_<key> }} template variables ✨. For example, the value of the YAML header field title: can be accessed with {{ fm.fm_title }}. Once the filename is set, Tp-Note writes out the new note on disk πŸ’Ύ.

Most of the above templates are dedicated to creating new note files. But two of them have a special role: prepend header to text file, and synchronize filename:

  • Prepend header to text file: when Tp-Note opens a regular text file without a YAML header and the --add-header flag is given (or arg_default.add_header = true is set in the configuration file), a new header is prepended. Its data originates mainly from the filename of the text file. The templates applied in this use case are: tmpl.from_text_file_content and tmpl.from_text_file_filename. Without --add-header, Tp-Note opens the plain Markdown file directly for editing and viewing, with zero modification πŸ™….
  • Synchronize filename: this function mode kicks in when [Tp-Note] opens an existing note file, right after its YAML header is evaluated. The extracted header information is then applied to the tmpl.sync_filename template, and the resulting filename is compared to the actual filename on disk. If they differ, [Tp-Note] renames the note file πŸ”„. Note that the tmpl.sync_filename template operates on its own, without a corresponding content template.

Note this lowkey important distinction: in the operation mode synchronize filename, the header data overwrites the filename of the note, whereas in the operation mode prepend header, the filename data is copied into the new prepended header. And keep in mind that even in the latter mode the filename might change slightly 😬 β€” because after the header creation with the tmpl.from_text_file_content template, the tmpl.from_text_file_filename template is applied, which might cause a slight filename modification due to its sanitization filters (cf. sanit() in the section Template filters).

The prepend header feature is disabled by default (arg_default.add_header = false). You can enable it either with the --add-header command line flag, or by setting arg_default.add_header = true in the configuration file. To disable all filename synchronization, set arg_default.no_filename_sync = true. That guarantees Tp-Note will never change either the filename or the YAML header of an existing file 🫢.

Want a more detailed description of templates and their defaults? Go consult the const definitions in Tp-Note's source code files config.rs and note.rs in the directory tpnote-lib/src/ πŸ“š.

Template variables

All Tera template variables and functions can be used inside Tp-Note's templates πŸ’–. For example, {{ get_env(name='LANG') }} gives you access to the LANG environment variable.

On top of that, Tp-Note defines the following variables:

  • {{ path }} β€” this is the canonicalized fully qualified path name corresponding to Tp-Note's positional command line parameter <path>. If no path was given on the command line, {{ path }} contains the current working directory path.
  • {{ dir_path }} β€” identical to {{ path }} with one exception: if {{ path }} points to a file, the last component (the file name) is omitted and only the directory path is retained. If {{ path }} points to a directory, {{ dir_path }} equals {{ path }}.
  • {{ doc_fm_text }} β€” the header as raw text of the file {{ path }} points to. Heads up: this variable is only available in the templates from_text_file_*, sync_filename and the HTML templates below.
  • {{ doc.body }} β€” the content of the file {{ path }} points to. If the file starts with a YAML header, its raw text is stored in {{ doc.header }}. Heads up again: these variables are only available in the templates from_text_file_content, sync_filename and in the HTML templates tmpl_html.*.
  • {{ doc_file_date }} β€” the file system creation date of the file {{ path }} points to. This variable is only available in the templates from_text_file_*, sync_filename and in HTML templates. This condition implies that {{ path }} points to a file. Note: on some platforms and with some filesystems, the variable {{ doc_file_date }} might not be available β€” in that case the file modification date is used instead.
  • {{ txt_clipboard.body }} β€” the complete plain/text clipboard text. If the clipboard's content starts with a YAML header, only the non-YAML content is retained. {{ txt_clipboard.header }} is the YAML header section of the clipboard data, if it exists; if not, the variable is empty.
  • {{ html_clipboard.body }} and {{ html_clipboard.header }} β€” contain the same text as their txt_clipboard.* counterparts, but as HTML πŸ¦‹. The HTML clipboard provides more information, e.g. hyperlinks contain a link destination besides their link text. In the TXT clipboard you see only link texts.
  • {{ stdin.body }} β€” the complete text content originating from the input stream stdin. This stream can replace the clipboard when it's not available. If the input stream's content starts with a YAML header, the latter does not appear in this variable. {{ stdin.header }} is the YAML section of the input stream, if one exists. Otherwise, the variable is still defined, but its value is the empty string.
  • {{ extension_default }} β€” the default extension for new notes (can be changed in the configuration file).
  • {{ username }} β€” the content of the first non-empty environment variable: TPNOTE_USER, LOGNAME, USER or USERNAME.
  • {{ lang }} β€” contains the user's language tag as defined in RFC 5646. Not to be confused with the UNIX LANG environment variable, from which this value is derived under Linux/macOS. Under Windows, the user's language tag is queried through the WinAPI. If defined, the environment variable TPNOTE_LANG overwrites the value of {{ lang }} (all operating systems).
  • {{ force_lang }} β€” a copy of the command line option --force-lang. If --force-lang=-, then the value of {{ lang }} is copied into {{ force_lang }}. This variable is always defined and is empty when the command line option is not set.

The following {{ fm.fm_* }} variables are typically generated after a content template was filled in with data: for example, a field named title: in the content template tmpl.from_dir_content will generate the variable fm.fm_title, which can then be used in the corresponding tmpl.from_dir_filename filename template ✨. {{ fm.fm_* }} variables are generated dynamically. That means a YAML front-matter variable foo: in a note will generate a {{ fm.fm_foo }} template variable. On the other hand, a missing foo: will cause {{ fm.fm_foo }} to be undefined. Also please note that the header variables may be localized, e.g. Untertitel: β€” but in templates, always use the English version, e.g. fm.fm_subtitle πŸ™.

It is to be observed that {{ fm.fm_* }} variables are not available in the tmpl.from_text_file_content content template πŸ’€.
  • {{ fm.fm_title }} β€” the title: as indicated in the YAML front-matter of the note.
  • {{ fm.fm_subtitle }} β€” the subtitle: as indicated in the YAML front matter of the note.
  • {{ fm.fm_author }} β€” the author: as indicated in the YAML front-matter of the note.
  • {{ fm.fm_lang }} β€” the lang: as indicated in the YAML front-matter of the note.
  • {{ fm.fm_file_ext }} β€” holds the value of that optional YAML header variable file_ext: (so like file_ext: rst). πŸ’–
    • {{ fm.fm_sort_tag }} β€” the sort tag variable as defined in the YAML front matter of this note (e.g. sort_tag: '20200312').
    • {{ fm }} β€” this one's a whole collection (map) of all the defined {{ fm.fm_* }} variables. It gets used in the tmpl.from_dir_content template, typically in a loop like this:
{{ fm.fm_title | to_yaml(key='fm_title') }}
{{ fm.fm_subtitle | to_yaml(key='fm_subtitle') }}
{{ fm  | remove(key='fm_title') | remove(key='fm_keywords') | to_yaml }}
⚠️ Okay bestie, important warning, listen up: there is no guarantee that any of the above {{ fm.fm_* }} variables are defined! Depending on the last content template result, certain variables might be undefined. And please take into consideration that a defined variable might contain the empty string ''. When you create a new note file with a content template, the note's header is parsed into {{ fm.fm_* }} variables. Those get type checked according to configurable rules, and the rules are defined in tmpl.filter.assert_precondition.

For a more detailed description of the available template variables, please consult the const definitions in Tp-Note's source code file note.rs. πŸ¦‹

🌸 Template filters 🌸

So besides Tera's built-in filters, Tp-Note comes with its own extra filters and functions, i.e.: append(newline=true), append(with=...), date(format=...), date, file_copy_counter, file_ext, file_name, file_sort_tag, file_stem, flatten_array, get_lang, heading, html_heading, html_to_markup(extension=..., default=...), insert(key=..., value=...), link_dest, link_text, link_title, map_lang, now(), prepend, prepend(newline=true), prepend(with=...), prepend(with_sort_tag=...), remove(key=...), replace_empty(with=...), sanit, to_html, to_yaml, to_yaml(key=...), to_yaml(tab=...) and trim_file_sort_tag. ✨

A filter is always used together with a variable. Here are some examples, bestie, take notes πŸ“:

  • {{ path | file_name }} β€” returns the final component of {{ path }}. If {{ path }} points to a file, the filter returns the complete filename including its sort tag, stem, copy-counter, dot and extension. If the <path> points to a directory, the filter returns the final directory name.
  • {{ path | file_sort_tag }} β€” this is the sort tag (that numerical filename prefix, you know the vibe) of the final component of {{ path }}, e.g. 01-23_9 or 20191022. It's similar to {{ path | file_name }} but without returning its stem, copy-counter and extension.
  • {{ path | file_sort_tag | assert_valid_sort_tag }} β€” does not change the above output, but the filter asserts at runtime that the resulting type is either String or Number and that all characters are part of the set filename.sort_tag.extra_chars. That extra runtime check simplifies template debugging β€” iconic for catching your oopsies early πŸ’….
  • {{ path | file_stem }} β€” similar to {{ path | file_name }} but without its sort tag, copy-counter and extension. Only the stem of {{ path }}'s last component is returned.
  • {{ path | file_copy_counter }} β€” similar to {{ path | file_name }} but without its sort tag, stem and extension. Only the copy counter of {{ path }}'s last component is returned.
  • {{ path | file_ext }} β€” is {{ path }}'s file extension without dot (period), e.g. txt or md.
  • {{ path | file_ext | prepend(with='.') }} β€” is {{ path }}'s file extension with dot (period), e.g. .txt or .md.
  • {{ path | trim_file_sort_tag }} β€” returns the final component of path which might be a directory name or a file name. Unlike the file_name filter (which also returns the final component), trim_file_sort_tag trims the sort tag if there is one.
  • {{ dir_path | find_last_created_file | incr_sort_tag(default="") }} β€” searches dir_path for the most recently created Tp-Note file, extracts the sort-tag from its file name and increments it. If the incremented sort-tag (e.g. 13) exists on disk in the note's directory already, the incrementation is performed by "branching" instead (e.g. 12a). If the incrementation fails, for example because the input sort-tag is a chronological sort-tag type or because it is empty, the default value is returned.
  • {{ dir_path | trim_file_sort_tag }} β€” returns the final component of dir_path (which is the final directory name in {{ path }}). Unlike the file_name filter (which also returns the final component), trim_file_sort_tag trims the sort tag if there is one.
  • {{ html_clipboard.body | html_to_markup(extension=e, default=d) }} β€” converts the clipboard's HTML content into the target markup language specified by {{ e }}, e.g. md. If the conversion fails or results in an empty string, stream the content of the variable {{ d }} instead.
  • {{ txt_clipboard.body | trunc }} β€” is the first 200 bytes from the clipboard.
  • {{ txt_clipboard.body | heading }} β€” is the clipboard's content until the end of the first sentence, or the first newline.
  • {{ html_clipboard.body | html_heading }} β€” searches in the HTML clipboard input for e.g <h2>HEADING</h2>, and returns the first HTML heading, e.g. HEADING.
  • {{ html_clipboard.body | link_text }} β€” is the name of the first Markdown or ReStructuredText formatted link in the clipboard.
  • {{ html_clipboard.body | link_dest }} β€” is the URL of the first Markdown or ReStructuredText formatted link in the clipboard.
  • {{ html_clipboard.body | link_title }} β€” is the title of the first Markdown or ReStructuredText formatted link in the clipboard.
  • {{ username | capitalize | to_yaml(key='author',tab=12) }} β€” is the capitalized YAML encoded username. As all YAML front-matter is YAML encoded, the to_yaml filter must be appended to any template variable placed in the front-matter block. The key='author' parameter prepends the key to the capitalized username, e.g.: autor: John. Note that the first letter of John starts at the tabulator position tab=12 πŸŽ€.
  • {{ fm.fm_subtitle | sanit }} β€” is the note's subtitle as defined in its front matter, sanitized in a file system friendly form. Special characters are omitted or replaced by - and _. See the section Filename template convention for more details about this filter.
  • {{ fm.fm_title | sanit | prepend(with_sort_tag=path|file_sort_tag) }} β€” is the note's title as defined in its front-matter. Same as above, but the title string is prepended with the note's sort_tag and with a filename.sort_tag.separator (by default -). Eventually, a second filename.sort_tag.extra_separator (by default '') is inserted after the first to guarantee that one of the separators unequivocally marks the end of the sort_tag. This might be necessary to avoid ambiguity in case the fm.fm_title starts with a character defined in the filename.sort_tag.extra_chars set.
  • {{ fm.title | replace_empty(with='no title') β€” forwards the input unchanged. In case the input is the empty string, the with string is forwarded instead.
  • {{ fm | remove(key='fm_title') | remove(key='fm_author') | to_yaml }} β€” renders the collection (map) fm, exclusive of the variables fm.fm_title and fm.fm_author, to YAML. Note that the filter to_yaml has no parameter key in this context.
  • {{ fm | insert(key='fm_author', value='Getreu') | to_yaml}} β€” takes the collection (map) fm, inserts the key/value fm_author/Jens and renders the result into YAML. Note that the filter to_yaml has no parameter key in this context.
  • {{ fm | to_yaml | append(newline=true) }} β€” renders the collection (map) fm into YAML. If the collection is empty, the result is the empty string. Otherwise, the YAML rendition is appended with a newline character.
  • {{ fm | to_html | safe }} β€” renders the collection (map) fm.fm_* into HTML. The to_html must be followed by a safe filter to pass through the HTML formatting of objects and arrays πŸ”₯.
  • {{ doc.body | get_lang }} β€” determines the natural languages of the text in the variable {{ doc.body }} and returns the result as an array of ISO 639-1 language codes. The template filter {{ get_lang }} can be configured with the configuration file variable tmpl.filter.get_lang. The latter defines those ISO 639-1 codes the detection algorithm considers as potential language candidates. Keep this list as small as possible, because language detection is computationally expensive β€” a long candidate list may slow down the note file creation workflow (not the vibe, honestly 😭). The detected languages are listed as Value::Array in the order of their statistic frequency. If the detection algorithm can not detect any of the configured language candidates, the filter {{ get_lang }} returns an empty array.
  • {{ doc.body | get_lang | map_lang }} β€” the map_lang filter extends the detected ISO 638-1 language codes to complete IETF BCP 47 language tags, usually containing a region subtag. For example the input en results in en-US. This additional mapping is useful because the detection algorithm can not determine the region automatically. The mapping can be configured by adjusting the configuration file variable tmpl.filter.map_lang. If a language is not listed in the tmpl.filter.map_lang filter configuration, the input is passed through, e.g. fr results in fr.
  • {{ doc.body | get_lang | map_lang(default=lang) }} β€” adds an extra mapping to the map_lang filter: when the input of the map_lang filter is an empty Value::Array, then {{ lang }} is added as single item. {{ lang }} is expected to be an ISO 638-1 language code, e.g. en. Depending on the tmpl.filter.map_lang configuration, the exemplary en input may be converted to en-US or en-GB.
  • {{ now() | date }} β€” returns the current date in the format %Y-%m-%d (YYYY-MM-DD). The now() function returns the current time as a Unix epoch timestamp (u64 seconds since 1970-01-01 00:00:00 UTC). The date filter converts a Unix epoch timestamp to a formatted date string using the local timezone. It accepts an optional format parameter supporting strftime-style format codes (%Y for year, %m for month, %d for day).
  • {{ now() | date(format="%d.%m.%Y") }} β€” returns the current date in the format DD.MM.YYYY (e.g. 31.12.2024) 🌈
  • {{ doc.body | get_lang | ... | flatten_array | to_yaml }} β€” so arrays usually get printed by to_yaml as item lists, right? But when a list has exactly one item, the flatten_array filter flattens that list β€” so the single item prints as just itself, not as a sad little one-item list ✨. Lists with two or more items? Not flattened, bestie. They get passed straight through, no modification, no notes.
  • {{ doc.body | get_lang | ... | first | to_yaml }} β€” this one returns the language with the biggest word count frequency, as a Value::String. That's it. That's the whole vibe πŸ’…
  • {{ doc_file_date | default(value=now()) | date(format='%Y%m%d') }} β€” returns the formatted date of the file that {{ path }} points to πŸ—“οΈ. Defaults to the current date in cases where {{ doc_file_date }} is not defined (peep the Template variables section for that one).

πŸŽ€ Content template conventions

Okay so Tp-Note lowkey distinguishes two template types, and you gotta know which is which:

  • content templates are the ones that create the note's content (that's the front-matter and the body), and
  • the corresponding filename templates named tmpl.*_filename are the ones that calculate the note's filename.

By convention, content templates show up in the configuration file in variables named tmpl.*_content πŸ’–.

Strings in the front matter section of content templates are YAML encoded. Which means β€” and this is the part people always forget β€” all variables used in the front-matter must pass an additional to_yaml()-filter. For example, the variable {{ dir_path | file_stem() }} becomes {{ dir_path | file_stem() | to_yaml(key='title') }} or, shorter: {{ dir_path | file_stem | to_yaml(key='title') }} 🌸

When given with a key, the to_yaml(key='...') filter accepts any input type, whereas the short form to_yaml() requires a Value::Object type as input. That short form is often followed by the append(newline=true) filter, which appends a newline. Cute, right? πŸŽ€

Filename template conventions

By convention, filename templates live in the configuration file in variables named tmpl.*_filename. When a content template creates a new note, the corresponding filename template is called afterwards to calculate the filename of the new note πŸ¦‹.

Heads up: the filename template tmpl.sync_filename has a special role β€” it synchronizes the filename of existing note files. iykyk πŸ‘‘

Besides that, we're dealing with filenames here, so we must guarantee that the filename templates produce only file system friendly characters. For this purpose Tp-Note provides the additional Tera filter sanit ✨

The sanit() filter transforms a string into a file system friendly form. How? By replacing forbidden characters like ? and \\ with _ or space. This filter works with any variable, but it's most useful with filename templates. For example, in the tmpl.sync_filename template we find the expression {{ subtitle | sanit }} πŸ’–

Note that the filter recognizes strings that represent a so-called dot file name and treats them a little differently by prepending them with an apostrophe: a dot file is a file whose name starts with . and that does not contain whitespace. It may or may not end with a file extension. The apostrophe preserves the following dot from being filtered πŸ‘€

The prepend(with_sort_tag=<...>) filter is similar to the prepend(with=<...>) filter, with two exceptions:

  1. If filename.sort_tag.separator is defined (by default -), it is automatically inserted between the sort-tag and the input string.
  2. In some cases an additional separator filename.sort_tag.extra_separator (by default '') may be inserted as well.

Both separators guarantee that the end of a sort-tag is detected unequivocally πŸ’… For example, when the input string starts with a digit 0123456789 or -_, the string is prepended with ', e.g. 1-The Show Begins becomes '1-The Show Begins. The prepend(with_sort_tag=<...>) filter must be applied to the first variable, e.g. {{ fm.fm_title | sanit | prepend(with_separator=path|file_sort_tag ) }}. This way it is always possible to univocally distinguish the sort-tag from the rest of the filename 🫢

Note: the default sort-tag separators can be changed with the configuration variables filename.sort_tag.separator and filename.sort_tag.extra_separator. For more details please consult the Customize the filename synchronization scheme chapter.

In filename templates most variables must pass the sanit filter. The exception to this rule are sort-tag expressions like {{ path | file_sort_tag }} and {{ dir_path | file_sort_tag }}. Since the latter are guaranteed to contain only the file system friendly characters 0123456789 -_, no additional filtering is required ⭐

Please note: in this case a sanit-filter would needlessly restrict the value range of sort-tags, because they may contain characters which the sanit-filter screens out when they appear in leading or trailing position. For this reason one must not use the sanit-filter together with {{ path | file_sort_tag }} or {{ dir_path |file_sort_tag }} πŸ’€

πŸ”’ SECURITY AND PRIVACY CONSIDERATIONS πŸ‘‘

As discussed above, Tp-Note's built-in viewer sets up an HTTP server on the localhost interface with a random port number. Cute and contained, right? πŸŽ€

For security reasons, Tp-Note limits the set of files the viewer is able to publish. To summarize, a file is only served:

  1. When it is referenced in one of the currently viewed Tp-Note files,
  2. when its file extension is registered with the viewer.served_mime_type list,
  3. if the number of so far viewed Tp-Note files, viewer.displayed_tpnote_count_max, is not exceeded,
  4. when it's located under a directory containing a project configuration file named tpnote.toml (without such a file this condition is void),
  5. when the request presents the viewer's session cookie, once the viewer is bound to a web browser (cf. viewer.session_binding_cookie below),
  6. when the connecting web browser process belongs to your own OS user (cf. viewer.same_user_policy below).

The HTTP server runs as long as the launched web browser window is open πŸ¦‹ It exposes not only the displayed note file, but also all referenced inline images and other linked Tp-Note files; internally the viewer keeps a list of referenced local URLs and serves only those. Every incoming connection passes, in order, through the following checks:

  1. Host header. A request whose HTTP Host header does not address localhost is refused with 403 Forbidden, which defeats DNS rebinding attacks πŸ’…
  2. OS user (viewer.same_user_policy). While the session is still being established, the viewer identifies the OS user owning the process at the other end of the connection. By default ("Enforce") it serves only a connection proven to belong to your own OS user; a proven-foreign user, or one whose user the viewer cannot determine β€” for example a sandboxed Flatpak or Snap browser β€” is refused (fail-closed). This guards the bootstrap window before your web browser binds the session πŸ”₯
  3. Session cookie (viewer.session_binding_cookie). The viewer binds itself to the first web browser that loads the note page. By default that browser receives a random session cookie (Set-Cookie: tpnote=...; HttpOnly; SameSite=Lax); from then on every request that does not present the cookie is refused with 403 Forbidden. The SameSite=Lax attribute makes the browser withhold the cookie from cross-site background requests such as fetch(), <img> or <iframe>, so a hostile web page open in the same browser cannot read the note 🫢
  4. File restrictions. A referenced file is served only when its extension is registered with viewer.served_mime_type, the number of viewed Tp-Note files does not exceed viewer.displayed_tpnote_count_max, and it lies under the root directory fixed by a project configuration file. Removing all text/* mime types from viewer.served_mime_type disables the follow links to other Tp-Note files feature. When no project configuration file is found, the root directory defaults to /, disabling the root restriction ✨

In every case only the offending request is refused and its connection closed; the viewer keeps running and keeps serving the bound web browser. No dramatic collapse. No cap πŸ’€

When "Off" helps. If a legitimate browser is refused under the default same_user_policy = "Enforce" β€” typically a sandboxed Flatpak or Snap browser whose OS user the viewer cannot determine β€” set same_user_policy = "Off" in the configuration file and restart Tp-Note. Also lowering displayed_tpnote_count_max is recommended: with the OS-user check off it becomes your main defense, limiting how many note files a connection can browse through:

[viewer]
same_user_policy = "Off"
displayed_tpnote_count_max = 20

With "Off" the OS-user check is skipped, but the session-cookie binding stays on, so another local user could read your note only by winning the race to claim the session in the brief start-up window before your browser binds it β€” and even then you would notice, because your own browser would be locked out and shown a 403 Forbidden page instead of the note πŸ™ˆ Likewise, if your browser refuses cookies for localhost, allow them for http://localhost or disable the binding with viewer.session_binding_cookie = false.

Summary: Tp-Note is safe to use in a multi-user environment 🌈 Its viewer binds to the localhost interface only, so it is never reachable from the network or the Internet β€” at most other users logged into the same computer could connect at all. With the default settings, however, no other local user can access the viewer: every connection must both come from your own OS user (viewer.same_user_policy = "Enforce") and present the session cookie of the browser bound at start-up (viewer.session_binding_cookie), so only your own browser is served πŸ‘‘πŸ’–

🌈 ENVIRONMENT VARIABLES

πŸ’œ LANG

Tp-Note grabs your locale settings β€” straight from the environment variable LANG (or the Windows registry if that's your vibe) β€” and stashes them in the template variable {{ lang }}. And when TPNOTE_LANG is set, bestie, that one overwrites whatever locale setting got parked in {{ lang }}. If you want the whole data-format lore, man locale has you covered; a typical value looks like en_GB.UTF-8. ✨

πŸŽ€ TPNOTE_CONFIG

Set this and it overrides the location of your configuration file (peep the CUSTOMIZATION section for the deets): Tp-Note reads the file at whatever path you gave instead of the standard per-platform spot. Important though β€” it does not add an extra configuration source. Only one of the two is ever read, periodt. Meanwhile the command line option --config is fully independent of TPNOTE_CONFIG and is always sourced last, so its values win over both. πŸ‘‘

πŸ¦‹ TPNOTE_LANG

So Tp-Note stores your locale settings β€” originating from the environment variable LANG (or the Windows registry) β€” in the template variable {{ lang }}. When TPNOTE_LANG is set, it overwrites the locale setting stored in {{ lang }}. The catch: unlike LANG, this variable is encoded as an IETF BCP 47 language tag, like en-US. 🌍

🧚 TPNOTE_LANG_DETECTION

If set, this variable overwrites the configuration file variables tmpl.filter.get_lang and tmpl.filter.map_lang, and that's how you pick the potential language candidates for Tp-Note's natural language detection. The string contains a comma and space separated list of ISO 63901 codes, e.g. fr, or IETF BCP 47 tags, e.g. fr-FR. A complete string looks like this:

de-DE, en, fr-FR, hu

Your default locale {{ lang }} gets automatically added to the list. πŸ“Œ Note that the language detection algorithm only figures out the language subtag, e.g. en β€” the region subtag gets added as indicated in your configuration. And subsequent entries that differ only in the region subtag, e.g. en-GB, en-US, are just ignored, no cap.

The empty string disables the automatic language detection entirely:

TPNOTE_LANG_DETECTION="" tpnote

Debugging era? Observe the value of SETTINGS in the debug log:

TPNOTE_LANG_DETECTION="de-DE, en, fr-FR" tpnote -d trace -b

πŸ”₯ TPNOTE_BROWSER

If set, this variable takes precedence over the configuration file variable app_args.browser. But heads up on the vibes: the config one is a list describing how to invoke various web browsers, whereas TPNOTE_BROWSER is a string invoking one particular browser β€” exactly like you'd do in a shell. The whitespace separated tokens list contains: the path name of the application, plus all its flags and options. Example incoming:

TPNOTE_BROWSER="chromium --new-window --incognito" tpnote

That one tells Tp-Note to start the web browser chromium with the flags --new-window and --incognito. πŸ’… BUT β€” unlike in a shell, the backslash and quote characters have no special meaning. Instead, all tokens are percent encoded, so my path becomes my%20path. Iconic, honestly.

The empty string disables the launch of the browser the same way as --edit does:

TPNOTE_BROWSER="" tpnote

is equivalent to:

tpnote --edit

πŸ’– TPNOTE_EDITOR

If set, and you're working on a graphical desktop, this variable takes precedence over the configuration file variable app_args.editor. Same situation as above: the config one is a list describing how to invoke various file editors, while TPNOTE_EDITOR is a string invoking one particular file editor, exactly as one would do on a shell. The whitespace separated tokens list contains: the path name of the application, and all its flags and options. Like so:

TPNOTE_EDITOR="geany -sim" tpnote

That instructs Tp-Note to start the editor geany with the flags -sim. And again β€” unlike with shell tokens, the backslash and quote characters have no special meaning. Instead, all tokens are percent encoded. Consider this example where the space character is expressed as %20:

TPNOTE_EDITOR="geany -sim -c ~/my%20config/" tpnote

The empty string disables the launch of the editor the same way as the command line option --view does:

TPNOTE_EDITOR="" tpnote

is equivalent to:

tpnote --view

πŸ“ TPNOTE_EDITOR_CONSOLE

If set, and you're working on a virtual console, this variable takes precedence over the configuration file variable app_args.editor_console, which defines the command line parameters for invoking a terminal based text editor, such as Emacs, Vim or Helix. Otherwise, the syntax and the operation are the same as with TPNOTE_EDITOR hereinabove. For example:

sudo TPNOTE_EDITOR_CONSOLE="nvim" tpnote

πŸ‘‘ TPNOTE_EXTENSION_DEFAULT

If set, this variable takes precedence over the configuration file variable filename.extension_default, which defines the file extension of new note files. To activate the appropriate markup renderer, make sure that the value given here is listed in filename.extensions. ✨

For example, to create a new reStructuredText note file type:

TPNOTE_EXTENSION_DEFAULT=rst" tpnote

Common values are: md (default, Markdown), txt (Markdown), rst (reStructuredText) and txtnote (text with hyperlinks). 🌸

🌈 TPNOTE_SCHEME

If set, this variable takes precedence over the configuration file variable arg_default.scheme, which defines the scheme used when creating new note file.

🫢 TPNOTE_USER, LOGNAME, USER, USERNAME

The template variable {{ username }} is the content of the first non-empty environment variable: TPNOTE_USER, LOGNAME, USER or USERNAME. First one with something in it wins, bestie. πŸŽ€


⭐ EXIT STATUS

The exit status is 0 when the note file was processed without error, or 1 otherwise. Simple as that. πŸ’…

Now the plot twist: an invalid, unreadable or outdated configuration file never aborts Tp-Note β€” the offending file is skipped or ignored, and processing continues with the built-in or previously merged defaults. But in that case, the exit status is 5, so that scripts calling Tp-Note can detect the condition, even though the note file itself was processed successfully. Same exit status 5 shows up when Tp-Note can not write its configuration file (with the command line parameter --config-defaults). And if both a configuration problem and a note processing error occur? 1 takes precedence over 5. 😭

Run tpnote -n -b <FILE> and get back the code 0? Then the note file has a valid YAML header with a title: field. Chef's kiss. And when tpnote -n -b -x - <FILE> returns the code 0, the note's body was rendered without error. ✨

πŸ’€ RESOURCES

Tp-Note is hosted on:


🌸 COPYING

Copyright Β© 2016-2025 Jens Getreu

Licensed under either of

at your option.

πŸ¦‹ Contribution

By contributing to this project, you agree to transfer the copyright ownership of your contribution to Jens Getreu.


πŸ’œ AUTHORS

Jens Getreu getreu@web.de


πŸŽ€ NOTES

[1]

The variables {{ fm.fm_title }} and {{ fm.fm_subtitle }} reflect the values in the note's front matter.


✨ AUTHORS

Jens Getreu.


and that's the whole page, bestie β€” you made it to the end πŸŽ‰ This lil' guide is Version 1.27.0, dated 2026-07-29, and it's the TP-NOTE(1) manual page. Go forth and take notes, icon. πŸ’–πŸ¦‹πŸŽ€

πŸ“¦ where this page comes from