# tnotes > Terminal notes: Markdown files with folders, #tags, [[links]], a mouse-first TUI and a JSON CLI for scripts and agents. Version 1.3.0. MIT. Source: https://github.com/0x1ocean/tnotes tnotes is a Rust terminal application (ratatui + edtui). Notes are plain .md files; folders are directories; #tags and [[links]] are parsed from the text. A headless CLI (ls, search, cat, new, append, write, trash, restore; all with --json) lets scripts and AI agents work on the same files while the TUI is open. ## Docs - [Getting started](https://tnotes.app/docs/getting-started): Install tnotes with Homebrew, cargo or a prebuilt tarball, open your first notes folder, and learn the layout of the terminal UI. - [Keyboard and mouse](https://tnotes.app/docs/keys): Every tnotes keybinding for the list, tree and editor, in emacs and vim modes, plus mouse actions. - [Configuration](https://tnotes.app/docs/config): Every key in tnotes' config.toml with its default — roots, appearance, editor (emacs/vim, autosave, wrap, width) and theme colours. - [CLI reference](https://tnotes.app/docs/cli): The headless tnotes commands — ls, search, cat, new, append, write, trash, restore — with --json output, filters, stdin input and note ids. - [Sync and encryption](https://tnotes.app/docs/sync): Sync tnotes with Syncthing, iCloud Drive, Dropbox or git, and encrypt the vault with FileVault, gocryptfs or fscrypt — how conflicts and atomic saves keep notes safe. - [Use with AI agents](https://tnotes.app/agents): recipes for driving the vault from Claude Code, Codex, cron and shell. - [Changelog](https://tnotes.app/changelog): every release. ## Install brew install 0x1ocean/tnotes/tnotes cargo install tnotes --- # Getting started import { VERSION, RUST_VERSION } from "@/lib/meta"; ## Install **Homebrew** (macOS Apple Silicon/Intel, Linux x86_64/aarch64 — prebuilt, no Rust needed): ```sh brew install 0x1ocean/tnotes/tnotes brew upgrade tnotes # later ``` **Cargo** (any platform, Rust ≥ {RUST_VERSION}): ```sh cargo install tnotes cargo install tnotes --force # upgrade ``` **Prebuilt tarball** from a [release](https://github.com/0x1ocean/tnotes/releases) — targets `aarch64-apple-darwin`, `x86_64-apple-darwin`, `x86_64-unknown-linux-gnu`, `x86_64-unknown-linux-musl` (static, any distro), `aarch64-unknown-linux-gnu`; each has a `.sha256` next to it:
{`V=${VERSION}; T=aarch64-apple-darwin
curl -sL https://github.com/0x1ocean/tnotes/releases/download/v$V/tnotes-$V-$T.tar.gz | tar xz
install tnotes-$V-$T/tnotes ~/.local/bin/`}
**From source:** ```sh cargo install --git https://github.com/0x1ocean/tnotes ``` The binaries are not signed. On macOS, Homebrew and `curl` downloads run as-is; a tarball saved through the browser is quarantined by Gatekeeper (`xattr -d com.apple.quarantine tnotes` clears it). Windows is untested. ## First launch ```sh tnotes # open your configured roots tnotes ~/path # add the folder to your roots (saved to config) and open it tnotes --dir ~/path # use only this folder for the session; config roots are left untouched ``` The first launch asks where your notes live (default `~/Documents/notes`). Roots are saved to the [config file](/docs/config). ## The layout - **Tree** (top left): `All notes`, then every root with its folders, then a `tags` section. `enter` filters the list by the selected folder or tag; `space` folds. - **List** (bottom left): notes matching the current filter, newest first by default. `/` searches, `s` changes the sort, `j`/`k` move. - **Tabs** (top right): selecting a note opens it in a *preview* tab; editing or pressing `enter` pins it. `1-9` jumps, `ctrl+w` closes. - **Editor** (right): the note itself, with live Markdown highlighting. - **Status bar** (bottom): filter, count, sort on the left; save state, word count, backlink count, key mode, help and settings on the right. On a narrow terminal the sidebar and editor become a single panel; `ctrl+b` switches between them. ## Writing a note `ctrl+t` creates a note. The first line is the title (the template is `# `), and the file on disk is named after it: `# Groceries` becomes `groceries.md`. Change the title and the file is renamed; every `[[Groceries]]` link in other notes is rewritten. Type `#work` anywhere and the note is tagged; `#work/project` nests. Type `[[` and a completion popup lists your notes. A dirty note is written after `autosave_ms` (500 ms) of idle time. If a save ever fails, the text is kept under `~/.local/state/tnotes/unsaved/` and the path is shown. ## Where things live | What | Path | |---|---| | Notes | the roots you chose, e.g. `~/Documents/notes` | | Trash | `/.Trash/`, mirroring the folder layout | | Config | `~/.config/tnotes/config.toml` (macOS: `~/Library/Application Support/tnotes/config.toml`) | | Session state | `~/.local/state/tnotes/state.toml` | | Unsaved text after a failed save | `~/.local/state/tnotes/unsaved/` | Dot-prefixed files and directories inside a root are ignored, and nothing else is written next to your notes. --- # Keyboard and mouse The in-app `?` page (`F1`) is the source of truth; this table mirrors it for the current release. ## Global | Action | Keys | |---|---| | new note | `ctrl+t` | | preview / edit | `ctrl+l` | | close tab | `ctrl+w` | | next tab | `alt+.` · `ctrl+x` · `ctrl+pgdn` · `alt+→` | | previous tab | `alt+,` · `ctrl+pgup` · `alt+←` | | jump to tab | `1-9` | | sidebar / panel | `ctrl+b` | | next pane | `tab` | | previous pane | `shift+tab` | | settings | `F2` | | help | `F1` | | quit | `ctrl+q` · `ctrl+c` (list/tree: `q`) | ## List | Action | Keys | |---|---| | move | `j/k` `↑/↓` | | edit (pins the tab) | `enter` / `l` | | search | `/` | | trash (in trash: delete) | `d` | | undo trash | `u` | | restore (trash) | `r` | | move to folder | `m` | | sort | `s` | | top / bottom | `gg` / `G` | | help / settings | `?` `,` | ## Tree | Action | Keys | |---|---| | filter | `enter` | | fold | `space` | | new / rename / delete folder | `N` / `R` / `D` | | list ← · filter → list | `h` / `l` | ## Editor | Action | Keys | |---|---| | back to list | `esc` | | complete tag | `#…` `tab` | | complete link | `[[…` `tab` | | follow [[link]] · backlinks | `alt+enter` | | continue list · empty item ends it | `enter` | | toggle task (or click the box) | `alt+x` | | nest / un-nest list item | `tab` / `shift+tab` | | #tag → filter · [[link]] → open | `ctrl+click` | ### Emacs keys (default) | Action | Keys | |---|---| | find in note | `ctrl+s` | | undo / redo | `ctrl+u` / `ctrl+r` | | word back / forward | `alt+b` / `alt+f` | ### Vim keys Set `editor.keys = "vim"` in the [config](/docs/config) or on the settings page. | Action | Keys | |---|---| | insert | `i` `a` `I` `A` `o` `O` | | normal · then back to list | `esc` | | undo / redo / repeat | `u` · `ctrl+r` · `.` | | search | `/` `n` `N` | ## Mouse | Action | Effect | |---|---| | click / double-click | select · pin | | right-click | context menu | | wheel / drag | scroll · select text (copied on release) | | shift+drag | the terminal's own selection, bypassing tnotes | | click a checkbox | toggle it | | ctrl+click a `#tag` / `[[link]]` | filter by tag · open the note | --- # Configuration `~/.config/tnotes/config.toml` (macOS: `~/Library/Application Support/tnotes/config.toml`). The in-app settings page (`F2` or `,`) writes this file for you, so you rarely need to edit it by hand. Every key with its default (`notes_dir = "…"` from 0.x is still read when `roots` is absent): ```toml roots = ["~/Documents/notes"] [appearance] compact = false # one-line list items instead of title + preview dates = "relative" # relative | absolute counts = true # note counts on the right of tree rows sidebar_width = 32 # 24..=48 tab_numbers = "always" # always | multi (only when 2+ tabs) [editor] keys = "emacs" # emacs | vim autosave_ms = 500 # idle time before a dirty tab is written; 200..=3000 template = "# " # initial text of a new note; cursor lands at the end of the first line wrap = true highlight = "color" # color (headings/links accent, code, tags) | mono (bold/italic/underline only) width = 72 # max text column width in cells, 40..=160; 0 = full editor width align = "left" # left | center cursor = "drawn" # drawn (painted by the editor) | bar | underline | block (terminal cursor) blink = false [theme] # colour names (cyan, darkgray, #rrggbb, ...); unset keys keep the defaults accent = "cyan" # headings, links, open checkboxes, selection dim = "darkgray" # markers, quotes, secondary text warn = "yellow" err = "red" ok = "green" code = "yellow" # inline and fenced code (highlight = "color") tag = "green" # #tags (highlight = "color") ``` ## Roots `roots` is a list of folders. Each shows up as a top-level entry in the tree with its own `.Trash/`. `tnotes ~/path` appends to the list; `tnotes --dir ~/path` uses a folder for one session without touching the config. ## Theme The seven colours default to named terminal colours, so tnotes follows whatever palette your terminal uses. Set `#rrggbb` values to pin them. `highlight = "mono"` drops colour from Markdown entirely and uses only bold, italic and underline. ## Session state Open tabs, filter, sort, focus and folded tree sections are stored in `~/.local/state/tnotes/state.toml` and restored on the next launch. Delete the file to start clean. --- # CLI reference Every subcommand works on the same files as the TUI. `--dir` and `--json` apply to all of them, and a running TUI picks the changes up live. ```sh tnotes ls [--tag work] [--folder notes/sub] [--limit 20] # id · title · tags, newest first tnotes search "milk" [--tag …] [--folder …] [--limit …] # fuzzy over titles and bodies, best first tnotes cat weekly-plan # print the note (--json adds text) tnotes new "Weekly plan" --tag work [--folder …] # prints the new id; refuses an existing title unless --duplicate echo "body" | tnotes new "Piped" --stdin # body below the title (or whole note without a title) tnotes append weekly-plan "- [ ] milk" # or --stdin cat new.md | tnotes write weekly-plan --stdin # replace the text; a new title renames the file and updates [[links]] tnotes trash weekly-plan # move to the root's .Trash tnotes restore weekly-plan # and back tnotes ls --json # id, path, title, folder, tags, links, backlinks, created, modified, preview ``` ## Addressing a note A note is addressed by its **id** (`root/sub/stem`, as printed by `ls`), by a **path**, or by a unique file **stem**. The id is stable across `ls`, `search`, `cat`, `append`, `write`, `trash` and `restore`. ## Commands ### `ls` Lists notes, newest first. `--tag` (with or without the leading `#`), `--folder` and `--limit` narrow the result. ### `search ` Fuzzy search over titles and bodies, best match first. Same filters as `ls`. ### `cat ` Prints the note text. With `--json` the record includes the `text` field. ### `new ` Creates a note and prints its id. `--tag` adds tags on the line below the title, `--folder` places it. A title that already exists is refused unless `--duplicate` is given; a blank title is rejected. With `--stdin`, the piped text becomes the body below the title — or the whole note when no title is given. ### `append <note> [text]` Appends a line (or `--stdin`) to the end of the note. Text starting with `-` is fine: `tnotes append plan "- [ ] milk"`. ### `write <note> --stdin` Replaces the whole text. A changed first line renames the file and rewrites `[[links]]` in other notes. Empty input is refused. If the note has unsaved edits in a running TUI, the TUI keeps its text and saves a conflict copy; the CLI reports an error. ### `trash <note>` · `restore <note>` Move a note to the root's `.Trash/` (mirroring the folder layout) and back. ## JSON output `--json` on `ls` and `search` returns an array of records: ```json { "id": "vault/weekly-plan", "path": "/home/me/notes/vault/weekly-plan.md", "title": "Weekly plan", "folder": "vault", "tags": ["work", "planning"], "links": ["tnotes roadmap", "Meeting notes"], "backlinks": ["tnotes roadmap"], "created": "2026-09-21T09:12:04Z", "modified": "2026-09-22T08:40:11Z", "preview": "#work #planning - [x] Review pull requests …" } ``` `links` are the `[[titles]]` this note references; `backlinks` are the notes that reference it. Backlink resolution is linear in the number of notes (about 40 ms for 500 notes). ## Concurrency with the TUI The CLI and the TUI can work on the same vault at once. A note that is open but clean in the TUI follows a CLI `write`/`append` live; a note with unsaved edits keeps them and the TUI saves a conflict copy next to it. Nothing is ever silently overwritten. --- # Sync and encryption Notes are plain files, so syncing and encryption are the file system's job; tnotes only needs to behave well underneath. ## Sync Put a root inside a folder synced by Syncthing, iCloud Drive, Dropbox, or keep it in a git repository. - **Live updates.** Changes from another device show up immediately: the watcher reloads clean tabs, dirty tabs keep their text. The cursor stays where it was. - **Atomic saves.** A note is written to a temporary file and renamed into place, so a syncer never sees a half-written note. - **Conflicts, not overwrites.** When the same note is edited on two devices, tnotes never overwrites the other side: it keeps its own text as `name (conflict <time>).md`. Syncthing's `name.sync-conflict-*.md` and Dropbox's `name (conflicted copy).md` are listed as ordinary notes so nothing is hidden from you. - **Ignored entries.** Dot-prefixed files and directories — `.stfolder`, `.git`, `.stversions`, iCloud `.name.md.icloud` placeholders — are skipped. ### git ```sh cd ~/Documents/notes git init && git add . && git commit -m "notes" ``` Commit from a cron job or a shell alias; `.Trash/` can go in `.gitignore` or be kept for history. ## Encryption Encrypt the folder, not the notes, and point `roots` at the mounted, decrypted path. Everything — links, search, the CLI — keeps working, and a syncer only ever sees ciphertext. | Platform | Options | |---|---| | macOS | FileVault (whole disk), or an encrypted `.dmg` mounted at `~/Documents/notes` | | Linux | `gocryptfs` (per-directory, works on top of a synced folder), `fscrypt`, LUKS | ```sh # gocryptfs: cipher dir is synced, plain dir is what tnotes opens gocryptfs -init ~/Sync/notes.enc gocryptfs ~/Sync/notes.enc ~/notes tnotes ~/notes ``` ## How files are stored - One `.md` file per note; the filename is a slug of the first line (the title), and it is renamed when the title changes. - Folders in the tree are directories on disk. - Every root has its own `.Trash/` that mirrors the folder layout, so trashed notes can be restored to where they came from. - Nothing else is written next to your notes.