Skip to content

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.

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:

Terminal window
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).

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 (PATH exports, eval "$(rbenv init -)") live in ~/.zshrc instead of ~/.zprofile. tmnl runs -l (login) which sources .zprofile, not .zshrc. Move PATH manipulation to .zprofile for the login case.
  • Your $SHELL env var doesn’t match your actual shell. Check echo $SHELL — if it’s wrong, fix it in System Settings → Users & Groups → Login shell.

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:

Terminal window
rm -rf ~/.config/tmnl/windows

Relaunch 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.

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:

Terminal window
brew install --cask font-jetbrains-mono-nerd-font

Then 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.

==, =>, != 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 = true

If 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.

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.0

Then 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.

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.

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-hotkey syntax ("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 LSUIElement or 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.

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 = 200

The 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.

You hit “Open mnml” from the launcher rail and get a tab labeled (no client) that never paints.

Three things to check:

  1. mnml is on PATH. tmnl spawns mnml --blit <socket> via Launcher::spawn; if the binary isn’t found, the tab sits empty forever. From a tmnl shell, which mnml should resolve. If it doesn’t, see Shell PATH is missing my tools above.
  2. The transfer socket is exported. TMNL_TRANSFER_SOCKET is set at startup before any subprocess spawn. Inside a tmnl tab, echo $TMNL_TRANSFER_SOCKET should 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.sock and relaunch.
  3. mnml version mismatch. If mnml is older than the published tmnl-protocol version tmnl advertises, the client decodes Caps::empty() and silently never sends frames. Upgrade mnml.

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.

Your backing app calls send_inline_image_create and the image never appears.

Three independent gates have to all be open:

  1. The client advertised Caps::INLINE_IMAGES in its Hello. Check Client::connect_with_caps(addr, Caps::INLINE_IMAGES | …). The default Client::connect advertises Caps::empty() and intentionally disables every optional feature.
  2. The tmnl server peer-caps include INLINE_IMAGES — every shipped tmnl does, this is mostly a check for forks.
  3. The image format is PNG. tmnl decodes via the image crate; 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.

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.

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.

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 returns None.
  • GitHub rate limit. Unauthenticated /releases/latest calls 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:

  1. Symlink the binary into ~/.cargo/bin (the universally-checked dir). The detection picks it up on the next overlay open.
  2. Run integrations.refresh from 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.

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 binary
color = "#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.

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:

Terminal window
./scripts/build-app.sh --nightly

This 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.4LSMinimumSystemVersion 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.