notrspec.md for your agent

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.

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:

2.3 Entry grammar

After - , in this order:

PieceFormMeaning
Marker[ ] , [x] , [>] , or noneopen 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.
Textfirst line of the entrywrapped in ~~ when the entry is done or struck
Metadatatwo 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:

StateMarkerText wrapExample
open[ ] none- [ ] text
done[x] ~~text~~- [x] ~~text~~
carried[>] none- [>] text <!-- carried 2026-09-12 -->
notenonenone- text
strucknone~~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

FromTo
open or carrieddone, and clear the active flag
doneopen
notestruck, and clear the active flag
strucknote

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

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

KeyAction
ReturnCommit and close
⌘Return (Ctrl+Return elsewhere)Commit and keep the drawer open, cleared, for the next thought
Shift+ReturnNewline inside the entry
EscapeClose and keep the draft. The draft is persisted and restored on next show, including across relaunch.
⌘ZUndo typing; if there is nothing to undo and a capture happened in the last ten seconds, take that capture back
⌘⇧VCommit the clipboard as its own entry without touching the draft
⌘← ⌘→ ⌘↑Nudge left, right, back to center
⌘OOpen the manage window
⌘A ⌘C ⌘X ⌘VStandard 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

KeyAction
↑ ↓Move selection
SpaceToggle done
aToggle shortlist
Carry to today (disabled on today's entries); in the trash view, restore
Backspace or DeleteTrash; in the trash view, delete forever
ReturnEdit in place; if the entry is exactly one URL, open it instead
⌘ReturnOpen the first URL in the entry
Shift+Return while editingNewline
EscapeCancel edit; else clear the filter; else leave sweep or trash view; else close the window
⌘Z ⌘⇧ZUndo, redo
⌘FFocus the filter
⌘EShow or hide done items on past days
⌘SStart or end sweep
⌘TShow 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

6. Settings

Everything the user can change. Nothing else is configurable.

SettingTypeDefaultRange
Capture shortcutkey recorder⌥Spaceany
Capture clipboard shortcutkey recorder⌥⇧Vany
Open notes shortcutkey recorder⌥⇧Spaceany
FontchoiceAtkinson HyperlegibleAtkinson Hyperlegible, OpenDyslexic, System
Drawer text sizeslider with number fieldnotes size + 412 to 48
Notes text sizeslider with number field1812 to 48
Drawer opacityslider with number field60%30% to 95%
Blur behind drawerslider with number field00 to 8
Notes filepath picker~/Documents/notr/notr.mdany
Launch at logintoggleoff

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.

TokenDarkLightUsed for
ground#15171B#F6F4EFwindow background, drawer tint
surface#1E2127#FFFFFFselected row, sweep card, inputs
ink#E9E6DF#1B1D22text
muted#8B8F98#6B6F78headings, hints, done text
accent#2BBF9F#0E6E64caret, selected bullet, shortlist, links, Active heading
strike#5C606A#A9ACB3strikethrough 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:

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

9. Platform notes

Things known to be hard outside macOS. Solve them honestly rather than approximating badly.

10. Test cases a compatible implementation must pass

  1. Parsing the example file in section 2.2 and serializing it produces identical text.
  2. Capturing into an empty file produces ## <today>\n\n- [ ] text <!-- HH:mm -->\n.
  3. 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.
  4. Capturing when today's section does not exist creates it at the top with one blank line before the previous first heading.
  5. A multi-line capture writes continuation lines with a two-space indent and reads back with \n joins.
  6. 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.
  7. - [ ] ! text <!-- 09:14 --> parses as open, active, time 09:14, no comment, and round-trips.
  8. - [>] text <!-- 09:05 carried 2026-09-07 --> parses time 09:05 and comment carried 2026-09-07 and round-trips.
  9. - [>] text <!-- carried 2026-09-07 --> with no time parses and round-trips.
  10. 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.