Native macOS menu bar client for Memos. Capture a note from anywhere with ⌃⌥M, search and edit without opening the web UI.
  • Swift 92.8%
  • HTML 6.3%
  • Shell 0.9%
Find a file
bogac 7daf03b8e3 Remove review rounds from the tree, name menu actions with #selector
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>
2026-10-24 22:01:25 +11:00
homarr-widget Homarr widget: tailnet-only via tailscale serve, hide cards and state memo, opaque background 2026-10-23 18:12:58 +11:00
Icons App icon and menu bar glyph, hide-cards toggle, smart dashes off 2026-10-23 18:12:58 +11:00
Sources/MemoBar Remove review rounds from the tree, name menu actions with #selector 2026-10-24 22:01:25 +11:00
Tests/MemoBarTests App icon and menu bar glyph, hide-cards toggle, smart dashes off 2026-10-23 18:12:58 +11:00
.gitignore Header updated 2026-08-14 16:16:56 +10:00
build.sh App icon and menu bar glyph, hide-cards toggle, smart dashes off 2026-10-23 18:12:58 +11:00
Info.plist App icon and menu bar glyph, hide-cards toggle, smart dashes off 2026-10-23 18:12:58 +11:00
LICENSE MemoBar: menu bar client for Memos 2026-08-14 15:59:10 +10:00
Package.swift Flashcards: SM-2 review mode, state memo persistence, four rounds of review fixes 2026-08-20 02:54:23 +10:00
README.md App icon and menu bar glyph, hide-cards toggle, smart dashes off 2026-10-23 18:12:58 +11:00

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.

License: MIT

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.