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.2.3; 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. notr writes two kinds of clause there, carried yyyy-MM-dd and the reminder clauses in §2.6; anything else must be preserved as it is. 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.

2.6 Reminders

A reminder lives in the entry's comment as plain words:

- [ ] call the dentist  <!-- 09:14 remind 2026-10-26 08:30 -->
- [ ] send Dana the invoice  <!-- 11:02 remind 2026-10-24 14:02 every 3h 08:30-20:30 -->
- [ ] evening meds  <!-- 11:40 remind 2026-10-24 20:30 every day 20:30 routine -->
ClauseFormMeaning
remindremind yyyy-MM-dd HH:mmthe earliest ring nobody has answered, in local wall-clock time
everyevery <rule>the schedule after that ring. Absent means the reminder rings once.
routineroutinewith every, Done logs a completion and the schedule continues (§3.9). Without it, Done ends the reminder.

Rules, as notr writes them:

RuleMeans
every 3h 08:30-20:30every 3 hours inside the window. Units are h and m (every 90m …). The window is always written out.
every day 08:30daily at that time
every day 08:30 20:30daily at each listed time
every weekday 09:00Monday to Friday
every mon thu 18:00the listed days, by three-letter English name

Rings happen at remind, then at each later time the rule produces. For an interval, each ring is the previous ring plus the interval, and a ring that would fall outside the window moves to the window's next start: every 3h 08:30-20:30 from 13:47 rings at 16:47 and 19:47, then 08:30 the next day, then 11:30.

A ring that goes unanswered writes nothing to the file, so every device that reads the same file computes the same rings, and the number of unanswered rings is the number of ring times between remind and now. The file changes only when the user answers (§3.9).

When reading, accept the clauses in any order and keep the comment's original text. When writing, re-serialize the comment only if the entry's reminder changed, and then write the comment's other words (such as carried yyyy-MM-dd) in their original order, followed by remind, every, and routine. A reminder on a done, struck, or carried entry is kept in the file but never rings.

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

Marking an entry done or struck removes its reminder clauses. On a routine, toggling done is the routine's Done answer instead (§3.9).

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. A reminder moves with the entry: the new copy gets the original's reminder clauses, and the original's comment becomes just carried yyyy-MM-dd.

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 without a reminder 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, remind with a preset (which takes it out of the sweep), 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."

3.9 Reminders

Setting a reminder writes the clauses of §2.6, replacing any reminder the entry had. Setting one on a done or struck entry reopens it. Carried originals can't be reminded.

A ringing reminder is answered in one of four ways. Dismiss is about the ring; Done is about the task.

AnswerRings onceRepeats until doneRoutine
DoneMark done (a note becomes struck), clear the active flag, remove the clausesSameAppend - [x] ~~text~~ <!-- HH:mm --> to today, and move remind past the ring this answers
DismissRemove the clauses; the entry returns to its day, still openSet remind to the next ring after now in the sequenceSame, and nothing is logged
SnoozeSet remind to the chosen timeSet remind to the chosen time and keep the ruleSame
StopRemove the clausesSameSame

Done on a routine that isn't ringing yet counts for the upcoming ring, and moves remind past it. Every answer is one undo step.

The open count leaves out routines, which stay open for good.

The when field and the presets read typed times forgivingly: 20m, 1h30m, in 2 hours, 3d, tonight, tomorrow, tomorrow 2pm, weekend, next week, fri, fri 3pm, next sat, 16:00, 9am, nov 2 9am, 2026-11-02 14:00, every 3h, every 90 minutes 9-17, every day, daily 7:15, twice a day, every weekday 9am, every mon thu 18:00, and routine anywhere in a repeat. tonight is today's day end (tomorrow's once that has passed), tomorrow, weekend, and next week are the day start of that day, and twice a day is day start and day end. A time already past today means tomorrow; a date already past this year means next year. The file always gets the canonical form of §2.6.

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. At the bottom, a one-line status bar in small monospaced muted type shows the keys that apply to the selected row, or while editing, ↩ save · ⇧↩ new line · esc cancel.

Sections in display order: Now (only while a reminder is ringing), 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 Now and Active in the accent color.

Now holds every entry whose reminder has rung and not been answered, oldest ring first. An entry waiting on a future ring leaves its day, the way a shortlisted entry does, and appears in the Later view (§5.6) until it rings; a shortlisted entry with a reminder stays in Active. The toolbar shows N later ⌘L while anything is waiting.

