- Swift 92.8%
- HTML 6.3%
- Shell 0.9%
review.md and response.md were process history; git log keeps them.
MainMenu now names its actions through an unadopted @objc protocol, which
clears the nine Selector("…") warnings without changing any selector.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|---|---|---|
| homarr-widget | ||
| Icons | ||
| Sources/MemoBar | ||
| Tests/MemoBarTests | ||
| .gitignore | ||
| build.sh | ||
| Info.plist | ||
| LICENSE | ||
| Package.swift | ||
| README.md | ||
Memobar
A macOS menu bar client for Memos. Write a note from anywhere with a keystroke, search and edit what you already wrote, and never open the web UI for a one-line thought again.
Native Swift and SwiftUI. No Xcode project, no package dependencies, one
build.sh.
Requirements
macOS 14 or later, the Xcode command line tools (xcode-select --install), and
a Memos server you can reach. Tested against the v1 REST API; see
API compatibility for how older servers are handled.
Build and install
git clone <this-repo> memobar
cd memobar
./build.sh
cp -R MemoBar.app /Applications/ && open /Applications/MemoBar.app
Roughly two minutes with the command line tools already installed. build.sh
compiles a release binary, assembles the .app bundle around it, and ad-hoc
signs it.
Setup
The first launch opens Settings. Two fields:
Server URL is the base URL with no trailing path, for example
https://memos.example.com. Plain http:// works, which matters because a
self-hosted instance is often at something like http://192.168.1.50:5230.
Prefer https:// where you can, since the access token is a bearer credential
sent on every request.
Access token comes from Memos under Settings, My Account, Access Tokens. It is stored in your login keychain, not in a plist or a dotfile.
Test Connection confirms both before you save. Launch at login is a checkbox
there too, wired through SMAppService.
Using it
Click the menu bar icon, or press ⌃⌥M from any application, and the compose
panel appears. Type, then ⌘↩ to save and close. Markdown and #tags work
much as they do in Memos, and typing # offers the tags you already use. The
tag reader here is MemoBar's own and is deliberately conservative: a tag has to
start at a whitespace boundary and end at one, so no#tag and #tag+more are
read as ordinary text although the server would extract a tag from them.
Attach files by dropping them anywhere on the panel, through the paperclip
button, or by pasting an image from the clipboard via the ··· menu. Files go
up to 32 MB each.
Below the compose box are a search field and your memos. The list pages in fifty at a time as you scroll, with a Load more button as a backstop. Drag the handle at the very bottom edge to resize the panel; the height is remembered and capped so the panel always fits under the menu bar on whatever screen you are on.
Click a memo to load it into the compose box and edit it in place. The button
becomes Update, the border turns orange, and esc cancels and restores whatever
you were writing before. Right click a memo for pin, archive, copy, and open in
browser.
··· then Open in Window moves the panel into an ordinary resizable window that
stays where you put it. Dragging the popover away from the menu bar does the
same thing. While that window is open the menu bar icon raises and hides it
instead of opening the popover.
Flashcards
Any memo tagged #card with a line of --- between question and answer is a
flashcard. Nothing else about it is special: it stays searchable, editable, and
visible in the list like any other note.
MemoBar switches off macOS's smart substitutions, which would otherwise rewrite
--- as an em dash and break the separator as you typed it. The parser also
accepts the dash forms in case a card was written elsewhere, or before this
setting existed.
#card #gametheory
What does a subgame perfect equilibrium rule out?
---
Non-credible threats. Every subgame must itself be at equilibrium,
so a threat you would not carry out cannot support the outcome.
⌘⇧R opens review, and the menu bar icon carries a count when cards are due.
Space reveals the answer, then 1 to 4 grade it: Again, Hard, Good, Easy.
Each button shows the interval it would schedule.
Scheduling is a simplified SM-2 variant, not SM-2 proper and not Anki's
scheduler. It keeps SM-2's 1 and 6 day opening and its interval × ease growth,
with Anki's four buttons in place of the original six-point quality scale, but
it has no separate learning or relearning queue and no overdue adjustment.
Intervals are capped at a hundred years. That cap exists so the scheduler can
never write a state its own loader would reject, and it is reachable in ordinary
use: eight consecutive Easy answers on one card reach it, after which Hard, Good,
and Easy all schedule the same hundred years.
Each card carries an interval in days, an ease factor
starting at 2.5, and a run of correct answers. Good multiplies the interval by
the ease, Easy takes the Good interval and stretches it, Hard grows the
interval but by less than Good would, and Again resets to one day, drops the
ease, and re-queues the card so it comes back before the session ends. Hard and
Easy are both derived from the Good interval, which is what keeps
hard ≤ good ≤ easy true in every state; Again ignores it and assigns one day.
Decks are just tags. Every tag on a card other than #card becomes a deck in
the picker, and nested tags roll up, so choosing econ reviews econ/auctions
and econ/matching together. A card with several tags appears under each.
The Due and All toggle switches between reviewing what is scheduled and simply browsing a deck with the arrow keys. Browsing writes nothing.
Cards are ordinary memos, so by default they appear in the note list alongside
everything else. Once a deck grows this buries the notes, so the ··· menu has
a Hide cards from the list toggle. It affects only what MemoBar shows: the cards
remain on the server, remain searchable there, and remain reviewable. Hiding is
decided by parsing, not by the tag alone, so a note that merely mentions #card
is not hidden.
Scheduling state lives in one private memo tagged #srs-state holding JSON
keyed by memo UID. Memos gives clients nowhere to store data but will store
text, so this is a scratch file, not a note. It means the schedule follows you
to any client that can reach the server, with no sidecar service and no local
database. A write is attempted after every grade rather than at the end of a
session, because the popover is transient and clicking away should not cost you
the work. That is best effort, not a guarantee: grades live in memory until the
write succeeds, so a grade whose write is still unresolved when you quit is
lost. MemoBar hides it from the note list and from tag autocomplete, though
other Memos clients will show it as an ordinary private memo.
Three limits worth knowing. Memos caps memo content, 8 KiB by default on older builds, and the schedule is one memo, so it has a ceiling; a warning reports the actual size in KB as it approaches. Entries are never pruned automatically, because absence from a fetch is not proof a card was deleted and guessing wrong would destroy history — which means archiving cards does not shrink the schedule.
MemoBar is a single-writer client: run one instance against a schedule, not two. This is an operating requirement rather than advice, because nothing in the app enforces it. Writes replace the whole map with no revision check, and a running instance does not revalidate, so a second machine can lose grades even when the two write hours apart rather than simultaneously. Worse, two instances that both start with no schedule memo will each create one, after which loading fails until you delete one by hand.
If a write that would have created the schedule memo ends without a clear answer — no id in the response, a dropped connection, a timeout, a gateway error — MemoBar stops writing rather than risk creating a second schedule, and says so in review. Reload once the server settles: finding the memo clears the block automatically. A note that the create is in flight is written to disk before the request goes out and removed only when the outcome is known, so a crash, a force quit, or a server switch in the middle of one leaves the block in place on the next launch rather than losing the fact that it happened. The note is scoped to the server and token it was written for; changing either one, by rotating a token for instance, starts a fresh record and abandons the old one.
If repeated reloads find no schedule memo at all, and enough time has passed that a memo which did commit should have appeared, "Create anyway" offers to take the block off. It asks for confirmation first, because from this side a write that committed invisibly and one that never arrived look identical, and the wrong guess leaves two schedule memos to sort out by hand. Grades held in memory at that point are not persisted and are lost on quit.
If the schedule memo ever becomes unreadable, review refuses to start rather than overwriting it.
Keyboard
| Shortcut | Action |
|---|---|
⌃⌥M |
Open the compose panel from any app |
⌘↩ |
Save the new memo, or update the one being edited |
⌘F |
Focus the search field |
↑ ↓ |
Move the list selection (when the compose box is not focused) |
↩ |
Edit the selected memo |
⌘⌫ |
Archive the selected memo |
esc |
Cancel an edit, or leave review |
⌘⇧R |
Open flashcard review (within MemoBar, not a global hotkey) |
space |
Show the answer |
1 – 4 |
Again, Hard, Good, Easy |
← → |
Previous and next card, when browsing |
The list shortcuts are deliberately inert while the compose box has focus, so
⌘⌫ still means delete-to-start-of-line where you are typing.
What survives a restart
The draft is written on every keystroke, so closing the popover or quitting
mid-note loses nothing. Staged attachments survive a closed popover. The token
lives in the keychain; the server URL, default visibility, panel height, and the
API dialects the app has learned live in UserDefaults.
API compatibility
Memos has renamed things across releases, and rather than pinning a version MemoBar works out what it is talking to and remembers the answer. Everything targets the v1 REST surface.
Visibility enum. Both PRIVATE and VISIBILITY_PRIVATE have shipped. The
spelling is read off the list response, and if a save still returns 400 the app
flips it and retries once.
Attachments. /api/v1/resources became /api/v1/attachments in v0.25.
Uploads try one and fall back to the other on a 404, and the memo payload then
uses the matching field name. If a server refuses attachments inline on create,
the memo is created first and the files are bound with SetMemoAttachments.
Search. The list endpoint takes a CEL filter whose grammar was rewritten
twice between v0.22 and v0.30. Only content.contains("…") is used, stable
since v0.23. A 400 turns server-side search off and the box filters loaded
memos locally instead, labelling itself "local" so the difference is visible.
Changing the server or the token resets that, since a different server may
accept it.
Archive. state replaced row_status in v0.23, so archiving tries the
first and falls back to the second.
Tags. Three shapes across versions. The current user is resolved through
/auth/me, then /auth/sessions/current, then /auth/status; tags come from
users/{u}:getStats, then users/{u}/stats, then the old memos/-/tags. If
all of them fail, autocomplete stays quiet.
Notes for hacking on it
Sources/MemoBar is fourteen files. AppState holds everything and is the only
ObservableObject, with the flashcard half of it in ReviewSession as an
extension; MemosAPI and Attachments are the network layer; SRS is the
scheduler and the state-memo codec, and is the one file with no UIKit, AppKit,
or SwiftUI dependency; ComposeView, SettingsView, and ReviewView are the
whole UI.
The global hotkey uses Carbon's RegisterEventHotKey, which deliberately avoids
needing Accessibility permission. To rebind it, change the defaults in
HotKey.register: keyCode is a kVK_* constant and modifiers an OR of
controlKey, optionKey, cmdKey, shiftKey.
Two things that are easy to break. The popover's height is set explicitly and
the memo list is the only element allowed to flex, which is what stops the panel
from growing past the screen; NSHostingController.sizingOptions must stay
empty or its Auto Layout constraints override that. And the Edit menu in
MainMenu.swift looks pointless because an accessory app never displays a menu
bar, but without it ⌘V has nothing to match against and pasting only works
from the right-click menu.
Known limitations
Attachments can only be added to a new memo, not to an edit, because the app does not track a memo's existing files and a partial update could replace them. Archiving has no undo. The bundle is ad-hoc signed, so macOS may ask once for keychain access, and again after each rebuild since the signature changes.
License
MIT. See LICENSE.