Troubleshooting
A category-organized list of things that aren’t bugs but can look like them, plus the handful of real edge cases worth calling out. Each entry starts with the symptom and ends with a concrete fix or an explanation of why the behavior is intentional.
Install and first launch
Section titled “Install and first launch”tmnl doesn’t launch from Finder
Section titled “tmnl doesn’t launch from Finder”Double-clicking tmnl.app flashes the dock icon and then nothing. No
window, no menu bar focus.
On a fresh download the most common cause is Gatekeeper quarantining the bundle. Open it once from the command line:
open /Applications/tmnl.app…and approve in System Settings → Privacy & Security. From v0.0.4 the DMG ships notarized, so a clean install from the GitHub release shouldn’t trip Gatekeeper at all. Older DMGs do; redownload from the latest release.
If you built from source and the launcher itself silently exits, run
the binary directly from a terminal — it logs to stderr there, and the
likely cause is a missing ~/Projects/tmnl/target/release/tmnl (the
nightly bundle execs the local binary; if the binary isn’t there, the
launcher has nothing to dispatch to).
Shell PATH is missing my tools
Section titled “Shell PATH is missing my tools”You launch tmnl from /Applications, open a shell, and which mnml /
which aws / which fnm come back empty even though those tools work
in every other terminal.
This is the macOS LaunchServices env: when you double-click the bundle
the launching process is launchd, not a shell, so none of your
~/.zshrc / ~/.zprofile exports are present. tmnl handles this by
running $SHELL -l -c env at startup and re-exporting the result —
the same load_login_shell_env_if_needed trick mnml and mixr use —
but only when stdin isn’t a tty (the LaunchServices case).
If you’re still seeing a stripped PATH, the most likely culprits are:
- Your shell hooks (
PATHexports,eval "$(rbenv init -)") live in~/.zshrcinstead of~/.zprofile. tmnl runs-l(login) which sources.zprofile, not.zshrc. Move PATH manipulation to.zprofilefor the login case. - Your
$SHELLenv var doesn’t match your actual shell. Checkecho $SHELL— if it’s wrong, fix it in System Settings → Users & Groups → Login shell.
Icon shows but window doesn’t appear
Section titled “Icon shows but window doesn’t appear”The dock icon bounces briefly, then nothing. No window, no crash dialog.
Usually a stale window-state snapshot saved off-screen — e.g. you used tmnl with an external monitor that’s no longer connected, and the saved geometry restores to coordinates that aren’t on any current screen. Clear the snapshot:
rm -rf ~/.config/tmnl/windowsRelaunch and the window comes up at the default position. If you
configured start_fullscreen = true or start_maximized = true in
~/.config/tmnl/config.toml, those still apply after the clear.
Fonts and glyphs
Section titled “Fonts and glyphs”Nerd Font glyphs render as boxes
Section titled “Nerd Font glyphs render as boxes”Tab chips, the integration rail, and the launcher row show □ or
tofu instead of the icons (nf-md-rocket-launch, nf-fa-database,
etc.).
tmnl walks ~/Library/Fonts and the system font dirs for files whose
stem contains the configured font_family string. If you set
font_family = "JetBrainsMono" but only installed the non-Nerd-Font
variant, no glyph font ships those private-use codepoints. Install a
Nerd Font build:
brew install --cask font-jetbrains-mono-nerd-fontThen either set font_family = "JetBrainsMonoNerdFont" in
~/.config/tmnl/config.toml, or leave font_family unset — the
default fallback chain already includes Nerd Font symbols when one is
installed.
Ligatures don’t render
Section titled “Ligatures don’t render”==, =>, != show as plain characters even though your font ships
combined ligature glyphs.
Check font_ligatures in ~/.config/tmnl/config.toml. Default is
true; if a previous tweak set it to false, ligatures are
suppressed regardless of the font:
[ui]font_ligatures = trueIf the setting is already true, the font itself probably doesn’t
ship the liga / calt OpenType features. JetBrains Mono and Fira
Code both do; their non--NoLigatures builds are the ones you want.
Font is the wrong size on retina
Section titled “Font is the wrong size on retina”Everything renders crisp but the text is much smaller or larger than expected on a 2x display.
The font_size config key is the point size at 1.0 zoom, default
14.0. Live ⌘+ / ⌘- zoom multiplies on top, persisted per tab.
If you carried a config.toml over from another machine with a
different DPI, set it explicitly:
[ui]font_size = 14.0Then press ⌘0 to reset the per-tab zoom to 1.0. The setting is
clamped to [6.0, 64.0] so the grid math stays sane.
Input and chords
Section titled “Input and chords”⌘F doesn’t open find
Section titled “⌘F doesn’t open find”You press ⌘F and nothing appears.
⌘F opens the in-pane find bar — but only when a pane is focused. If
you’re sitting on the welcome overlay or a modal (settings, palette,
discovery), the chord routes to the modal first. Dismiss the overlay
(Esc) and try again.
If you’re in native mode (running mnml or another backing app),
⌘F may be intercepted by the backing app’s own find. mnml binds
⌘F to its file-in-workspace search; that’s expected.
See Menu bar reference for the full chord list.
Global hotkey isn’t firing
Section titled “Global hotkey isn’t firing”You set up quick-terminal mode with a
global_hotkey chord, but pressing it from another app does nothing.
On macOS the cross-platform global-hotkey crate hands off to Carbon’s
RegisterEventHotKey, which doesn’t require Accessibility
permissions. The usual causes:
- Parse failure. The chord string follows the
global-hotkeysyntax ("CmdOrCtrl+Shift+Space","Alt+Backquote", etc.). tmnl logs a warning to stderr if parsing fails — launch from a terminal to see it. - Another app owns the chord. Carbon hotkeys are first-registered-wins. If Raycast / Alfred / Spotlight has the same combo, tmnl’s registration silently fails. Pick a different chord or unregister the conflicting one.
- Background-only quirk. The hotkey works when tmnl is in the
foreground but not when fully background. macOS only delivers
global Carbon events to apps with
LSUIElementor with at least one open window. Check~/.config/tmnl/config.toml— quick-terminal mode keeps a window alive (just hidden), so this shouldn’t bite unless you’ve quit tmnl outright.
Drag-resize spawns repeated prompts
Section titled “Drag-resize spawns repeated prompts”Resizing the window by dragging an edge fires multiple bell sounds or flashes the same toast over and over.
bell_rate_limit_ms is the deduplication knob. Default 0 (every BEL
fires). Bump it to 200 or so to coalesce bursts:
[ui]bell_rate_limit_ms = 200The drag itself is throttled internally — tmnl debounces SIGWINCH so zsh doesn’t get a redraw per pixel — so the bell-spam is the visible symptom, not actual repeated resize events.
Native mode
Section titled “Native mode”mnml doesn’t render in the tab
Section titled “mnml doesn’t render in the tab”You hit “Open mnml” from the launcher rail and get a tab labeled
(no client) that never paints.
Three things to check:
mnmlis on PATH. tmnl spawnsmnml --blit <socket>viaLauncher::spawn; if the binary isn’t found, the tab sits empty forever. From a tmnl shell,which mnmlshould resolve. If it doesn’t, see Shell PATH is missing my tools above.- The transfer socket is exported.
TMNL_TRANSFER_SOCKETis set at startup before any subprocess spawn. Inside a tmnl tab,echo $TMNL_TRANSFER_SOCKETshould print a path under$TMPDIR. If it’s empty, tmnl’s transfer listener failed at startup (printed to stderr) — usually a stale socket from a crashed previous tmnl. Delete<TMPDIR>/tmnl-*-transfer.sockand relaunch. - mnml version mismatch. If mnml is older than the published
tmnl-protocolversion tmnl advertises, the client decodesCaps::empty()and silently never sends frames. Upgrade mnml.
tmnl-protocol version mismatch
Section titled “tmnl-protocol version mismatch”Connecting a backing app produces a tab that draws once then hangs, or
a stderr line tmnl-protocol version mismatch.
PROTOCOL_VERSION is currently 7 (v7 added inline images). The
wire format is back-compat for older clients — a peer that
advertises an older version decodes optional flags as
Caps::empty() and tmnl gates the new features off. A peer that
advertises a newer version than tmnl knows about is the failure
case.
The fix is to upgrade the older side. The client SDK
(tmnl-protocol on crates.io) and the tmnl binary should match
the same major release. See the SDK manual for the
capability negotiation walkthrough.
Caps::INLINE_IMAGES not respected
Section titled “Caps::INLINE_IMAGES not respected”Your backing app calls send_inline_image_create and the image never
appears.
Three independent gates have to all be open:
- The client advertised
Caps::INLINE_IMAGESin itsHello. CheckClient::connect_with_caps(addr, Caps::INLINE_IMAGES | …). The defaultClient::connectadvertisesCaps::empty()and intentionally disables every optional feature. - The tmnl server peer-caps include
INLINE_IMAGES— every shipped tmnl does, this is mostly a check for forks. - The image format is PNG. tmnl decodes via the
imagecrate; PNG is the v7 wire format. JPEG is roadmap.
If all three pass and the image still doesn’t appear, the image is probably scrolled off-screen — inline images are cell-anchored, not pixel-anchored, so they scroll with the underlying row. Scroll back to the row you anchored to.
Theme and visual
Section titled “Theme and visual”Stoplights overlap the palette chip
Section titled “Stoplights overlap the palette chip”On launch the macOS traffic-light buttons (●●●) sit under or behind
the palette chip in the top chrome strip.
The strip pipeline puts stoplight centers in a fixed column and the
palette chip starts after a tuned gap. If a particular inset /
tab_layout combination still overlaps, raise inset slightly in
settings (⌘, → UI → inset). Default 4 is correct for the
shipped chrome geometry; tighter values risk this overlap.
If you’re on Linux or Windows there are no real stoplights — tmnl paints three placeholder chips in the same slot so the layout matches across platforms. Those are decorative; clicking them does nothing.
Tab strip drifts when window resizes
Section titled “Tab strip drifts when window resizes”You drag the window edge and the tab chips appear to shift left or right by a pixel or two during the drag, snapping back when you release.
This is expected. tmnl resizes the grid on every winit Resized
event, recomputes chip layout, and repaints — but the strip
geometry rounds chip widths to whole pixels (chip widths are
derived from cell widths, which are derived from atlas glyph
widths). The “drift” is the rounding ricocheting as the window
crosses pixel boundaries. After the drag settles the chips land at
their final integer positions.
If the strip is also losing chips during the drag, see Vertical sidebar tab layout — the vertical layout truncates chip labels at the sidebar width, which can look like chips disappearing if the sidebar gets narrower mid-drag.
Image renders below the text that emitted it
Section titled “Image renders below the text that emitted it”You print an OSC 1337 inline image from a shell-mode command and the image appears one or two rows below where you ran the command, with your prompt sitting on top of it.
This is the cell-anchor model again. tmnl uploads the image to a wgpu texture and renders it as an overlay anchored to the row where the OSC sequence appeared — which, when emitted at the end of a command’s output, is the row after the command (where the next prompt then lands). Scroll up by one row and the image is right where you’d expect.
Long-form image work (plots, screenshots) is the native-mode inline-image path, which doesn’t have this issue — the backing app explicitly addresses cell coordinates.
Updates and integrations
Section titled “Updates and integrations”Update notification not showing
Section titled “Update notification not showing”You’re on an older release and never see the tmnl: vX.Y.Z available
line.
The check is a single blocking GET to
api.github.com/repos/chris-mclennan/tmnl/releases/latest on a
background thread at startup, surfaced on stderr only (v1).
Visible when you launch from a terminal; the .app launcher’s
stderr goes to a log file you’d have to tail. From v2 the
notification surfaces in the welcome overlay too.
Other failure modes:
- No network. The check fails silently — tmnl doesn’t degrade or
retry, and the
take_pending_announcement()API returnsNone. - GitHub rate limit. Unauthenticated
/releases/latestcalls are rate-limited per IP. A burst of CI launches from the same IP can hit the limit; back off and try again later. - You’re already on the latest tag. No surprise here, but worth
checking with
tmnl --version.
See Updates for the full notification flow.
Discovery overlay doesn’t list installed apps
Section titled “Discovery overlay doesn’t list installed apps”You hit the + chip on the integrations rail, the overlay opens, but
mnml / mixr / mnml-aws-codebuild are all tagged ✗ not installed
even though you can run them from the shell.
The detection logic is an in-process $PATH walk (no which fork)
plus per-OS well-known install dirs (~/.cargo/bin universally,
Homebrew prefixes on macOS / Linux). If a binary is on a custom path
that none of those cover — e.g. a Nix profile dir, a mise
shim — the detection misses it. Two workarounds:
- Symlink the binary into
~/.cargo/bin(the universally-checked dir). The detection picks it up on the next overlay open. - Run
integrations.refreshfrom the palette to clear the per-session cache, in case the binary was installed after the first overlay open. The first call seeds the cache; subsequent opens reuse it until refresh.
See Integrations — the launcher rail for the full detection flow.
Launcher chip click does nothing
Section titled “Launcher chip click does nothing”You click a chip on the launcher rail and the window just blinks.
Launcher chips are config-driven via [[ui.launcher_icon]]. The
command field is either a registered command id (e.g.
"mixr.show") or a colon-prefixed ex-cmdline (":host.launch myapp"). If the command id is misspelled or the colon-prefixed
binary isn’t on PATH, the click resolves to a no-op.
Check the chip definition in ~/.config/tmnl/config.toml:
[[ui.launcher_icon]]id = "mnml"glyph = "\u{e620}"fallback = "m"command = "mnml.open_recent" # registered command id, not the binarycolor = "#5FB3D9"tooltip = "Open mnml"If command references a registered id but the matching backing app
isn’t installed, the click silently fails — same dynamic as the
discovery overlay. Install the sibling or change the chip’s
command to :host.launch <binary> and put the binary on PATH.
Nightly app bundle
Section titled “Nightly app bundle”The repo ships a nightly bundle target for contributors who want a
one-click dock icon for their latest cargo build output alongside
the released app:
./scripts/build-app.sh --nightlyThis produces target/tmnl-nightly.app with bundle identifier
rs.tmnl.app.nightly and an inverted warm-orange icon (vs. the
released build’s charcoal icon), so the two coexist in
/Applications and pin to the dock as separate entries. The nightly
launcher execs your local release binary directly — there’s no
dispatch shim — so updating means rebuilding the binary, not
rebuilding the bundle.
The nightly bundle is local-only. It isn’t produced by release CI and isn’t shipped to GitHub Releases. The intended use is a personal dock pin for contributors and the author; everyone else should download a tagged release.
macOS Tahoe — “Support Ending for Intel-based Apps” warning
Section titled “macOS Tahoe — “Support Ending for Intel-based Apps” warning”If you’re on macOS 26 (Tahoe) and a tmnl.app you previously
installed triggers a “Support Ending for Intel-based Apps” warning
at launch, the cause is LSMinimumSystemVersion in the bundle’s
Info.plist, not the binary itself. Pre-v0.0.4 builds declared
LSMinimumSystemVersion = 10.14, which Tahoe uses to classify the
bundle as legacy Intel — even though the binary itself is a real
arm64 build.
The fix landed in v0.0.4 — LSMinimumSystemVersion is now
11.0 in both the stable and nightly Info.plist. Redownload the
DMG from
releases and the
warning goes away.
v0.0.4 is also the first release that ships with macOS code-
signing and notarization wired into CI, so on a clean install
Gatekeeper now trusts the DMG without the separate “unidentified
developer” warning either.