Row: a bullet column and the text. A row's layout never depends on whether it is selected, so moving the selection never re-wraps text or shifts the rows below. 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. An entry with a reminder shows it beside the text in small monospaced muted type: rang 14:02 · every 3h · 3rd ring while ringing, tomorrow 08:30 · every day while waiting. 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; on a routine, done this time
aToggle shortlist
1 – 9Remind with that preset. On a ringing entry with a schedule, a one-off preset snoozes it and keeps the schedule.
rOpen the when field
dDismiss a ringing reminder: skip this ring of a repeat, or stop a one-off
→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
⌘LShow or leave the Later view
⌘CCopy the selected entry's text
⌘W ⌘Q ⌘,Close window, quit, settings

Clicking a bullet toggles done. A click anywhere on a row selects it at once and moves keyboard focus to the list; on a URL-only entry, a single click also opens the URL. Double click edits. Clicking a row never scrolls the list; keyboard moves scroll only as far as needed to keep the selection in view. Row text is not selectable. In the filter, ↓ or Return moves focus to the list. Ending an edit any way except Escape, such as clicking another row, saves it.

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, 1–9 later, ↓ 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

5.6 Later view

Same list layout. Every entry waiting on a future ring, grouped under the day it rings, soonest first, each showing its reminder. The list keys work here, except carry. Escape or ⌘L returns to the log.

5.7 When field

A small popover anchored to the selected row, with keyboard focus in its text field the moment it opens: a text field, a live preview of what was typed (→ today 16:47 · then every 3h, 08:30–20:30, until done, or ? when it can't be read), and the nine presets with what each resolves to right now. Return sets what was typed, the preset a lone digit 1–9 names, or the highlighted preset when nothing was typed. ↑ ↓ move the highlight and clear anything typed, so presets can be chosen without typing at all, Tab adds or removes routine, Backspace in an empty field stops an existing reminder, and Escape closes it. Nothing is set until Return or a click on a preset.

5.8 First reminder

The first time a reminder is set, one line appears above the list: "How should reminders reach you?" with two choices, Mac notifications and just the menu bar dot. Only choosing notifications asks the operating system for permission. If launch at login is off, the next line says once that reminders ring only while notr is running and offers to open it at login. Neither is ever turned on without that click.

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
Mac notificationsbuttonoff until chosen (§5.8)
Ring on this Mactoggleonkept per device, not in the notes file
Day startstime08:30
Day endstime20:30
Presets 1 – 9text, in the when field's words20m 1h 3h tonight tomorrow next week every 3h every day twice a day

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. Notifications follow the same rule: no permission prompt until the user chooses notifications. These are hard rules.

The Mac app groups settings into four tabs: General (notes file, launch at login), Capture (shortcuts, drawer opacity and blur), Text (font and sizes), and Reminders.

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 open tasks, not done, carried, notes, or routines.
  11. - [ ] text <!-- 11:02 remind 2026-10-24 14:02 every 3h 08:30-20:30 --> parses time 11:02, a reminder at 2026-10-24 14:02 repeating every 180 minutes between 08:30 and 20:30, and round-trips byte for byte.
  12. A hand-written comment such as 09:00 note remind 2026-10-26 08:30 round-trips unchanged until its reminder changes.
  13. every 3h 08:30-20:30 starting at 16:47 rings at 19:47, then 08:30 the next day, then 11:30.
  14. Dismiss on that reminder at 13:47 when remind is 12:00 sets remind to 15:00. Done removes the clauses and marks the entry done.
  15. Done on remind 2026-10-24 08:30 every day 08:30 20:30 routine at 13:47 that day logs - [x] ~~text~~ <!-- 13:47 --> under 2026-10-24, keeps the entry open, and sets remind to 2026-10-24 20:30.
  16. every day 08:30 across the end of daylight saving time still rings at 08:30 local time.

11. What not to build

Tags, folders, notebooks, rich text, images, links between notes, collaboration, accounts, a sync service, due dates, deadlines, overdue states, priorities, streaks, parsing times out of a note's own text, AI features, autocorrect, confirmation dialogs. Any of these would put a decision between the user and the thought. Reminders are set after capture, never during it.