A window switcher for macOS, in the spirit of AltTab. It lists every open window — not every app — and switches to the one you pick, either by stepping through the list or by typing a few letters of the app or title you are after.
The single design goal is that pressing the shortcut shows the list instantly. Most of what follows is a consequence of that.
curl -L https://github.com/andrecarlucci/mrtab/releases/latest/download/MrTab.tar.gz | tar -xz -C /Applications
open /Applications/MrTab.appMrTab opens its Settings window when you launch it, which is also how it reports whether it can work at all:
- The banner at the top will be amber, saying Accessibility has not been granted.
- Click Open System Settings, then add and enable MrTab under Privacy & Security → Accessibility.
- The banner turns green on its own — no restart, no reopening. The shortcut is live from that moment.
Then hold ⌥ and tap Tab.
Universal (Apple Silicon and Intel), macOS 13 or later. MrTab lives in the menu bar and has no Dock icon.
Unpack with
taras above rather than double-clicking a downloaded archive. MrTab is ad-hoc signed rather than notarised, and Archive Utility stamps whatever it extracts withcom.apple.quarantine, which Gatekeeper then blocks — reporting it as "damaged and can't be opened", which means unsigned, not corrupt.tardoes not propagate the flag.
If you already have a blocked copy — it arrived by AirDrop, or you double-clicked the archive — there are two ways out. Neither requires re-downloading anything.
Clear the flag from a terminal. One command, and the app opens normally afterwards:
xattr -dr com.apple.quarantine /Applications/MrTab.appOr use Gatekeeper's own override, if you would rather not touch a terminal. On macOS 15 and later:
- Double-click MrTab so it gets blocked. This step is required — the override only appears after a refused launch, and it expires roughly an hour later.
- Open System Settings → Privacy & Security and scroll down to the Security section.
- You will see "MrTab was blocked to protect your Mac." Click Open Anyway and authenticate.
- Open MrTab again. Confirm Open in the dialog that follows.
On macOS 13 and 14 you can instead Control-click MrTab in Finder, choose Open, then Open again in the dialog. That shortcut was removed in macOS 15, so on newer systems use the Settings route above.
Two things worth knowing either way. The override applies to that exact copy — replace MrTab with
a newer download and you do it again, which is why the curl … | tar install is the quieter path.
And this only settles Gatekeeper: Accessibility is a separate permission and still has to be
granted as described above.
MrTab has no way to enumerate windows or raise one without it, and the failure is silent: the shortcut simply does nothing. If the switcher ever stops responding, open Settings and read the banner before assuming anything else is wrong.
⌥ Tab open the switcher, select the previous window
⌥ held tap Tab to move down the list, tap ⇧ to move back up
the arrow keys work too
⌥ release switch to the highlighted window
a-z, 0-9 filter the list down to what you type
⌫ delete a character of the filter
Esc clear the filter, or cancel and stay where you are
Return switch immediately
⌘ W close the highlighted window
⌘ R name the highlighted window
⌥ 1-9 switch to a numbered window, from anywhere, without opening the list
Both are whatever you set them to; ⌥ Tab and ⌥ 1-9 are only the defaults.
A second shortcut can open the list already pinned open, as though you had typed the first character of a search: the list stays up when the modifiers come up, and Return or a click is what switches. It has no default — set it under Open pinned in Settings. Reach for it when you know you are going to search, rename or close a few windows rather than tab straight through. Pressing it while the list is already up pins a panel you were holding, which is how you let go of ⌥ without switching.
⌘ Tab is left alone — the macOS app switcher keeps working as it always did.
The panel has a header carrying the app icon, its name, and a gear on the far right. Clicking the gear dismisses the switcher and opens Settings.
Start typing and the list narrows to the windows whose app name or title matches, with the header
showing what you have typed so far. Words match in any order, so sl gen finds Slack's #general
and code mrtab picks one editor window out of six. Case and accents are ignored: sessao finds
Sessão.
The first keystroke pins the panel open. Letting go of ⌥ stops meaning switch now — searching is a two-handed, unhurried act, and holding a modifier through it is not. Move with Tab or the arrows, then Return or a click to switch. Esc clears the filter and leaves the switcher up; a second Esc cancels, as does clicking into another app. Nothing changes for the plain tap-and-release flow: it only ever becomes a search if you type. The second shortcut above is the same state entered deliberately, before the first keystroke rather than with it.
Closing a window takes ⌘ W rather than a bare W for the same reason — W is the first letter of far too many app names to spend on a shortcut.
Moving the pointer over a row highlights it and clicking switches to it. The row under the pointer also grows an ✕ at its right edge; clicking that closes that window and leaves the switcher up, so you can shut several in a row without reopening it. It is the same thing ⌘W does to the highlighted row, and it appears only under the pointer — an ✕ on all twelve rows at once is noise, and the space it needs is reserved on every row regardless, so nothing reflows as you move.
Two more buttons appear under the pointer for the same reason, to the left of the ✕: a dashed ring, and a tag.
Clicking the ring gives that window a number, drawn in its place as a filled badge. The first window you click gets 1, the next 2, and so on up to 9. Clicking a numbered badge takes the number away again, and the next window you number takes the lowest one going spare — so the numbers close up behind you rather than creeping upwards as you change your mind.
A number is worth having because of what it buys outside the switcher: ⌥ with a digit goes straight to that window from anywhere, with the list never appearing at all. The terminal you keep coming back to is one chord away, in one motion, whatever is in front of you — no list to open, nothing to read, nothing to aim at. Inside the switcher digits are ordinary characters again and filter like any other key.
⌥ is the default because it pairs with ⌥ Tab and is the easiest chord to hit without moving
your hand. It does cost you the characters ⌥ 1-9 used to type — ¡™£¢∞§¶•ª — so if you want
those, ⌃⌥ is the obvious second choice: ⌃ 1-9 alone is how Mission Control switches Spaces, but
the pair is free. Change it in Settings, or switch it off there entirely.
Numbers stick to the window, not to its position in the list, so a window keeps its badge as it drifts down the most-recently-used order. Jumping reaches a numbered window even when the current settings would keep it out of the list — minimized, hidden, on another Space — which is rather the point of having numbered it. Closing the window frees its number for the next one. See What a note is pinned to for how long one lasts.
Unlike typing, numbering does not pin the panel open: like the ✕, it is something you do with the switcher held, so keep the modifier down while you click.
A number or a name is pinned to the window-server id, which is macOS's own name for a window. It is worth knowing what that does and does not survive, because the obvious alternatives survive much less.
A name or number lasts until its window closes, and survives MrTab quitting, crashing, or being
replaced by a new build — notes are written to ~/.config/mrtab/windows.json and read back on
launch. What it does not survive is closing the window, or a reboot: ids start again from low
numbers afterwards, so MrTab records the boot they were taken in and discards the file if it was
an earlier one. Each note also carries the app that owned the window, and is refused on load if
the id has since been handed to a different app.
Titles were the tempting key and are the worst one available. Measuring an ordinary desktop for 40 seconds found 30 title changes and not one change of window identity — a terminal running a coding agent rewrites its own title about twice a second. A note keyed on a title would come loose within seconds; keyed on the id, it does not notice.
Liveness is asked of the window server rather than of the apps, which matters more than it sounds. Reading a window list out of an app goes through that app's main thread and comes back empty when the app is busy, so a note pruned on that basis would vanish for windows that were merely slow to answer. The window server always knows, and cannot stall. If a jump finds its window missing from the list, MrTab asks the window server who owns it and goes to it anyway rather than doing nothing.
The tag opens a field where the row's title is, and whatever you type there becomes what MrTab calls that window. ⌘ R does the same to the highlighted row without reaching for the mouse. Return keeps the name, Esc abandons it, and an empty name gives the window its real title back.
A name does not replace the title so much as lead it: the name takes the heavier weight and the real title falls in behind, because a row that hid what it actually was would be worse than one with an unhelpful title. Filtering matches both, so a window you have called deploy notes is still reachable by typing the name its app gave it, months after you stopped reading that name.
This is MrTab's name for the window, not the window's name. A title belongs to the app that drew it — macOS exposes it read-only through Accessibility, and the rare app that allows a write overwrites it on the next redraw — so nothing outside the switcher changes.
While a name is being typed the panel belongs to it. Stepping the list, closing a row and switching are all held off until Return or Esc, and clicking another row keeps the name rather than following the click — one click, one thing.
Reachable three ways: the gear in the panel header, Settings… (⌘,) on the menu bar item, or simply opening MrTab again from Applications or the Dock.
It covers the shortcuts, which windows get listed, the panel's proportions, and whether MrTab opens at login. There is no OK or Cancel — changes apply the moment you make them and are written straight to disk, which suits a utility whose whole surface is a handful of toggles.
Changing the shortcut. Click the shortcut field and press the combination you want. It has to include ⌘, ⌥ or ⌃: the modifier is not decoration, it is what holds the switcher open while you browse. Shift alone will not do, because Shift already means "step backwards". Esc leaves the field unchanged.
Open pinned is the optional second shortcut, and is recorded the same way. What it opens is the ordinary switcher, already pinned: nothing has to be held, so it suits searching, renaming and closing rather than tabbing through. Delete switches it off, which is also where it starts.
Jump to number is the chord that reaches a numbered window directly. Record it by pressing the whole thing — the modifiers and a digit — so a chord another app has already taken announces itself there rather than by silently doing nothing later. Only the modifiers are kept; the digit you happened to press is not part of the setting. Delete switches jumping off, which the switcher's own shortcut has no equivalent of, since without that one there is no way in at all.
Show on decides which display the switcher opens on when you have more than one. Screen with the pointer is the default and follows your mouse; Main screen pins it to the display carrying the menu bar, so it always appears in the same place no matter where the pointer wandered off to. On a single display the two are the same thing.
Open at Login is also a toggle on the menu bar item. It uses SMAppService, which registers
the bundle at its current path — move MrTab and you need to switch it off and on again.
Settings appears when you open MrTab, but not when macOS launches it at login. macOS offers no
dependable way to tell those apart — XPC_SERVICE_NAME is set for an ordinary Finder launch too —
so MrTab stays quiet only when it is both registered as a login item and the machine has just
come up. Logging out and back in without rebooting will show Settings once.
Settings writes ~/.config/mrtab/config.json. You can edit it directly instead, in which case
restart MrTab to apply. Window names and numbers live beside it in windows.json, which MrTab
writes as you make them; it is keyed on window-server ids and not meant to be edited by hand.
| Key | Default | Meaning |
|---|---|---|
shortcut |
{"keyCode": 48, "modifiers": ["option"]} |
Virtual key code plus modifier names |
pinnedShortcut |
absent | A second shortcut that opens the list pinned open; leave the key out for none |
jump |
{"modifiers": ["option"]} |
Modifiers that pair with 1-9; [] switches jumping off |
includeMinimized |
true |
List minimized windows |
includeHidden |
true |
List windows of hidden apps |
showAllSpaces |
true |
When false, only windows on the current Space |
panelScreen |
"active" |
"active" follows the pointer, "main" pins to the menu-bar display |
panelWidth |
620 |
Panel width in points |
rowHeight |
36 |
Row height in points |
maxVisibleRows |
12 |
Rows shown before the list scrolls |
fullRefreshInterval |
15 |
Seconds between safety-net rescans |
Key codes are positional rather than characters, which is why the shortcut field is the easier way
to set this. The older {"modifier": "option", "key": "tab"} form is still read, and is rewritten
to the structured form the first time settings are saved.
Choosing ⌘ Tab makes MrTab compete with the system switcher rather than replace it — macOS wins
that fight. Replacing ⌘ Tab outright needs a CGEventTap and Input Monitoring permission, which
is not implemented here.
There is no work on the hotkey path. When you press the shortcut, MrTab reads an array that is
already sitting in memory and calls orderFront on a window that already exists.
- The window list is built ahead of time. Enumerating windows means Accessibility IPC to every running app, which is far too slow to do on demand. A background queue keeps a live list and republishes an immutable snapshot to the main thread. The hotkey handler only ever reads that snapshot.
- The list is maintained by observers, not polling.
AXObservernotifications (window created, destroyed, minimized, retitled, focus changed) plusNSWorkspaceapp launch/activate/quit notifications keep it current. A full rescan runs every 15s purely to heal any drift. - The panel is created once and pre-warmed at launch. It is drawn off-screen immediately after startup so the window server has already allocated its backing store and run a first draw pass. The first invocation costs the same as the thousandth.
- The list is drawn by hand. One custom
NSViewdrawing a dozen rows, rather than a table view or a SwiftUI hierarchy that would need building and laying out on first display. - Icons are rasterised in advance. Scaled once when a snapshot lands, cached by pid.
- Filtering is a pass over that same in-memory snapshot, run on the main thread between keystrokes. No Accessibility call, no index to maintain, and nothing to invalidate: a few dozen string comparisons cost less than the redraw they trigger.
- Accessibility calls are timeout-capped at 250ms and never run on the main thread, so an app that is beachballing cannot stall the switcher.
Requires the Xcode command line tools. No Xcode project, no dependencies.
./build.sh # produces build/MrTab.app
./build.sh --run # build, then launch it
./build.sh --debug # unoptimised build
./build.sh --universal # fat binary for both Apple Silicon and Intel
./build.sh --package # also produce build/MrTab.tar.gzA plain build targets only the machine that built it. --universal cross-compiles both slices and
lipos them together; it needs nothing beyond the command line tools, because it spells out the
target triples rather than using swift build --arch, which would require a full Xcode install.
The app icon is drawn in code by IconGenerator and turned into an .icns during the build, so
the artwork is source you can diff rather than a checked-in binary.
This will bite you, so it is worth understanding. Without a signing identity the bundle is
ad-hoc signed, and TCC records the grant against the binary's cdhash. Any rebuild that
changes the code changes that hash, and macOS stops honouring the grant — while still showing the
toggle as switched on. The symptom is a switcher that does nothing, and trusted=false in the
log.
Clear the stale entry and grant it again:
./build.sh --reset-permission
open build/MrTab.app # prompts fresh; re-add MrTab in System SettingsTo stop it happening at all, sign with a stable identity. Create a self-signed code signing
certificate once (Keychain Access → Certificate Assistant → Create a Certificate → name it
MrTab-dev, type Code Signing), then:
MRTAB_SIGN_IDENTITY=MrTab-dev ./build.shTCC then keys the grant on the certificate rather than the binary hash, so it survives rebuilds.
Every push to main builds on a GitHub Actions macOS runner and publishes a universal bundle as
v1.2.<run number>, with both a versioned tarball and an unversioned one so
releases/latest/download/MrTab.tar.gz is a stable URL.
CI builds the real artefact rather than a reduced one — macOS runners ship the whole toolchain, and ad-hoc signing needs no keychain or secrets. What it cannot do is notarise, which needs the paid Developer ID certificate, or exercise the hotkey path, which needs an Accessibility grant no runner can give. The verify step therefore checks what is checkable: both architectures present, signature valid, icon present, version stamped.
Building from source there is the least friction, since it settles architecture, signing and Gatekeeper in one go:
xcode-select --install # if the command line tools are missing
git clone https://github.com/andrecarlucci/mrtab.git
cd mrtab
./build.sh --runOtherwise use the install command at the top of this file, or copy a --universal build across.
Quarantine is applied by the receiving program, not by macOS in general, so how it arrives
decides whether Gatekeeper objects:
| Transfer | Quarantined? |
|---|---|
Built locally, or curl + tar -xz |
No |
scp / rsync over SSH, USB drive |
No |
| AirDrop, browser download, Mail, Messages | Yes |
| Double-clicking a downloaded archive | Yes |
A Yes is not fatal — see If macOS blocks it for the two ways to open a quarantined copy.
Nothing else travels with the app: ~/.config/mrtab/config.json and ~/Library/Logs/MrTab.log are
per-machine, and Accessibility has to be granted separately on each Mac.
MrTab logs to ~/Library/Logs/MrTab.log. It records the trust state at launch, the result of hot
key registration, every time the shortcut fires, and how many windows each invocation found — which
is enough to tell "not trusted", "shortcut taken by another app" and "no windows" apart.
tail -f ~/Library/Logs/MrTab.logEach line is tagged with its process id, which matters because two copies of MrTab cannot
coexist. A build in the repo and a copy in /Applications share a bundle identifier, so they
compete for the same Accessibility grant and the same hot key. The failure is quiet and misleading:
the older instance keeps the hot key, but granting access to the newer one revokes the older one's,
so the switcher still opens and lists almost nothing — and both processes write to one log, which
then reads as a single app contradicting itself.
MrTab terminates any earlier instance on launch, so the most recently opened copy wins. If you
suspect a leftover, pgrep -xl MrTab should print exactly one line.
| File | Role |
|---|---|
AppDelegate.swift |
Wiring, menu bar item, launch behaviour, single-instance guard |
WindowStore.swift |
Background window tracking, AX observers, MRU ordering, snapshot publishing |
SwitcherController.swift |
Show / step / filter / commit / cancel, modifier-release detection |
SwitcherPanel.swift |
The floating NSPanel, created once and pre-warmed |
SwitcherView.swift |
Hand-drawn header and row rendering, hover and click hit testing |
WindowFilter.swift |
Matching the list against what the user typed |
WindowNotes.swift |
The numbers and names pinned to windows by hand, and their file |
HotKey.swift |
Carbon RegisterEventHotKey registration, for the shortcut and the nine jump chords |
AXHelpers.swift |
Typed wrappers over the Accessibility C API |
Permissions.swift |
Accessibility trust checks and polling |
IconCache.swift |
Pre-scaled app icons by pid |
Config.swift |
JSON settings, loading and saving |
Shortcut.swift |
Modifier+key model, Carbon translation, key code names |
SettingsWindowController.swift |
Settings window and the permission banner |
ShortcutRecorderView.swift |
Click-then-press shortcut field |
LoginItem.swift |
Launch at login via SMAppService |
IconGenerator.swift |
Draws the app icon; build.sh pipes it through iconutil |
Log.swift |
Append-only log; an agent app has no console to fail loudly in |
SelfTest.swift |
MRTAB_RENDER=x.png renders the switcher and Settings offscreen |
MRTAB_RENDER=/tmp/shot.png build/MrTab.app/Contents/MacOS/MrTab writes six PNGs and exits: the
plain switcher, a filter with matches, a filter with none, a row hovered with its three buttons
showing, a row being renamed, and the settings pane. Two rows carry numbers in every shot, one of them
selected, since a badge is drawn against the accent fill there rather than against the panel. It
needs no Accessibility and no Screen Recording, so it works in any context, and it is how the
layout gets checked.
The switcher is rendered over a stand-in for colourful wallpaper rather than a flat fill. A flat backdrop flatters the design and hides the only contrast that matters — the panel is translucent, so anything drawn at partial opacity competes with whatever is behind it. Row text is therefore drawn at full strength, with the app name distinguished from the window title by weight rather than opacity, over a scrim that keeps the base tone constant. The panel also pins itself to a dark appearance rather than following the system theme — like the system Command-Tab switcher — so the vibrancy material and the dynamic text colours can never disagree about which of them is dark.
- No live window thumbnails. Icons and titles only — a deliberate choice, since thumbnails need
Screen Recording permission and capture costs real time.
SwitcherView.Rowis the seam to add them behind. - Names and numbers do not survive closing the window they name, or a reboot — see What a note is pinned to for why.
⌘ Tabcannot be replaced (see Configuration).- Windows on other Spaces are listed by default; switching to one moves you to that Space.
- Not notarised, so a downloaded copy needs
tar,xattr, or Gatekeeper's Open Anyway override — see If macOS blocks it. Notarisation requires the paid Apple Developer Program; there is no free route to it.