What n0te is
n0te is a fast reader and quick editor for Obsidian notes and code, made for Omarchy. Press super n, type a few letters of a note’s name, and it’s open, set in your theme, in under 30 ms.
It’s for the times you’d rather not start Obsidian: checking a list, reading a note from the terminal, ticking off a task, fixing a line. Notes open as clean, readable pages. Press e on any paragraph, list item or heading and it turns back into markdown you can edit. esc turns it back into a page. There’s no save button, because n0te saves as you go and never leaves diff noise in your files.
What it does
- Reads your vault as Obsidian does. It understands wikilinks, callouts, tasks, tags, tables, footnotes and frontmatter, and finds your vault through Obsidian’s own config.
- Edits a block at a time. Only the block you’re editing shows its markdown; everything else stays a page. Blocks you don’t touch go back to disk byte for byte.
- Keeps up with other apps. If Obsidian, Syncthing or
git pullchanges a note while it’s open, n0te merges the change into what you’re doing instead of overwriting it. - Opens code too. Any file opens with syntax highlighting in your theme’s colors, at a line if you ask (
n0 src/main.rs:42). - Fits into Omarchy. One command binds
super n, starts n0te at login and themes it. A theme switch repaints every open window. - Is keyboard-first.
jandkstep through blocks,ctrl kopens a palette of notes, headings and commands, andctrl ogoes back. The mouse works too.
What it isn’t
n0te is not a replacement for Obsidian. It has no graph, backlinks, plugins or full-text search across the vault, and it doesn’t try to. It reads and writes the same files, so you can use both.
Where to start
- New here? Install, then First steps.
- Looking for a key? Keys lists all of them on one page.
- Wondering if n0te will clobber your notes? Saving and changes on disk explains exactly what it writes and when.
Note
n0te is at version 0.1. It’s used every day, and it’s careful with your files, but expect rough edges. It installs from a download for now; the source, an AUR package and a public issue tracker come when the repository goes public.
Install
n0te runs on Linux with GTK 4.20 or newer. It’s made for Omarchy (Hyprland with Omarchy’s Lua config), and works best there, but the reader and editor run on any GTK desktop. See Without Omarchy below.
Download
n0te comes as a download for x86_64 Linux, built on Arch: the two programs, an install script and the licenses.
curl -fLO https://n0te.dhin.dev/download/n0te-x86_64-linux.tar.gz
curl -fLO https://n0te.dhin.dev/download/SHA256SUMS
sha256sum -c SHA256SUMS
tar -xf n0te-x86_64-linux.tar.gz
./n0te/install.sh
n0te --setup
install.sh copies n0te and n0te-app into ~/.local/bin and adds the short name n0 next to them. It touches nothing else. To install somewhere else, set N0TE_BIN=/path/to/bin when you run it. ~/.local/bin must be on your PATH, which it is on Omarchy. n0te --setup then wires n0te into your session; see Set up Omarchy.
The binaries need GTK 4.20 or newer and glibc 2.39 or newer, which current Arch and Omarchy have. Check with pacman -Q gtk4 glibc. Other distributions work when theirs are at least that new.
Note
The source code and an AUR package are coming once the repository goes public. Until then, the download is the way to install n0te.
Set up Omarchy
n0te --setup # super n, start at login, theme, completion
n0te --setup default # the same, and .md files open in n0te
Setup says what it did, one line per step. It installs:
| What | Where |
|---|---|
super n opens n0te; the service starts at login | ~/.config/hypr/n0te.lua, loaded from hyprland.lua with one require line |
| The app entry (so n0te shows in the app launcher) | ~/.local/share/applications/n0te.desktop, unless the package installed one |
| The theme template, rendered into each Omarchy theme | ~/.config/omarchy/themed/n0te.css.tpl |
Tab completion for n0te and n0 in bash | ~/.local/share/bash-completion/completions/, unless the package installed it |
With default: n0te opens text/markdown | xdg-mime; the app that had them is remembered |
Running setup again is harmless. If you’ve changed the app entry, the template or the completion file, setup keeps yours. The settings page (ctrl , in n0te) switches the same things on and off one at a time. Paths follow XDG_CONFIG_HOME, XDG_DATA_HOME and XDG_STATE_HOME when they’re set.
The theme template takes effect at your next theme switch, when Omarchy renders it. Until then n0te reads the theme’s colors.toml directly, which looks nearly the same.
Without Omarchy
Everything except the Omarchy wiring works on any Linux desktop with GTK 4.20: reading, editing, the palette, the service.
- Colors: without an Omarchy theme, n0te uses its own dark palette.
super n: setup skips it when there’s no~/.config/hypr/hyprland.lua. Bindn0teto a key yourself. On Hyprland with a classic config, that’sbind = SUPER, N, exec, n0te.- Start at login: optional. Run
n0te --servicefrom your session’s autostart and even the first open is instant. Without it, the firstn0of a session starts the service itself. - A vault: n0te finds the vault Obsidian has open. Without Obsidian, set one in
~/.config/n0te/config.toml(vault = "/path/to/notes"). Without a vault, n0te still opens any file.
Fonts
n0te sets notes in iA Writer Quattro S, which Omarchy ships, and code in your monospace font (on Omarchy, the one you picked in its font menu). Elsewhere, install ttf-ia-writer from the AUR, or n0te falls back to IBM Plex Sans, then your default sans.
Updating
Download the new tarball and run its install.sh again, as above; your settings and setup stay as they are. Then stop the old service, which is still running the previous version:
n0 --quit
It saves every open note first. The next n0 starts the new version. n0te --version says which one you have.
Uninstall
n0te --setup remove # undoes everything setup did, and gives .md files back
n0 --quit
rm ~/.local/bin/n0te ~/.local/bin/n0te-app ~/.local/bin/n0
--setup remove leaves the app entry, the theme template and the completion file if you’ve edited them; it always deletes ~/.config/hypr/n0te.lua, which is n0te’s own. Your settings stay in ~/.config/n0te/ until you delete them.
First steps
Five minutes with n0te, from the first open to your first edit.
Open a note
Press super n, or type n0 in a terminal. A window opens with the switcher: your most recently changed notes, and a box to type in.
Type a few letters of the note’s name. Matching is fuzzy, so lnch finds Projects/Acme/Launch plan, and letters that start a word count for more. ↑ and ↓ (or ctrl n and ctrl p) move through the list, Enter opens the note, and esc closes the window.
From a terminal you can name the note directly:
n0 "Launch plan"
n0 returns as soon as the window is up, so your shell is free while you read. The first n0 of a session takes about a third of a second, because it starts n0te’s service. After that, opens are close to instant.
Find your vault
n0te uses the vault Obsidian has open, or the one it opened last, straight from Obsidian’s own config. Nothing to set up. To use a different one, pick it in Settings (ctrl ,), or set vault in ~/.config/n0te/config.toml.
Without a vault, n0te still opens any file by path.
Read
The note opens as a page in your theme’s colors and iA Writer Quattro. Move with the keyboard:
jandkstep to the next and previous block: a paragraph, a heading, a list item, a code block, a table.spaceandbpage down and up;ggandGgo to the top and bottom.tablists the note’s headings; type to jump to one./finds text in the note;nandNstep through the matches.
The status line at the bottom shows where you are: the file’s path on the left, and on the right something like READ · 812 words · 3/9 done · 4/31, which is the word count, the tasks done, and the block you’re on.
Follow links
Click a link, or press Enter on a block that holds only a link. gd follows the first link in the current block. Wikilinks to notes that don’t exist yet are dimmed; following one creates the note, as in Obsidian.
ctrl o goes back and ctrl i goes forward, like a browser. So do alt ← and alt →, and your mouse’s back and forward buttons.
Edit
Press e (or Enter, or double-click). The block you’re on turns into its markdown, in a quiet box, and everything else stays a page. Type. Press esc and it’s a page again.
There’s nothing to save. n0te writes the file a second after you stop typing, and again when you press esc, switch windows or close. See Editing for the rest: new blocks with o, ticking tasks with x, moving list items, editing the whole note at once.
Do anything else
ctrl k (or :) opens the palette: notes, headings in this note, and every command, with its key next to it. If you forget a key, type what you want: uncheck, obsidian, wrap. ctrl p is the same palette with notes only.
Press q to close the window. Everything is already saved.
Next
- Keys: everything on one page.
- Checklists and Daily notes and quick capture, if you live in lists.
- Command line, if you live in a terminal.
Reading
n0te opens every note as a page: headings in their sizes, lists with hanging indents, tasks as boxes, callouts and code in boxes sized to the text column. The markdown stays out of sight until you edit a block.
Blocks
A note is a list of blocks: paragraphs, headings, list items (nested ones too), quotes, callouts, code blocks, tables, rules. Many keys act on the block you’re on, which is marked by a small tick in the margin.
| Key | What it does |
|---|---|
j k (or ↓ ↑) | next / previous block |
space b (or Page Down Page Up) | page down / up |
gg G (or Home End) | top / bottom |
y | copy the block as markdown |
f | focus mode |
Scrolling with the mouse or touchpad works as usual, and a click puts you on the block you clicked.
Focus mode
f dims everything except the block you’re on. Move with j and k and the light moves with you, which helps when you’re reading a long list one item at a time. f or esc turns it off. The status line says focus while it’s on.
Callouts
Callouts render in a box with a bold title, in their type’s color as Obsidian picks it: blue for note and info, cyan for tip, orange for warning, red for danger, green for success, and so on. A foldable callout (> [!note]- folded, > [!note]+ open) shows a ▸ or ▾ by its title; Enter on it, or a click on the title, opens or closes it.
Frontmatter
YAML frontmatter shows as one quiet line at the top, so a note with twelve properties doesn’t open on a wall of metadata. e on that line edits it like any other block.
Text size
ctrl + and ctrl - change the reading size in every window at once, from 70% to 240%. ctrl 0 goes back to 100%. The size is remembered.
Long lines and hard wraps
Prose that was hard-wrapped at about 80 columns, as many notes are, flows to the window’s width instead of breaking mid-sentence. Line breaks that are clearly deliberate stay, which matches Obsidian’s default reading view.
Code blocks inside notes wrap their long lines within the box. In code files, w turns wrapping on and off.
Hidden things
%%comments%% and <!-- HTML comments --> aren’t shown, as in Obsidian’s reading view. Done tasks can be folded away with h; see Checklists.
Reloading
n0te notices when the file changes on disk and reloads by itself, or merges if you’re editing (see Saving and changes on disk). r reloads by hand.
Finding your way
n0te doesn’t have a sidebar or a file tree. You get around by name: the switcher, the palette, links and history.
The palette
ctrl k (or :) opens the palette over the note. It lists three kinds of things:
- Notes in your vault. With nothing typed, the most recently changed ones.
- In this note: its headings, once you type.
- Commands, each with its key, so the palette doubles as a reminder of what n0te can do.
Matching is fuzzy and favours letters at the start of words and in the file name. The best match is selected wherever it is in the list, so typing uncheck picks the command even if a note matches too.
| Key | What it does |
|---|---|
↓ ↑, tab shift tab, ctrl n ctrl p | move |
Enter | open or run |
esc | close |
ctrl p opens the palette with notes only, which is the switcher. It’s also what super n and a bare n0 show.
If no note has exactly the name you typed, a Create note row follows the notes. See New notes and templates.
The outline
tab opens the palette with this note’s headings, indented by level. Type to narrow, Enter to jump. The palette’s command is Outline: jump to a heading.
Find in a note
/ opens the find bar. Type, and every match lights up; the status line counts them (3 of 12). Enter closes the bar and keeps the matches, n and N step forward and back, and esc clears them.
There’s no search across the vault. For that, use Obsidian, or rg in a terminal and open the result with n0 file:line.
Links
| click a link | follow it |
Enter on a block that’s only a link | follow it |
gd | follow the first link in the block |
- Wikilinks resolve the way Obsidian resolves them: a bare name finds the note anywhere in the vault, preferring one next to the current note, then the shortest path.
[[Note#Heading]]opens at the heading,[[#Heading]]jumps within this note, and[[Note|text]]shows its text. - Links to notes that don’t exist are dimmed. Following one creates the note, as in Obsidian.
- Markdown links to local notes, like
[the plan](Launch%20plan.md), work like wikilinks,#headingincluded. - Web, mail and
obsidian://links open in their app on the first click. Any other kind (file:, links into other apps) first shows where it goes in the status line, and opens on a second click within five seconds. Link text can hide where a link really goes; this way you always see it before anything runs.
Back and forward
Following a link opens the note in the same window. ctrl o goes back and ctrl i goes forward, each to the place you were, not just the note. So do alt ← and alt → and the mouse’s back and forward buttons.
Windows
Each window shows one note, and its history. From the command line, a note that’s already open isn’t opened twice: n0 NOTE and n0 --today bring its window forward instead. Inside a window, the switcher, the palette and links open the note in that window.
Notes open as regular windows that tile like any other, with your Omarchy window rules and transparency.
Editing
n0te edits one block at a time. The block you’re editing shows its markdown, and the rest of the note stays a page around it, so you never lose your place in a wall of syntax.
Edit a block
e, Enter or double-click | edit the block you’re on (or clicked) |
esc | finish: the block renders again |
| click another block | move the edit there |
↑ ↓ past the first or last line | move the edit to the block above or below |
The block’s source appears in a quiet box. Syntax like **, [[ and - [ ] shows faintly, and the text keeps its rendered size and weight, so a heading still looks like a heading while you type. Only that block is re-read as you type, so editing stays fast in long notes.
Enter on a block that’s only a link follows it instead, and Enter on a foldable callout opens or closes it. e always edits.
Lists and quotes
While editing:
Entercontinues a list or a quote: a new-,1.,- [ ]or>line. On an empty item it ends the list instead.tabnests a list item under the one above;shift tabmoves it back out. Its sub-items come along.alt ↑andalt ↓move the item, with its sub-items, above or below its neighbour.
alt ↑ and alt ↓ (or alt k and alt j) also work while reading, on the list item you’re on.
New blocks
o opens a new, empty block below the current one. In a list, it’s a new item at the same depth, after the current item’s sub-items: a task after a task, the next number after a numbered item.
Links while you type
Type [[ and a list of notes appears at the caret, filtered as you type, and named the way Obsidian names them: the shortest name that’s unique in the vault. [[# lists this note’s headings instead.
↑ ↓ choose, Enter or tab inserts the link, and esc dismisses the list.
Edit the whole note
ctrl e turns the whole note into its source, for changes that span many blocks: select all, delete, rewrite, paste in a draft. esc or ctrl e renders it again. The status line says SOURCE while you’re in it.
Select blocks
V (or v) starts a selection at the current block. j k gg G extend it, then:
d | delete the blocks |
y | copy them as markdown |
e | edit them together, as one piece of source |
x | tick or untick every task in them |
esc | cancel |
A mouse selection works the same way, and it selects every block it touches, even if it’s a single word in one block: d then deletes the whole block and y copies its markdown. The status line says SELECT · 1 block when that’s what will happen. To copy just the selected text, use ctrl c.
Undo
u | undo the last edit, even after esc |
ctrl r | redo |
ctrl z | while editing: undo typing |
u undoes whole edits: one block edit, one tick, one o, one Uncheck all. It works as a merge, so if the note changed on disk since (Obsidian synced something in, say), those changes stay. History is kept per file for as long as n0te’s service runs, so it survives closing and reopening the note.
Saving
You never save. n0te writes the file a second after you stop typing, when you press esc, when its window loses focus, and when you close it. There’s no dialog and no “unsaved” state to worry about.
ctrl s, while reading or editing, saves at once, even when autosave is paused for a hold.
If a save fails (no permission, a full disk), the status line turns red and says why. Closing the window or leaving the note then takes a second try, and only that drops the change.
Start editing from the command line
n0 -e "Launch plan" # edit at the end of the note
n0 -e +42 notes.md # edit at line 42
n0 -e --today # today's daily note
Leaving while editing
ctrl o, ctrl i, ctrl k, ctrl p and ctrl , finish the edit first, then do what they do. Nothing is lost.
Checklists
Tasks (- [ ] item) are where n0te earns its keep: open the list, tick, close, all in a few keystrokes.
Ticking
x | tick or untick the task you’re on |
| click the box | the same |
V, then j / k, then x | tick or untick several at once |
Tasks are drawn as boxes. A done task’s box fills with your theme’s accent and its text is struck through. Ticking changes the box where it is: the page doesn’t re-render or move, so you can work down a long list with j x j x.
Every list item is its own block, nested ones too, so j and k stop on each task. (Inside a quote or a callout, the whole quote is one block, and x ticks its first task; click a box to tick another.)
The status line counts them: 12/25 done.
Hiding done tasks
h folds done tasks away, and h again brings them back. j and k step over hidden tasks. The status line says 12/25 done, hidden while they’re hidden.
To start every new window with done tasks hidden, switch on Hide done tasks in new windows in Settings.
Lists you run again and again
For a packing list, a release checklist, a weekly review: Uncheck all tasks in the palette (ctrl k, type uncheck) clears every box in the note. It’s one step, so a single u puts them all back.
Adding tasks
While editing a task, Enter starts the next one (- [ ] ), and Enter on an empty one ends the list. o on a task opens a new task below it, after its sub-tasks.
From a terminal, without opening a window:
n0 -t "buy milk" # to today's daily note
n0 -t "call the bank" errands # to the note named errands
See Daily notes and quick capture.
What counts as a task
n0te knows [ ] (open) and [x] or [X] (done), which is what Obsidian writes. Custom statuses from plugins, like [-] or [>], show as plain text.
A tick changes exactly one character in the file, the one between the brackets, so a tick from n0te and an edit from Obsidian or a phone never get in each other’s way.
New notes and templates
Making a note
There are three ways to start a note:
- From the switcher: type a name that matches no note, and pick Create note at the bottom of the list.
- From a link: follow a link to a note that doesn’t exist yet (it’s dimmed), as in Obsidian.
- From the command line:
n0 "Trip to Lisbon". A name that matches no note opens the switcher with the name typed in, and Create note is the last row, already selected when nothing else matches.
The note goes where Obsidian would put it, following Obsidian’s Default location for new notes: the vault’s root, a chosen folder, or next to the current note. A name with a slash, like Projects/Acme/Retro, is a path in the vault. Characters Obsidian won’t allow in a file name (\ : * ? " < > |) become dashes.
Templates
If you use Obsidian’s Templates core plugin and its folder has templates in it, n0te asks how to start (otherwise the note simply starts blank):
- Blank note: just the title, as
# Trip to Lisbon. - One of your templates, from the templates folder you set in Obsidian.
The one you picked last time is already selected, so making a run of notes from one template is Enter, Enter, Enter.
n0te fills in Obsidian’s template variables: {{title}}, {{date}}, {{time}}, and formats like {{date:YYYY-MM-DD}}. An empty # heading gets the title too, and so does a heading that ends with a colon, like # Decision: .
Where the caret lands
The new note opens ready to type, at the first place in it that asks for something: an empty list item or label (like - [ ] or **Owner:** ), or an empty section, whichever comes first. If there’s nothing to fill in, you start typing at the end of the note, which for a blank note is just below the title.
Nothing is written until you type
The status line says NEW NOTE until the note is saved. If you leave without typing, no file is left behind, so a mistyped name costs nothing.
New files outside the vault
Anything that looks like a path opens as a new, empty file instead of a note: ./idea.md, ~/todo.txt, /tmp/scratch.sh, or a name with a file extension other than .md.
n0 ./new-idea.md
It’s created the first time you type.
Daily notes and quick capture
Today’s note
n0 --today # open today's daily note
n0 -e --today # …and start typing at the end
The palette has it too: Today’s daily note.
n0te follows the settings of Obsidian’s Daily notes core plugin: its folder, its date format and its template. If today’s note doesn’t exist yet, n0te creates it from the template, filling in {{title}}, {{date}} and {{time}}. Without those settings, the note is YYYY-MM-DD.md in the vault’s root.
Date formats use Moment.js tokens, as Obsidian’s do. n0te understands the common ones: YYYY, YY, MMMM, MMM, MM, DD, dddd, ddd, HH, hh, mm, ss, A, and ww, which n0te takes as the ISO week number. Bracketed literals like [Week] ww aren’t supported; the brackets end up in the name.
Quick capture
-t adds a task without opening a window:
n0 -t "buy milk"
# Added to Notes/Daily/2026-10-08.md
The task goes at the end of today’s daily note, as - [ ] buy milk. If the note ends with a list, the task joins it; otherwise it starts a new one after a blank line.
Name a note to add to it instead:
n0 -t "call the bank" errands
n0 -t "renew passport" "Projects/Admin"
The note is found the way n0 finds one. If it doesn’t exist, it’s created where Obsidian would put a new note.
From a key or a script
-t takes its text as an argument and prints one line, so anything that can ask for a line of text can feed it: a launcher in dmenu mode, or a script. Bind that to a key in your own Hyprland config, not in ~/.config/hypr/n0te.lua, which setup and the settings page rewrite. Empty text adds nothing: -t refuses it and exits with status 2.
Code and other files
n0te opens any text file, not just notes. Markdown files (.md) render as pages; everything else opens as code, in your monospace font with line numbers and syntax highlighting.
n0 src/main.rs:42 # at line 42
n0 src/main.rs:42:7 # file:line:col, as grep and compilers print it
n0 +42 src/main.rs # the same, vim style
n0 -e +42 src/main.rs # editing at line 42
n0 ~/.config/hypr/hyprland.lua
Paste a location from rg -n, a compiler error or a stack trace, and n0te opens it at that line.
Highlighting
Highlighting uses tree-sitter for bash, C, CSS, Go, HTML, JavaScript, JSON, Lua, Python, Rust, TOML, TypeScript, TSX and YAML. Smaller built-in highlighters cover Hyprland’s classic config, .conf, .ini, .desktop and .service files, diffs and markdown source. Shell dotfiles like .bashrc, .zshrc and PKGBUILD count as bash.
The colors come from your theme, mapped the way Omarchy maps them for Helix, so code in n0te looks like code in your editor.
Code blocks inside notes are highlighted the same way, by their fence’s language (```rust, ```sh).
Reading code
j k | scroll a few lines |
space b, gg G | page down / up, top / bottom |
w | wrap long lines, or stop wrapping |
/, n N | find, next, previous |
y | copy the whole file |
Long lines don’t wrap until you ask with w.
Editing code
e, Enter or a double-click edits the whole file, with the caret on the first line in view (or, for a double-click, where you clicked). Highlighting catches up 200 ms after you stop typing. esc finishes. Saving works as for notes: automatic, atomic, and keeping the file’s mode, line endings and final newline.
When someone else changes the file while you’re editing (a git checkout, a formatter), n0te merges line by line. When the same lines changed in both places, it writes git-style conflict markers and stops autosaving until you’ve resolved them and pressed ctrl s. See Holds.
For bigger work, Edit in editor in the palette saves and opens the file in your editor (Omarchy’s omarchy-launch-editor).
Pipes and stdin
git log -1 | n0 - # stdin
n0 <(git show HEAD:README.md) # a pipe, read the same way
curl -s https://example.com/notes.md | n0 -
n0te reads the input into a private temporary file and renders it as markdown. Since that copy is deleted once read, an stdin window can’t be reloaded or opened in your editor.
Limits
- Files up to 64 MB open. Larger ones show a short page saying so.
- n0te opens text. A file that isn’t valid UTF-8, or that has NUL bytes in it (most binary files), shows a short page explaining why, and nothing is ever written to it.
Living next to Obsidian
n0te is built to share a vault with Obsidian, not replace it. Both read and write the same plain markdown files, and n0te takes care not to fight with Obsidian, sync, or anything else that touches them.
What n0te reads from Obsidian
| From | n0te uses it for |
|---|---|
Obsidian’s obsidian.json (native, Flatpak or Snap) | which vault is open, and the list of vaults in Settings |
.obsidian/app.json | where new notes go (Default location for new notes) |
.obsidian/daily-notes.json | the daily note’s folder, date format and template |
.obsidian/templates.json | the templates folder |
n0te never writes to .obsidian/, and keeps no index, cache or database of its own. Folders starting with a dot (.obsidian, .trash, .git) are left out of the switcher.
Clean round trips
An edit in n0te changes only what you changed. Blocks you didn’t touch are written back byte for byte, with the file’s line endings (LF or CRLF), its final newline, its mode, and symlinks kept. A tick changes one character. So a note edited in n0te shows a one-line diff in git, and Obsidian (or Obsidian Sync) sees one small change.
When both have a note open
n0te watches the file it shows. When Obsidian (or Sync, Syncthing, git pull) changes it:
- with nothing unsaved in n0te, the page reloads in place;
- while you’re editing, n0te merges their change with yours, block by block;
- if Obsidian renames the note, n0te follows it to the new name.
See Saving and changes on disk for the details.
Same rules for links
Wikilinks resolve as Obsidian resolves them, [[ completion writes the shortest unique name as Obsidian does, and following a link to a missing note creates it where Obsidian would. Notes created in n0te start from your Obsidian templates.
The updated: date
If your notes carry a date in their frontmatter, like updated: 2026-10-01, switch on Set updated: to today when saving in Settings. Saving a change then sets it to today, the way plugins like Update time on edit do in Obsidian. It’s only touched when something else in the note changed, and never while you’re editing the frontmatter yourself.
Jumping across
Open in Obsidian, in the palette, opens the current note in Obsidian (through an obsidian:// link). Links to obsidian:// in your notes open on a single click.
What n0te doesn’t do
Some Obsidian features have no counterpart in n0te, by design or not yet:
- No graph, backlinks or vault-wide search. n0te finds notes by name.
- Embeds (
![[note]]) show as links, and images as placeholders, for now. - Plugin syntax isn’t run: a Dataview query shows as a code block, and custom task statuses show as text.
- No canvas, PDF or other attachment views; n0te opens text.
Themes
n0te has no theme of its own. It takes its colors from the running Omarchy theme, and when you switch themes, every open window repaints in place, without reopening anything.
Where the colors come from
- The theme’s
n0te.css, in~/.local/state/omarchy/current/theme/. Omarchy renders it on every theme switch from the template thatn0te --setupinstalls (~/.config/omarchy/themed/n0te.css.tpl). A theme can also ship its ownn0te.css, to color n0te its own way. - The theme’s
colors.toml, for any colorn0te.cssleaves out, or when there’s non0te.cssyet (before your first theme switch after setup, say). - n0te’s own dark palette, when there’s no Omarchy theme at all: black, warm paper-white text and a crimson accent.
Light themes work too: n0te reads mode = "light" from colors.toml.
The colors
n0te.css sets colors with @define-color lines, in #rrggbb hex:
@define-color bg #1a1b26;
@define-color accent #7aa2f7;
| Name | Used for |
|---|---|
bg | the page |
text | body text |
heading | headings, table headers |
text_dim | quotes, links to missing notes |
text_faint | markdown syntax while editing, done tasks, #tags, frontmatter, rules |
surface | code and callout boxes |
surface_2 | inline code |
rule | table lines, card borders, the scrollbar |
raise | the palette and other cards |
accent | links, done boxes, the caret, the current find match |
selection | selected text, find matches |
comment | comments and punctuation in code |
red orange yellow green cyan blue magenta | code highlighting and callouts, mapped as Omarchy maps them for Helix |
A value that isn’t plain hex (a template placeholder that wasn’t filled in, say) is ignored, and n0te’s own value for that color stays.
Styling beyond colors
Everything in n0te.css other than the @define-color lines is ordinary GTK CSS, loaded above n0te’s own, for n0te’s windows: window.n0te, the status line (.status), the palette (.palette), the settings page (.settings) and the link list (.complete). For example:
window.n0te .status { font-size: 10px; }
For theme authors
To give your theme its own n0te colors, ship an n0te.css in the theme’s folder next to colors.toml, with any of the colors above. It comes along when your theme is picked, and n0te uses it instead of the one rendered from the template. Start from n0te’s template, which n0te --setup puts in ~/.config/omarchy/themed/n0te.css.tpl. It shows how most of the colors are derived from colors.toml by default, in Omarchy’s template syntax:
@define-color bg {{ background }};
@define-color text {{ foreground }};
@define-color heading {{ bright_foreground }};
@define-color text_dim {{ mix foreground background 28% }};
@define-color text_faint {{ mix foreground background 46% }};
@define-color surface {{ mix background foreground 4.5% }};
@define-color surface_2 {{ mix background foreground 9% }};
@define-color rule {{ mix background foreground 15% }};
@define-color accent {{ accent }};
@define-color selection {{ selection_background }};
@define-color red {{ red }};
@define-color yellow {{ yellow }};
@define-color green {{ green }};
@define-color cyan {{ cyan }};
@define-color blue {{ blue }};
@define-color magenta {{ magenta }};
raise, comment and orange aren’t in it; n0te derives those itself.
Window transparency
n0te opens as a regular window and keeps Omarchy’s default window opacity, like your terminal. To make it opaque, add a window rule for its class, dev.n0te.N0te, in your Hyprland config.
Keys
Every key in n0te, on one page. The palette (ctrl k) lists the commands too, each with its key.
Anywhere
| Key | What it does |
|---|---|
ctrl k | palette: notes, headings, commands (also : while reading) |
ctrl p | switcher: notes only |
ctrl o ctrl i | back / forward (also alt ← alt → and the mouse’s back and forward buttons) |
ctrl + ctrl - ctrl 0 | text size up / down / back to 100%, in every window |
ctrl , | settings |
ctrl s | save now, even when autosave is paused for a hold |
Reading
| Key | What it does |
|---|---|
j k, ↓ ↑ | next / previous block (in code: scroll) |
space b, Page Down Page Up | page down / up |
gg G, Home End | top / bottom |
Enter | the block’s main action: edit it; follow it if it’s only a link; open or close a foldable callout |
gd | follow the first link in the block |
| click | follow a link, tick a box, fold a callout, or go to the block |
tab | outline: jump to a heading |
/ | find in the note |
n N | next / previous match |
f | focus mode |
h | hide or show done tasks |
y | copy the block as markdown (in code: the whole file) |
w | wrap long lines (code) |
r | reload from disk |
esc | leave focus mode, or clear find matches |
q | close the window (everything is already saved) |
Changing the note
| Key | What it does |
|---|---|
e, double-click | edit the block (in code: the file) |
ctrl e | edit the whole note as source |
o | new block below; in a list, a new item at the same depth |
x, click the box | tick or untick the task |
alt ↑ alt ↓, alt k alt j | move the list item (with its sub-items) up / down |
u | undo the last edit |
ctrl r | redo |
Selecting blocks
V or v starts a selection (or drag the mouse across blocks).
| Key | What it does |
|---|---|
j k gg G | extend the selection |
d, Delete, Backspace | delete the selected blocks |
y | copy them as markdown |
e, Enter | edit them together |
x | tick or untick every task in them |
esc | cancel |
Editing
| Key | What it does |
|---|---|
esc | finish editing |
Enter | new line; continues a list or quote, and ends it on an empty item |
↑ ↓ | past the first or last line: move the edit to the block above or below |
tab shift tab | on a list item: nest it / move it back out |
alt ↑ alt ↓ | on a list item: move it among its siblings |
[[ | link to a note ([[# for a heading in this note) |
ctrl z | undo typing |
ctrl e | from a block: edit the whole note as source; from the source: finish |
ctrl o ctrl i, ctrl k, ctrl p, ctrl , | finish the edit, then do what they do |
| click another block | move the edit there |
All of GTK’s usual text keys work too: ctrl a, ctrl c, ctrl x, ctrl v, word movement with ctrl ← ctrl →, and so on.
The [[ list
| Key | What it does |
|---|---|
↓ ↑, ctrl n ctrl p | choose |
Enter, tab | insert the link |
esc | dismiss the list |
The palette
| Key | What it does |
|---|---|
| type | filter (fuzzy) |
↓ ↑, tab shift tab, ctrl n ctrl p | move |
Enter | open or run |
esc | close |
Find
| Key | What it does |
|---|---|
| type | find as you type |
Enter | close the bar, keep the matches |
esc | close the bar and clear the matches |
Settings page
| Key | What it does |
|---|---|
j k, ↓ ↑, tab shift tab | move |
space, Enter | switch or change the setting |
← →, h l | pick the vault or the text size |
esc, q, ctrl , | close |
Command line
n0te [options] [file…] n0 is the same command
n0 hands its arguments to n0te’s service and returns as soon as the window is up, so your shell stays free. If no service is running, it starts one first.
Opening things
n0 # the switcher over your vault
n0 "Launch plan" # a note by name, anywhere in the vault
n0 "Projects/Acme/Launch plan" # vault-relative, with or without .md
n0 Projects/Acme # a folder: the switcher, narrowed to it
n0 blue # no such note: the switcher, with "blue" typed in
n0 src/main.rs:42 # any file at a line; also file:line:col
n0 +42 src/main.rs # the same
n0 '~/notes/todo.txt' # a quoted ~ is expanded
n0 ./new-idea.md # a file that doesn't exist yet opens empty
n0 a.md b.md # several files, a window each
git log -1 | n0 - # stdin
n0 <(git show HEAD:README.md) # a pipe, read the same way
How a name is found
- A real path, relative to where you are, wins.
- Then a path in the vault, with or without
.md. - Then any note with that name, the way a wikilink resolves.
- A folder in the vault opens the switcher narrowed to that folder.
- Anything else that looks like a path (
./x,../x,~/x,/x,notes/x.mdwhennotes/is a folder where you are, or a short file extension other than.md, likex.sh) opens as a new, empty file, written when you type. So does everything, when there’s no vault. - Anything else opens the switcher with what you typed, ready to find it or create it. A name with a dot that isn’t an extension, like
v0.1 plan, counts as a name.
Options
| Option | What it does |
|---|---|
+N | start at line N |
-e | start editing: at line N if given, else at the end of a note (at the top of code) |
-t TEXT [NOTE] | add - [ ] TEXT to today’s daily note, or to NOTE, without opening a window |
--today | today’s daily note, created from your template if needed |
- | read from stdin |
--settings | open the settings page |
--setup | wire n0te into Omarchy; --setup default also opens .md files in n0te; --setup remove undoes it all |
--service | run the service (n0te starts it by itself when needed) |
--quit | save every open note and stop the service |
--help, -h | print the usage |
--version, -V | print the version |
--bench | report the time to the first frame, then close |
--check | render every note in the vault and report problems (for development) |
Examples
n0 -e --today # write in today's note
n0 -t "buy milk" # capture a task
n0 -t "call the bank" errands # …into a note
rg -n TODO | head -1 | cut -d: -f1,2 | xargs n0 # the first TODO, at its line
n0 -e +1 "Inbox" # edit the top of a note
Exit status
| Status | Meaning |
|---|---|
| 0 | done; for an open, the window is up |
| 1 | something failed, and n0te said what (-t couldn’t write, --quit couldn’t save a note) |
| 2 | a usage mistake: an unknown --option, -t without text |
| 127 | n0te-app couldn’t be started |
Tab completion
In bash, n0 <Tab> completes note names from your vault, also by file name (n0 laun<Tab> → Projects/Acme/Launch plan), then files and options. n0te --setup installs it; it takes effect in new shells.
Environment
n0 sends only its arguments and the current folder to the service, so these are read where the service runs: your session’s environment if it starts at login, or the shell of the first n0. After changing one, n0 --quit so the next n0 starts a service that sees it. (Tab completion runs in your shell, so it sees your shell’s N0TE_VAULT straight away.)
| Variable | What it does |
|---|---|
N0TE_VAULT | use this vault, over Settings and Obsidian’s |
XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_STATE_HOME | where config, app entries and state go (defaults under ~) |
XDG_RUNTIME_DIR | where the service’s socket and log live |
GSK_RENDERER | GTK’s renderer; n0te defaults to gl, which measured fastest |
Settings
Open the settings page with ctrl , in any window, Settings in the palette, or n0te --settings. j k move, space changes the row you’re on, ← → pick the vault or the text size, and esc closes. Changes take effect at once.
Notes
| Setting | What it does | Default |
|---|---|---|
| Vault | Follow the vault Obsidian has open, or always use one of Obsidian’s vaults | follow Obsidian |
| Text size | The reading size, 70% to 240%; also ctrl + ctrl - ctrl 0 anywhere | 100% |
| Hide done tasks in new windows | New windows start with done tasks folded away; h shows them | off |
Set updated: to today when saving | When a note’s frontmatter has updated: with a plain date (2026-10-01), saving a change sets it to today. Left alone while you’re editing the frontmatter yourself | off |
Omarchy
| Setting | What it does |
|---|---|
| super n opens n0te | Switches the binding line in ~/.config/hypr/n0te.lua on or off; Hyprland reloads its config |
| Start at login | The same, for the line that starts the service at login |
| Open markdown files in n0te | Makes n0te the text/markdown default. Switching it off gives .md files back to the app that had them |
These three aren’t stored by n0te. The page reads them from Hyprland’s config and xdg-mime each time it opens, so it always shows what’s really set, even after a hand edit or n0te --setup. Without Omarchy’s Lua config (~/.config/hypr/hyprland.lua), the Hyprland rows are greyed out.
config.toml
The settings in the first group live in ~/.config/n0te/config.toml. Open config.toml, at the bottom of the page, opens it in n0te. It’s fine to edit by hand: n0te reads it fresh each time it needs a setting, except text_size, which a hand edit changes after n0 --quit.
# A vault to use instead of the one Obsidian has open.
vault = "/home/you/Notes"
# Start new windows with done tasks hidden (h shows them).
hide_done = false
# When saving a note, set its frontmatter `updated:` to today.
bump_updated = false
# Reading text size, percent (ctrl + and ctrl - change it).
text_size = 100
- Lines are
key = value. A line that starts with#is a comment; a comment after a value isn’t supported (hide_done = true # xreads as false). Unknown keys are ignored. vaultis a quoted path. Leave it out (or comment it out) to follow Obsidian.text_sizeaccepts 50 to 300 by hand; the keys step through 70, 80, 90, 100, 110, 120, 135, 150, 170, 200 and 240.- The vault in
config.tomlcan be any folder, not only one Obsidian knows.N0TE_VAULToverrides it.
What renders
n0te reads CommonMark with the extensions Obsidian uses. Here is what each construct looks like in n0te, and what isn’t supported yet.
Text
| Markdown | In n0te |
|---|---|
**bold**, *italic*, ~~struck~~ | as you’d expect |
==highlight== | highlighted, in the theme’s colors |
`inline code` | monospace, on a faint background |
#tag, #nested/tag | quiet, in faint text |
%%comment%%, <!-- comment --> | hidden, as in Obsidian’s reading view |
| hard-wrapped lines | reflowed to the window’s width; deliberate line breaks stay |
Blocks
| Markdown | In n0te |
|---|---|
# Heading to ###### Heading | three sizes (#, ##, and ### and below), in the theme’s heading color |
- item, 1. item, nested lists | hanging indents; every item is its own block |
- [ ] task, - [x] task | drawn boxes; see Checklists |
> quote | indented, set in italics |
> [!type] Title callouts | boxed, in the type’s color; + and - fold them |
``` fenced code | boxed and highlighted by language; long lines wrap inside the box |
| tables | set in the reading face; see below |
--- | a rule |
[^1] footnotes | a quiet [1] in the text, and the footnote, marked [1], where it’s written |
| YAML frontmatter | one quiet line at the top |
Links
| Markdown | In n0te |
|---|---|
[[Note]], [[Note|text]] | a link, resolved like Obsidian’s |
[[Note#Heading]], [[#Heading]] | opens at the heading |
[[Missing note]] | dimmed; following it creates the note |
[text](Other%20note.md) | works like a wikilink |
[text](https://…), <https://…> | opens in your browser |
![[Note]] embeds | a link to the note, marked ↳; the note isn’t shown inline yet |
, ![[image.png]] | a quiet ▣ placeholder with the file’s name; images aren’t drawn yet |
Tables
Tables are set in the reading face, not as a monospace grid. Columns are measured from their text and line up on tab stops, including right and center alignment. Cells keep bold, italics, code, links and <br> line breaks.
A table that fits the text column lines up with the text. A wider one grows toward the window’s edges (up to 1200 px), and past that its cells wrap, with short columns keeping their width. A hairline sits under the header and fainter ones between rows. Resizing the window lays tables out again.
Not yet
- Images and embeds show as placeholders and links. Drawing them is planned.
- Math (
$…$,$$…$$) shows as written. - Mermaid and other diagram blocks show as code.
- Plugin syntax (Dataview, Tasks queries) shows as written, usually as a code block.
- Custom task statuses (
[-],[>]) show as text. - HTML beyond comments and
<br>in tables shows as quiet source text.
Files and environment
Everything n0te reads and writes outside your notes. Paths are shown with their defaults; XDG_CONFIG_HOME, XDG_DATA_HOME and XDG_STATE_HOME move them when set.
What n0te writes
| Path | What | Written by |
|---|---|---|
~/.config/n0te/config.toml | your settings | the settings page, ctrl + / ctrl - |
~/.config/hypr/n0te.lua | the super n binding and the service at login | setup, the settings page |
~/.config/hypr/hyprland.lua | one require("hypr.n0te") line, with a comment | setup (removed by --setup remove) |
~/.config/omarchy/themed/n0te.css.tpl | the theme template | setup |
~/.local/share/applications/n0te.desktop | the app entry, unless a package installed one | setup |
~/.local/share/bash-completion/completions/n0te, n0 | tab completion, unless a package installed it | setup |
~/.local/state/n0te/markdown-default | the app that opened .md files before n0te, to give them back | setup, the settings page |
If you’ve changed the app entry, the theme template or the completion file, setup leaves yours in place, and --setup remove leaves it too. n0te.lua is n0te’s own: setup and the settings page rewrite it, and --setup remove deletes it, so put your own Hyprland lines elsewhere. Files are replaced atomically, so Hyprland never reads half a file, and a symlink (dotfiles managed with stow, say) stays a symlink.
While it runs
These live in $XDG_RUNTIME_DIR (usually /run/user/1000):
| Path | What |
|---|---|
n0te.sock | the service’s socket, readable only by you |
n0te-service.log | what the service prints, appended to |
n0te-stdin-*.md | a copy of stdin or a pipe, deleted once the window has read it |
Without XDG_RUNTIME_DIR, they go in a private n0te-UID folder in $TMPDIR (or /tmp), which n0te creates readable only by you. It refuses a folder of that name that isn’t yours or that others can read.
While saving a note, n0te writes .NAME.n0te-PID~ next to it for an instant, then renames it over the note.
What n0te reads
| Path | For |
|---|---|
~/.config/obsidian/obsidian.json; Flatpak and Snap equivalents | Obsidian’s vaults, and which one is open |
VAULT/.obsidian/app.json, daily-notes.json, templates.json | new notes, daily notes, templates |
~/.local/state/omarchy/current/theme/n0te.css, colors.toml | colors |
~/.local/state/omarchy/current/theme.name | watched, to repaint on a theme switch |
~/.config/hypr/hyprland.lua, n0te.lua | what the settings page shows for super n and login |
Environment
These are read by the service when it starts, not by each n0; see Command line.
| Variable | What it does |
|---|---|
N0TE_VAULT | the vault to use, over Settings and Obsidian |
XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_STATE_HOME | base folders, defaulting to ~/.config, ~/.local/share and ~/.local/state |
XDG_RUNTIME_DIR | where the socket, log and stdin copies go |
XDG_DATA_DIRS | where to look for other apps’ entries (to name the markdown app you had) |
HYPRLAND_INSTANCE_SIGNATURE | when set, n0te asks Hyprland to reload after changing n0te.lua |
GSK_RENDERER | GTK’s renderer; n0te sets gl unless you choose another |
For development, N0TE_APP_ID runs a separate instance with its own app id and N0TE_FLOAT makes windows fixed-size; see Contributing.
Saving and changes on disk
n0te’s promise is that it never loses your words and never clobbers anyone else’s. This page is how it keeps it.
When n0te saves
There’s no save button to forget. n0te saves:
- a second after you stop typing;
- when you press
escto finish an edit; - when its window loses focus;
- when you close the window, or leave the note for another;
- for every open note, on
n0 --quit. If one can’t be saved, the service doesn’t quit, and says which.
A note you only read is never written. (The exception is today’s daily note: --today, the palette’s Today’s daily note and -t create it from your template as soon as they need it.)
How it writes
- It checks the disk first. Just before writing, n0te reads the file again. If it changed since n0te last saw it, that change is merged first (see below), so nothing is ever overwritten blind.
- It changes only what you changed. The new text is your edited block spliced into the file as it is on disk. Every other block goes back byte for byte.
- It writes atomically. The text goes to a hidden temporary file next to the note (
.NAME.n0te-PID~), which is flushed to disk, then renamed over the note. Readers see the old file or the new one, never half of one, and Obsidian sees one clean change.
It keeps the file’s permissions, follows symlinks (the file they point to is replaced, and the link stays), and keeps the line endings (LF or CRLF) and the final newline, or the lack of one.
In a folder you can’t write to, the temporary file can’t be made; a note there that you can write is saved in place instead.
When a save fails
No permission, a full disk, a missing folder: the status line turns red and says why (NOT SAVED · …). Your text stays in the window. Closing the window or leaving the note then takes a second try (do that again to discard your changes), and only that drops the change.
When the file changes on disk
n0te watches the file it shows. When something else writes to it (Obsidian, Obsidian Sync, Syncthing, git pull, a script) n0te waits until two reads in a row agree, so a file still being written isn’t taken half-done. Its own saves are recognised and ignored.
- Nothing unsaved: the page reloads in place, without jumping, and the status line says
Reloaded: changed on disk. - You’re editing: n0te merges. It compares three versions: the file as it was when you started, yours, and the one on disk now. Changes on either side are kept. Notes merge block by block, and then line by line inside a block; code merges line by line. Your edit stays open where it was. The status line says
Merged changes from disk. - The same lines changed on both sides: in a note, n0te keeps both versions, yours first, saves, and says
kept both versions, yours first. Tidy up whichever you don’t want.
Holds
A few situations are too risky to settle on its own. Then n0te holds: autosave pauses, the status line says why in red, and you decide.
| Status line | What happened | What to do |
|---|---|---|
CONFLICT MARKERS FROM A MERGE | A merge in a code file hit lines changed on both sides, and n0te wrote git-style <<<<<<< markers into your copy | Fix them, then ctrl s |
CHANGED ON DISK TOO | Both sides changed, and the file was too big to merge | ctrl s keeps yours. To take the disk’s, esc, then r twice |
MOVED OR DELETED ON DISK | The file is gone from where it was | ctrl s writes your unsaved changes back at the old path. With nothing unsaved, there’s nothing to keep: close the window |
BINARY ON DISK NOW | The file on disk turned binary (NUL bytes) | ctrl s keeps yours. To take the disk’s (read-only), r twice |
ctrl s works while reading and while editing. r reloads only while reading, and during a hold the first press warns that your version will be dropped (do that again to discard your changes); a second press within five seconds does it.
A moved file isn’t silently recreated, because that would undo a deliberate move or delete. But two common cases need no hold:
- A rename in Obsidian is followed: the window switches to the new name.
- Vim and Emacs rename a file to a backup while saving it. n0te ignores that rename and stays on the file.
Undo across changes
u undoes your edits as merges, too: it takes out your change and keeps anything that changed on disk since. History is kept per file for as long as the service runs, so it survives closing and reopening a note, but not n0 --quit or a logout.
The updated: date
With Set updated: to today when saving on, a save that changes something also sets a plain updated: YYYY-MM-DD in the frontmatter to today. The new date is exactly as long as the old one, so nothing else moves. It’s skipped while you’re editing the frontmatter yourself, or the whole note as source.
The service
Opening a note in n0te feels instant because almost nothing happens when you type n0. The work was done earlier, by a service that’s already running.
Two programs
| Program | Job |
|---|---|
n0te (and n0) | The command you type. It doesn’t load GTK, so it starts in under a millisecond. It sends its arguments to the service over a Unix socket and exits when the service answers. |
n0te-app | The GTK app that renders notes and owns the windows. As n0te-app --service, it stays resident with the GPU, fonts and syntax grammars warmed up. |
A GTK app takes 200 ms or more just to start. The service pays that once per session; every open after that costs only the work of rendering the note.
Starting and stopping
- At login, if Start at login is on (
n0te --setupturns it on), Hyprland runsn0te --service, so even the first open is instant. - Otherwise, the first
n0of a session starts the service in the background. That open takes about 350 ms; the next ones about 30. - The service runs in its own session, so closing the terminal you started it from doesn’t stop it.
n0 --quitsaves every open note and stops it. Do that after an update or a rebuild, so the next open runs the new version.
If the service can’t be reached or started, n0 runs n0te-app directly for that open. It’s slower, but it works.
Timings
Measured from spawning n0 to the compositor showing the first frame, on the reference machine (RTX 3070, Hyprland 0.56, 240 Hz), opening a 31 KB note:
| Open | p50 | p95 |
|---|---|---|
| Service running | 28 ms | 29 ms |
| Cold, no service | 203 ms | 207 ms |
The first n0 of a session, which also starts the service, takes about 350 ms. Rendering every note in a 49-note vault takes 29 ms in total.
n0 --bench FILE reports the time to first frame on your machine, and bench/m0.sh in the repository runs the full measurement.
The socket and the log
The socket is n0te.sock in $XDG_RUNTIME_DIR (usually /run/user/1000), readable only by you. The service’s output goes to n0te-service.log next to it: start-up and socket errors, GTK warnings, and a crash’s last words. Problems with a file show in the window’s status line instead.
Without XDG_RUNTIME_DIR, both live in a private n0te-UID folder in /tmp that n0te creates for you, and refuses to use if anyone else owns it or can read it.
Memory
The service holds one GTK process with its windows. Your undo history lives there too, which is why u still works after you close and reopen a note, and why it’s gone after n0 --quit.
Blocks
Most of n0te’s keys act on blocks, and editing happens a block at a time. This page explains what a block is and how editing one stays exact.
What’s a block
When n0te renders a note, it cuts it into blocks:
- each paragraph, heading, code block, table, rule and footnote;
- each list item, nested ones too, so
jstops on every task in a checklist; - each quote or callout, as one block with everything inside it.
A list item and its sub-items form a subtree. o adds a new item after the whole subtree, alt ↑ and alt ↓ move the item with its subtree, and tab nests it along with its subtree.
Every block knows its source
For every block, n0te records exactly which bytes of the file it came from, the way the file is on disk, %%comments%% included. That’s what makes block editing safe:
eswaps the block’s rendered text for those bytes, in an edit box. Only the box is editable; the rest of the page is locked.- While you type, only that block is parsed and restyled, so a long note stays fast.
- On save, the edited text is spliced into the file in place of those bytes. Every other byte of the file stays as it was. Blank lines at the edges of your edit are tidied, so a block can’t swallow its neighbours’ spacing.
- A tick changes the one status character between
[and], and the page updates where it is, without re-rendering.
n0te --check tests this on your whole vault: it renders every note, checks that each block maps back to its source, and that putting an unchanged block back reproduces the file byte for byte. It also lists any markdown left on the page as literal text. On the maintainer’s vault, both counts are 0.
Edits of more than one block
Vtheneedits a run of blocks as one piece of source.ctrl eedits the whole note as source, for changes that cut across blocks. n0te restyles it 200 ms after you stop typing.- Code files are one block:
eedits the whole file.
The page doesn’t jump
When a block changes, n0te re-renders the note, but it doesn’t redraw the page. It keeps the parts of the page that are the same before and after, replaces only the middle, and holds the block you were on in place while GTK lays out the new text. So esc after an edit, deep in a long note, leaves you where you were.
Safety and privacy
n0te handles your notes, so here is everything it does that reaches beyond the window.
Privacy
- No network. n0te makes no network connections: no telemetry, no update checks, no accounts. The only time anything leaves your machine is when you open a web link, which goes to your browser.
- No index or cache. n0te reads your notes when it needs them and keeps nothing about them on disk. The only things it writes outside your notes are listed in Files and environment.
- Private runtime files. The socket, log and stdin copies live in
$XDG_RUNTIME_DIR, or an0te-UIDfolder that only you can read. n0te never puts them in a shared/tmp, where another user could read or replace them.
Your files
- Atomic saves, so a crash or power cut mid-save leaves the old file or the new one, never half of one. (The one exception is a note in a folder you can’t write to, which is saved in place; see Saving.)
- Merges, not overwrites, when a file changes on disk while you edit.
- Holds for the cases n0te can’t settle safely: conflict markers in code, a file moved away, a file turned binary.
- Binary files are read-only. n0te opens text. A file with NUL bytes or invalid UTF-8 shows a short page explaining why, and n0te never writes to it.
- No surprises from links. Web, mail and
obsidian://links open on a click. Any other scheme (file:, other apps’ handlers) first shows its target, and opens only on a second click, since link text can hide where a link goes.
Your system
- Setup is explicit. Nothing changes in Hyprland, the app menu or xdg-mime until you run
n0te --setupor flip a switch on the settings page, andn0te --setup removeundoes it. - Your edits are kept. Setup leaves the app entry, the theme template and the completion file alone if you’ve changed them, and it keeps symlinked dotfiles as symlinks.
- The settings page reads the real state. It never trusts a cached copy of Hyprland’s or xdg-mime’s settings.
Reporting a problem
If n0te ever loses or mangles text, that’s the most serious kind of bug. Note the steps, what the status line said, and anything in n0te-service.log (see The service). n0te’s issue tracker, and private reporting for security problems, open with the public repository.
Troubleshooting
First, the status line and the log
Problems with a file (a failed save, a merge, a hold) show in the status line at the bottom of the window, in red, with what to do. Note what it says.
Problems with n0te itself (a crash, a GTK warning, the service failing to start or listen) go to the service’s log:
less "$XDG_RUNTIME_DIR/n0te-service.log"
To start fresh, n0 --quit, then open any note: that starts a new service.
super n does nothing
- Open Settings (
n0 --settings) and check super n opens n0te. If the row is greyed out, there’s no~/.config/hypr/hyprland.lua: you’re not on Omarchy’s Lua config, and you’ll need to bind the key yourself (see Install). - Check that
~/.config/hypr/hyprland.luastill has therequire("hypr.n0te")line. Runningn0te --setupagain puts it back. - Hyprland runs
n0tefrom its ownPATH. If you installed from source into~/.local/bin, make sure that’s on thePATHyour session starts with.
Every open is slow
The service isn’t staying up. Check whether it’s running:
pgrep -a n0te-app
If nothing shows --service, look at the log: a crash leaves its last words there. A first open of about 350 ms per session is normal without Start at login; every open taking 200 ms or more is not.
n0te still behaves like the old version
After an update or a rebuild, the old service is still running. n0 --quit, then open a note.
The wrong vault opens
n0te follows the vault Obsidian has open (or opened last). To pin one, pick it under Vault in Settings, or set vault = "…" in ~/.config/n0te/config.toml. N0TE_VAULT overrides both, but only in the environment the service started in (your session’s, or the shell of the first n0); check it isn’t set somewhere you forgot, and n0 --quit after changing it.
The colors don’t match my theme
- Right after
n0te --setup, the theme template hasn’t been rendered yet. Switch theme once (or switch away and back). - If the theme ships its own
n0te.css, that one wins; check~/.local/state/omarchy/current/theme/n0te.css. - Off Omarchy, n0te uses its own dark palette. See Themes.
The status line is red
It’s either a failed save (NOT SAVED · …) or a hold. Both say what happened and what to do. Your text is still in the window either way.
For NOT SAVED, check that you can write the file and its folder (ls -l), and that the disk isn’t full.
A file opens as a short page saying it’s binary
The file has NUL bytes or isn’t valid UTF-8. n0te opens text and won’t edit such a file, so it can’t damage it. Open it with a tool made for its format.
“isn’t a private folder of yours”
Without XDG_RUNTIME_DIR, n0te keeps its socket in n0te-UID in your temp folder, and refuses to use that folder if someone else owns it or others can read it. Someone (or something) created it with the wrong owner or permissions. Remove it, or make sure XDG_RUNTIME_DIR is set, as it is in any normal login session.
Tab completion doesn’t work
It needs bash with bash-completion 2.12 or newer, and takes effect in new shells. n0te --setup installs it, unless a package already did.
Markdown files open in another app
Switch on Open markdown files in n0te in Settings, or run n0te --setup default. If another app takes them back later (some do when they update), switch it on again.
Still stuck
n0te doesn’t have a public issue tracker yet; it comes with the public repository. When you report a problem, include what you did, what happened, what the status line said, n0te --version, your GTK version (pacman -Q gtk4) and anything in the log.
Contributing
n0te is MIT-licensed. Its repository isn’t public yet; this page is for working on n0te once it is, and for whoever is working on it already. Bug reports, fixes and docs improvements will be welcome. For a bigger change, open an issue first, so we can talk about whether it fits: n0te stays small on purpose.
Build and test
cargo build --release # 0 warnings
cargo clippy --release # 0 warnings too
cargo test --release # unit tests
The unit tests cover splicing, merging, undo, tasks, lists and the updated: date in the app, and config, vault detection, the runtime folder and setup in the library. GTK tests run when there’s a display and are skipped otherwise.
N0TE_APP_ID=dev.n0te.Check target/release/n0te-app --check
--check renders every note in your vault and must report 0 with leftover syntax and 0 that don't round-trip. Run it after any change to the renderer. tests/fixtures/kitchen-sink.md has every construct the renderer handles.
Run a test instance next to your own
Your own n0te service keeps running while you work. To try a build without touching it:
N0TE_APP_ID=dev.n0te.Somethingruns a separate instance under its own app id.N0TE_VAULT=target/test-vault, pointing at a copy of a vault, keeps tests from writing to your real notes.- To try setup or the settings page, point
HOMEandXDG_CONFIG_HOME,XDG_DATA_HOMEandXDG_STATE_HOMEat a sandbox (your shell may set the XDG ones explicitly), and unsetHYPRLAND_INSTANCE_SIGNATUREso n0te doesn’t reload your Hyprland.
Driving a window from a script
N0TE_VAULT=<copy> bench/script.sh OUT FILE STEPS
bench/script.sh runs n0te on GTK’s Broadway backend with a headless Chromium as its viewer, so nothing appears on your screen and nothing takes focus. A steps file (keys, typing, clicks on text, scrolls, waits, screenshots) runs against a copy of FILE, and the log shows every scroll change with the step that caused it, plus where blocks and the caret are. The steps are documented at the top of src/bin/n0te-app/script.rs.
bench/drive.sh does the same with a real, floating window, wtype and grim. It takes focus, so don’t run it while you’re typing elsewhere.
Timing
bench/m0.sh [FILE] [RUNS]
Measures cold and warm opens to the first frame. The targets are under 50 ms warm and under 400 ms cold (p95). It stops any running n0te-app, including your own service, so close what you’re working on first.
How the code is laid out
| Path | Job |
|---|---|
src/main.rs | n0te, the client: std only, never links GTK; sends arguments to the service, starts it if needed, runs --setup and --complete |
src/lib.rs | shared, std only: finding the vault (obsidian.rs), settings (config.rs), the runtime folder (runtime.rs), Omarchy wiring (setup.rs) |
src/bin/n0te-app/main.rs | the GTK app: the service, arguments, prewarming, --check, --bench |
src/bin/n0te-app/window.rs | one window: history, palette, find, keys, status line, editing, saving, file watching, undo |
src/bin/n0te-app/render.rs | markdown to a text buffer, with blocks and their source ranges, links, headings and folds |
src/bin/n0te-app/edit.rs | no GTK: splicing, atomic writes, the three-way merge, undo history, list helpers |
src/bin/n0te-app/view.rs | the text view: boxes, the edit box, the block tick, the focus veil, line numbers |
src/bin/n0te-app/table.rs | tables, measured and wrapped to the window |
src/bin/n0te-app/highlight.rs | tree-sitter and the small built-in highlighters |
src/bin/n0te-app/vault.rs | the note index, wikilinks, daily notes, templates |
src/bin/n0te-app/theme.rs | Omarchy colors and live reload |
packaging/ | the Hyprland file, desktop entry and theme template; the release tarball (release.sh, install.sh, licenses.py) and the AUR package draft |
site/ | this website and these docs |
CLAUDE.md at the repository’s root has the working notes: design decisions, how editing works inside, and the gotchas already paid for. Read it before a big change.
These docs
The docs are an mdBook in site/docs/, and the front page is plain HTML in site/www/. To preview both:
site/build.sh --serve # builds into site/dist and serves it on localhost
A change to how n0te behaves should update its page in the same commit. Releases and the site deploy from a maintainer’s machine with site/deploy.sh; see site/README.md.