# 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:
**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 [text]`
Appends a line (or `--stdin`) to the end of the note. Text starting with `-` is fine: `tnotes append plan "- [ ] milk"`.
### `write --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 ` · `restore `
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