- Published
- min
- 6
Jot: write a thought down in three seconds, from anywhere
A quick-capture notes tool that lives in the macOS menu bar: press ⇧⌘J, type, press Return — the note is saved and the panel is gone. No account, no network, no "which notebook does this go in". Plus why Chinese search needed a different tokenizer, and three keyboard bugs that only showed up once the app was actually driven.
Note apps lose to friction, not to missing features
A thought usually arrives while you are doing something else: reading a PDF, replying to an email, waiting for a build.
Most note apps ask two things of you at that moment: switch over, then decide which notebook, folder or tag this belongs to. Each decision takes two seconds, and together they are just long enough to make "I will write it down later" the easier option. Later usually means never.
Jot does one thing: it takes both decisions out of the path between the thought and the note.
Shortcut, type, Return
Jot lives in the macOS menu bar. Press ⇧⌘J in any app and a floating panel appears high on the screen; type, press Return, and the note is saved and the panel is gone. No app switch, and nobody asks where it should go.
There is a second way in: clicking the menu bar icon opens a popover anchored beneath it. Both routes share the same input and the same draft.
The keyboard is short:
| Key | Does |
|---|---|
| ⇧⌘J | Open the capture panel from any app (configurable) |
| Return | Save and close |
| ⇧Return | New line |
| Esc | Close, keeping the draft |
| ⌘F | Open the review window and start searching |
| ⌘⌫ | Move the note being edited to the trash |
| ⌘Z | Undo — including a deletion |
Neither Escape nor clicking away loses half-written text, and it survives an app restart. There is no confirmation dialog anywhere in the app, because nothing is destroyed without a way back: deletion goes to a trash that empties itself after a window you choose, and ⌘Z takes a deletion back immediately.
A journal, not an inbox
"Deciding where it goes" has not disappeared; it has moved into a second mode you enter far less often.
The review window has two columns. The left one indexes the days that hold notes, newest first; the right one is that day's stream, oldest first, opened at the bottom — where the latest thought sits. The two columns deliberately run in opposite directions: an index puts what you reach for most within reach, and a stream runs downwards.
To categorise, type #work inline. Tags are parsed on write, and clicking one is an exact tag search. Never required, never a dropdown.
Why Chinese search finds nothing
This is where most of the thinking went.
SQLite's FTS5 defaults to the unicode61 tokenizer. It segments English well — on whitespace and punctuation — but it cannot segment Chinese at all. A whole Chinese sentence goes into the index as a single token, so searching for 租約 only matches a note that happens to be exactly that sentence.
Jot uses the trigram tokenizer instead: it indexes every three-character window, which gives real substring search in any script.
The cost is that queries shorter than three characters never match — and two-character Chinese words are probably what gets searched most. So anything under three characters skips the index and falls back to a LIKE scan. At personal-notebook scale the scan is imperceptible, and it is the difference between 租約 finding things and finding nothing.
Queries are also classified. When the entire input is one tag (#work), it becomes an exact tag lookup, so #work does not drag in #workflow; #work meeting is an ordinary text search. Text queries are wrapped as an FTS5 phrase, so punctuation and operators the user typed cannot turn into query syntax — or into a syntax error.
Tag parsing has a few rules of its own, to keep ordinary prose from sprouting accidental tags:
- A
#must start the text or follow whitespace, sohttps://x#anchorandC#are left alone - A tag must contain at least one letter, so "issue #1" stays prose
- "Letter" means any script's letters, so
#工作is a tag
Three bugs you only see by actually opening the app
There are 60 unit tests, covering the repository, tag parsing, query classification, export and localisation — all pure logic that never touches the UI. Actually running the app, and driving it with scripts that press the shortcut, type and hit Return, surfaced three bugs no test would ever have caught:
- Fast typing silently lost characters. A keystroke landing while a SwiftUI re-render was in flight got overwritten by the stale binding value; typing
jot e2e check #testtagsavedez. The fix: the text view only accepts writes that did not originate from itself. - ⌘A, ⌘C, ⌘V, ⌘X and ⌘Z did nothing in the capture field. AppKit dispatches those through the main menu, and an
LSUIElementapp — one that lives only in the menu bar — has none by default. So paste, the single most common way a link reaches a capture tool, was broken. A menu is now installed; it stays invisible but carries the key equivalents. - The panel never became key while another app was frontmost. The
.nonactivatingPanelflag keeps the app in the background, and a background app's panel never becomes key — the keystrokes go on reaching whatever app was already in front. Order matters too: order the panel front first, then activate; the other way round hands focus to whichever window of this app is already frontmost.
A fourth one is not a bug, but it nags on every launch: macOS records where each file came from in com.apple.provenance, and an app carrying provenance from the Documents folder prompts "would like to access files in your Documents folder" every time it starts. The build script strips that attribute before signing.
A few deliberate choices
- The global shortcut uses Carbon's
RegisterEventHotKey, not a globalNSEventmonitor. The monitor requires Accessibility permission — a scary system prompt for a notes app. The Carbon call does not. - The shortcut is chosen from a pre-checked list rather than recorded live. A recorder has to solve conflict detection to be worth having, and that is a bigger job than it looks. The list deliberately leaves out ⌘Space, ⌃Space and ⌥⌘Space, which macOS reserves for itself.
- The database lives in Application Support, never in Documents or any cloud-synced folder. SQLite's write-ahead log and file-level sync engines corrupt each other quietly, and a notebook lost that way is not recoverable. To take a copy out, export: Markdown grouped by day, or JSON with tags resolved.
- No network at all. No account, no sync, no requests of any kind.
- No Xcode required. Swift 6 plus the Command Line Tools is enough: SwiftPM produces the binary, and
build.shwraps it in an.appand ad-hoc signs it. The one trap is that the Command Line Tools shipTesting.frameworkoutside SwiftPM's search paths, soswift testreports a missing module andtest.shhas to point the compiler and linker at it.
What it still cannot do
- No link titles, images or attachments. The schema and migrations already have room for them.
- No signing, notarisation or auto-update. So build it yourself — a locally built app carries no quarantine flag and Gatekeeper leaves it alone, while a downloaded one will not be.
- No sync. The export exists so this is not a trap.
- No automated UI tests. All three bugs above were found by driving the app by hand;
Scripts/holds a CGEvent clicker, because synthetic clicks miss SwiftUI views.
v1. I use it every day; it is not yet packaged for anyone else.
Take it
https://github.com/Grayidea-bit/Jot (MIT)
git clone git@github.com:Grayidea-bit/Jot.git
cd Jot
./build.sh
cp -R build/Jot.app /Applications/
open /Applications/Jot.app
macOS 14 or later. The first launch opens the review window with an empty state, because everything else about the app is behind a shortcut and a menu bar icon you have not seen yet.