This page is rendered from spec.md. Hand that file to a person or an LLM agent to build a compatible app for Windows or Linux.
notr implementation spec
This document is everything needed to build a notr-compatible app on another platform. It is written for a person or an LLM agent doing the implementation. Everything here describes behavior the macOS app has as of version 0.1.2; where the spec says "must," the Mac app does it that way and a compatible app has to as well, so that both can share one notes file through any file sync.
The product idea in one sentence: a keyboard shortcut summons a see-through drawer over whatever the user is doing, they type a thought, press Return, and they are back where they were with nothing to name, file, or remember.
1. Principles
These are not decoration. Every one of them exists because the alternative made the user abandon the app.
- The file is the product. All state lives in one plain Markdown file the user owns. The app is a view onto it. If the app disappeared, the notes would still be readable, editable, and grep-able.
- Capture asks nothing. No title, tag, folder, type, or confirmation at capture time. The only decision is what to type.
- Every action is one key. Strike, carry, shortlist, trash, and edit are single keystrokes with no modifier. Undo is always available. There are no confirmation dialogs anywhere, including for delete.
- Nothing is ever lost. Drafts persist across dismiss and relaunch. Deletes go to a trash file. Every change in the manage window is undoable.
- Do not steal focus. The drawer appears over the user's work and takes keyboard input without activating the app, so dismissing it returns focus exactly where it was.
- No automatic text correction. Spelling, quotes, dashes, capitalization, completion: all off. What is typed is what is saved.
- Two levels only. The drawer is level zero. The manage window is level one. There is no level two.
2. The notes file
2.1 Location
Default path: ~/Documents/notr/notr.md. The user can choose another path in settings. The app creates the directory and an empty file on first run.
A trash file lives beside it as notr.trash.md, same format.
Encoding is UTF-8 with \n line endings. Do not write a byte-order mark.
2.2 Structure
## 2026-09-11 Friday
- [ ] invoice Dana by Tue, PO 4471-B <!-- 11:53 -->
- [ ] ! book the dentist <!-- 11:20 -->
- [x] ~~pick up prints~~ <!-- 10:02 -->
- [>] renew passport <!-- 09:14 carried 2026-09-12 -->
- a plain note with no checkbox <!-- 09:01 -->
- ~~a plain note that was struck~~
- [ ] a multi-line entry <!-- 08:40 -->
continues on lines indented by two spaces
as many as needed
## 2026-09-10 Thursday
- [ ] older item
Rules:
- A day section starts with
##followed by an ISO dateyyyy-MM-dd, a space, and the English weekday name. Days appear newest first in the file. The heading text after##must match exactly what the app would generate for that date, because sections are located by string comparison. - One blank line follows each heading. One blank line separates sections. No trailing blank lines at the end of the file beyond the final newline.
- An entry starts with
-at column zero. Continuation lines start with exactly two spaces and belong to the entry above. Entries within a day appear in capture order in the file, oldest first. The app displays them newest first. - Anything before the first heading is a preamble and must be preserved on write. Any line inside a section that is not an entry, a continuation, or blank is a raw line and must be preserved in place.
2.3 Entry grammar
After - , in this order:
| Piece | Form | Meaning |
|---|---|---|
| Marker | [ ] , [x] , [>] , or none | open task, done task, carried task, or plain note. [X] is accepted on read and written as [x]. |
| Active flag | ! | shortlisted. Only meaningful on open tasks and notes; cleared when the entry is done or struck. |
| Text | first line of the entry | wrapped in ~~ when the entry is done or struck |
| Metadata | two spaces, then <!-- HH:mm comment --> | optional. Time is the 24-hour capture time. comment is free text after the time, currently only carried yyyy-MM-dd. Either part may be absent. |
States and how they serialize:
| State | Marker | Text wrap | Example |
|---|---|---|---|
| open | [ ] | none | - [ ] text |
| done | [x] | ~~text~~ | - [x] ~~text~~ |
| carried | [>] | none | - [>] text <!-- carried 2026-09-12 --> |
| note | none | none | - text |
| struck | none | ~~text~~ | - ~~text~~ |
Parsing must be forgiving: unknown prefixes are text, a missing metadata block is fine, ~~ on a note makes it struck. Serializing must be exact: a file parsed and written back unchanged must be byte-identical apart from blank-line normalization. The Mac app has a test for this and a compatible app should too.
2.4 Multi-line text
Text may contain newlines. On write, the first line goes on the entry line and each further line is written with a two-space indent. Metadata goes on the first line only. On read, strip the two-space indent and join with \n.
2.5 Entry identity
Entries have no IDs in the file. The app derives a stable identity from heading + "|" + text + "|" + occurrence, where occurrence counts entries with identical text within the same section, starting at zero. Any deterministic scheme with those inputs works; it only needs to survive re-parsing so selection is not lost when the file is rewritten.
3. Operations
All operations read the current file, transform the document, and write it back atomically (write to a temporary file in the same directory, then rename over the original). Never append to the file in place. On platforms with a file coordination API, use it.
The app watches the file and reloads when something else changes it. The watch must survive the atomic rename. Debounce about 300 ms. Ignore changes whose content equals what the app last wrote.
3.1 Capture
Trim leading and trailing whitespace. If empty, do nothing. Otherwise append an open entry with the current HH:mm to today's section. If today's section does not exist, create it at the top of the file with a blank line after the heading and a blank line before the previous first section.
3.2 Toggle done
| From | To |
|---|---|
| open or carried | done, and clear the active flag |
| done | open |
| note | struck, and clear the active flag |
| struck | note |
3.3 Carry to today
Only for entries not already in today's section. Set the entry's state to carried, set its comment to carried yyyy-MM-dd with today's date, and append a new open entry with the same text and the current time to today's section. The original keeps its place and its time.
3.4 Shortlist (active)
Toggle the flag. If the entry was done or struck, reopen it (done becomes open, struck becomes note). Shortlisted entries display in an Active section above today, with their original day shown beside them, and disappear from their day while flagged. In the file they never move.
3.5 Edit
Replace the text. Trim; if the result is empty, do nothing. Keep marker, flag, time, and comment.
3.6 Trash
Remove the entry from the notes file and append it, serialized exactly as it was, to the trash file under a heading for today's date. Purge sections in the trash file older than 30 days on launch. Restoring an entry removes it from the trash and appends it to today as a new open entry with the current time. Deleting from the trash is permanent, with no confirmation.
3.7 Undo
Snapshot the whole file's text before each operation. Keep the last 50. Undo restores the previous snapshot and writes it; redo goes forward. Any new operation clears the redo stack. Capture from the drawer is not on this stack; instead, for ten seconds after a capture, the drawer offers to take back that one entry (remove the most recent open entry under today whose text matches, and put the text back in the field).
3.8 Sweep
The set of stale entries is every open, non-shortlisted entry in sections before today, oldest day first, in file order within a day. Present them one at a time. For each: carry to today, mark done, trash, shortlist, or skip. Skipped entries are not shown again during that sweep. The count of remaining entries is visible throughout. When empty, show "Nothing to carry. You're current."
4. Capture drawer
4.1 Window
- Summoned by a global shortcut that works from any application. Default on macOS is ⌥Space. Pressing it again dismisses.
- Appears on the monitor that contains the mouse cursor, anchored to the top edge, horizontally centered. The user can nudge it to the left or right third of the screen and back; the position is remembered.
- Width is
max(640, drawerTextSize × 24)points. Height fits the content: a header line, the text area, and a hint line. - Corners are square-ish (radius 6) on the bottom two corners only, so it reads as a drawer hanging from the screen edge.
- Must appear over full-screen applications and on every virtual desktop, and must not activate the app or move focus away from the user's window. On dismiss, focus is back where it was with no visible change.
- Translucent so the user can read what is beneath while typing. A flat tint of the ground color at a configurable opacity, default 60%, range 30% to 95%. No blur by default; an optional blur radius 0 to 8 is offered because some users prefer it. Text stays fully opaque.
- Hold the shortcut's modifier alone (⌥ on macOS) for about 150 ms and the drawer fades to 10% opacity so the user can read through it. Release and it returns with the draft intact.
- Slides down over 120 ms on show; settles down 10 points and fades over 150 ms on commit. Both are skipped when the OS reduce-motion setting is on.
- Clicking anywhere outside the drawer dismisses it and keeps the draft.
4.2 Content
Top line, small monospaced, muted: today's date as EEEE, MMM d on the left and the open count as N open on the right. Then the text area. Then a hint line listing the keys.
The text area starts at three lines tall, grows with the content, and stops growing when the whole drawer reaches about width / 1.618, after which it scrolls with the caret kept in view.
4.3 Keys
| Key | Action |
|---|---|
| Return | Commit and close |
| ⌘Return (Ctrl+Return elsewhere) | Commit and keep the drawer open, cleared, for the next thought |
| Shift+Return | Newline inside the entry |
| Escape | Close and keep the draft. The draft is persisted and restored on next show, including across relaunch. |
| ⌘Z | Undo typing; if there is nothing to undo and a capture happened in the last ten seconds, take that capture back |
| ⌘⇧V | Commit the clipboard as its own entry without touching the draft |
| ⌘← ⌘→ ⌘↑ | Nudge left, right, back to center |
| ⌘O | Open the manage window |
| ⌘A ⌘C ⌘X ⌘V | Standard editing; the app must provide these itself if it has no menu bar |
A second global shortcut (default ⌥⇧V) commits the clipboard as an entry without opening the drawer. The drawer flashes with the text for under half a second, without taking focus, and settles away. An empty clipboard beeps.
5. Manage window
5.1 Layout
A normal, resizable window. One column. Toolbar with a filter field on the left and, on the right, a Sweep button that appears only when stale entries exist (label Sweep · N older than today) and the open count. Below, the list.
Sections in display order: Active (only if any entry is shortlisted), then each day newest first. Entries within a day display newest first. Each section has a small monospaced heading, EEEE, MMM d, with today beside the current day's heading in the accent color, and Active in the accent color.
Row: a bullet column, the text, and on the selected row only, a monospaced hint of the keys that apply. Bullets: • open, ✓ done or struck, > carried, – note, ! shortlisted in the accent color. Done and struck text is muted and struck through. Carried text is dimmed. URLs in text are detected at render time and shown as links in the accent color; the file keeps raw URLs.
Past days hide done and carried entries behind a footer N done hidden by default. A toggle shows them. Today always shows everything. The filter ignores this and searches all entries by case-insensitive substring of the text.
The selected row is highlighted with the surface color. Selection moves with the arrow keys as one continuous list across sections. Selection survives a file rewrite by entry identity, then by position.
5.2 Keys
| Key | Action |
|---|---|
| ↑ ↓ | Move selection |
| Space | Toggle done |
| a | Toggle shortlist |
| → | Carry to today (disabled on today's entries); in the trash view, restore |
| Backspace or Delete | Trash; in the trash view, delete forever |
| Return | Edit in place; if the entry is exactly one URL, open it instead |
| ⌘Return | Open the first URL in the entry |
| Shift+Return while editing | Newline |
| Escape | Cancel edit; else clear the filter; else leave sweep or trash view; else close the window |
| ⌘Z ⌘⇧Z | Undo, redo |
| ⌘F | Focus the filter |
| ⌘E | Show or hide done items on past days |
| ⌘S | Start or end sweep |
| ⌘T | Show or leave the trash view |
| ⌘W ⌘Q ⌘, | Close window, quit, settings |
Clicking a bullet toggles done. Single click on text selects; on a URL-only entry it also opens the URL. Double click edits.
5.3 Sweep view
Replaces the list. A large card with the entry's day and time above the text, the accent color as a thin edge on the left, and a row of key hints beneath: → carry, a shortlist, Space done, Backspace drop, ↓ skip, ↑ back, Return open link. The toolbar shows Sweep and N left.
5.4 Trash view
Same list layout, sections headed Trashed EEEE, MMM d, hints Return restore to today · Backspace delete forever.
5.5 Empty states
- No entries: "Nothing yet." and "Press [shortcut] anywhere." using the user's actual shortcut.
- Filter with no match:
No entries match "term". - Empty trash: "Trash is empty."
6. Settings
Everything the user can change. Nothing else is configurable.
| Setting | Type | Default | Range |
|---|---|---|---|
| Capture shortcut | key recorder | ⌥Space | any |
| Capture clipboard shortcut | key recorder | ⌥⇧V | any |
| Open notes shortcut | key recorder | ⌥⇧Space | any |
| Font | choice | Atkinson Hyperlegible | Atkinson Hyperlegible, OpenDyslexic, System |
| Drawer text size | slider with number field | notes size + 4 | 12 to 48 |
| Notes text size | slider with number field | 18 | 12 to 48 |
| Drawer opacity | slider with number field | 60% | 30% to 95% |
| Blur behind drawer | slider with number field | 0 | 0 to 8 |
| Notes file | path picker | ~/Documents/notr/notr.md | any |
| Launch at login | toggle | off |
Every slider has a number field beside it that shows the live value and accepts typed input. Changes apply immediately, including in an open manage window, with no restart.
Launch at login must be off by default and never pre-registered on the user's behalf. This is a hard rule.
Text sizes apply to entry text only. Headings, hints, the filter, and the drawer's date and count lines stay fixed at 13 and 11 points.
7. Appearance
Dark-first with a real light theme. Follow the system appearance.
| Token | Dark | Light | Used for |
|---|---|---|---|
| ground | #15171B | #F6F4EF | window background, drawer tint |
| surface | #1E2127 | #FFFFFF | selected row, sweep card, inputs |
| ink | #E9E6DF | #1B1D22 | text |
| muted | #8B8F98 | #6B6F78 | headings, hints, done text |
| accent | #2BBF9F | #0E6E64 | caret, selected bullet, shortlist, links, Active heading |
| strike | #5C606A | #A9ACB3 | strikethrough line |
The accent appears in one place per screen. No other color is used.
Fonts, bundled with the app, all under the SIL Open Font License:
- Atkinson Hyperlegible Next, variable weight, for entry text. Google Fonts distributes it.
- Atkinson Hyperlegible Mono for all chrome: dates, counts, hints, key caps.
- OpenDyslexic Regular, Bold, and Italic as the alternate entry face, with line spacing multiplied by 1.35.
The mark is a drawer hanging from the screen edge with one line of text inside, drawn on a 24-unit grid with a 2-unit stroke and 0.75-unit bottom corners. SVG source:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 5.5h20"/>
<path d="M5.5 5.5v12.25a.75.75 0 0 0 .75.75h11.5a.75.75 0 0 0 .75-.75V5.5"/>
<path d="M9.5 12.5h5"/>
</svg>
The app icon is that mark in ink #1B1D22 on the accent #2BBF9F, inset 11% on each side. The system tray or menu bar icon is the mark alone as a monochrome template.
8. App shape
- Runs as a background app with a tray or menu bar icon and no taskbar or Dock presence by default.
- Menu: Capture, Capture Clipboard, Open notr, Sweep, Show Notes File, Show Trash, Settings, Check for Updates, Quit.
- Single instance.
- Keeps the drawer window created and hidden so summoning is under 100 ms from keypress to visible caret. This is the number the whole product depends on.
- Self-updates. The Mac app uses Sparkle with an appcast at
https://notr.site/appcast.xml. Other platforms should use their conventional mechanism and must ask the user before checking automatically.
9. Platform notes
Things known to be hard outside macOS. Solve them honestly rather than approximating badly.
- Global shortcuts on Linux. Under X11,
XGrabKeyworks. Under Wayland there is no portable mechanism; GNOME and KDE each have a portal, and the user may have to bind the shortcut in system settings to a command that signals the running app. Document that step instead of silently failing. - Over full-screen and all desktops. On Windows, a topmost tool window without activation (
WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,HWND_TOPMOST) gets most of the way. On Wayland, compositors decide; a layer-shell surface is the closest match where supported. - Not stealing focus while accepting keyboard input is the hardest requirement. If the platform cannot do it, prefer stealing focus and restoring it precisely on dismiss over a drawer that cannot be typed into.
- Translucency needs a compositor. Fall back to opaque rather than to a broken window.
- Font registration from bundled files is different everywhere. The fonts must be loaded from the app bundle, not assumed installed.
10. Test cases a compatible implementation must pass
- Parsing the example file in section 2.2 and serializing it produces identical text.
- Capturing into an empty file produces
## <today>\n\n- [ ] text <!-- HH:mm -->\n. - Capturing when today's section exists and older sections follow inserts after today's last entry with exactly one blank line before the next heading.
- Capturing when today's section does not exist creates it at the top with one blank line before the previous first heading.
- A multi-line capture writes continuation lines with a two-space indent and reads back with
\njoins. - Two entries with identical text in one section get distinct identities; the same text in two sections gets distinct identities; identity is the same across two parses of the same file.
- [ ] ! text <!-- 09:14 -->parses as open, active, time09:14, no comment, and round-trips.- [>] text <!-- 09:05 carried 2026-09-07 -->parses time09:05and commentcarried 2026-09-07and round-trips.- [>] text <!-- carried 2026-09-07 -->with no time parses and round-trips.- Open count counts only
- [ ]lines, not done, carried, or notes.
11. What not to build
Tags, folders, notebooks, rich text, images, links between notes, collaboration, accounts, a sync service, reminders with times, recurring tasks, AI features, autocorrect, confirmation dialogs. Any of these would put a decision between the user and the thought.