2026-05-02 23:18:49 -05:00
2026-05-02 12:48:31 -05:00

tmux-popups

tmux-popups. Instrument Panel wordmark beside a popup over a terminal pane.

Tools up. Context stays.

A quick menu for your tmux tools. One TSV registry generates the menu and direct bindings, with shared shell helpers, dependency checks and local overrides.

Website · Docs · Install · Gitea · Support SUBCULT

A SUBCULT project.

tmux-popups demo

Features

  • Prefix+Enter quick menu generated from registry rows
  • Direct bindings generated from the same rows
  • Default registry: popups.tsv
  • Local override registry: ~/.config/tmux-popups/popups.local.tsv
  • Dependency status in help and CLI output
  • Doctor script for common tmux/TPM/config problems
  • Bash launcher with optional Python helpers; no build step
  • Generated config stored at ${XDG_CACHE_HOME:-$HOME/.cache}/tmux-popups/generated.conf

Source and pull requests: canonical Gitea repository. The GitHub repository is a push mirror used by the TPM shorthand below. See migration and rollback and troubleshooting. The product website has installation and usage docs. See the website source for local preview instructions.

Install with TPM

Add to ~/.tmux.conf or your sourced tmux config:

set -g @plugin 'PatrickFanella/tmux-popups'

Reload tmux, then press TPM install key (prefix + I).

Install manually

git clone https://git.subcult.tv/PatrickFanella/tmux-popups.git ~/.config/tmux/plugins/tmux-popups

Then source the plugin entrypoint:

run-shell "$HOME/.config/tmux/plugins/tmux-popups/tmux-popups.tmux"

Reload tmux:

tmux source-file ~/.tmux.conf

Local development with TPM

Useful when hacking on the plugin but still letting TPM manage load/update keys.

git clone https://git.subcult.tv/PatrickFanella/tmux-popups.git ~/Projects/tools/tmux-popups
mkdir -p ~/.config/tmux/plugins
ln -sfn ~/Projects/tools/tmux-popups ~/.config/tmux/plugins/tmux-popups

Keep TPM registration in tmux config:

set -g @plugin 'PatrickFanella/tmux-popups'

TPM will see tmux-popups; the symlink points it at your working tree.

Default bindings

The small default enables help, shell, daily notes, timer and tmux keys. Optional adapters remain in the registry and appear with their enabled/dependency status in help and scripts/list-popups.sh --deps.

Key Action
Prefix+Enter Quick menu
Prefix+C-h Help, dependency status and optional adapter discovery
Prefix+T Shell
Menu N Daily note
Menu P Timer
Prefix+C-b tmux keys
Prefix+R Reload tmux config

To retain the previous full menu and direct keys, set these before loading:

set -g @tmux-popups-profile 'full'
set -g @tmux-popups-availability 'ignore'
set -g @tmux-popups-conflicts 'replace'

full enables every shipped row. ignore retains the previous behavior of launching adapters even when their dependencies are missing. Leave availability at its default to show unavailable tools as disabled menu labels instead. The replace conflict policy restores the previous behavior of taking existing prefix keys. Local rows and per-entry options still take precedence over the profile.

Enable an optional adapter without copying or deleting its row:

set -g @tmux-popups-lazygit-enabled 'on'
set -g @tmux-popups-chat-enabled 'off'

Reload through the plugin entrypoint after changing settings. An off row has no direct or menu binding and cannot be dispatched by ID. It remains listed for discovery. Existing seven-, eight- and nine-column local rows default to enabled, including local overrides of optional shipped rows.

Session switching is intentionally left to tmux-sessionx. Example:

set -g @plugin 'omerxx/tmux-sessionx'
set -g @sessionx-bind 'j'
set -g @sessionx-prefix 'on'
set -g @sessionx-window-height '80%'
set -g @sessionx-window-width '60%'

Then Prefix+j opens sessionx. If you prefer tmux's built-in picker, copy the sessions row from examples/popups.optional.tsv.

Plugin options

Set these before the plugin loads:

set -g @tmux-popups-menu-key 'Enter'
set -g @tmux-popups-reload-key 'R'
set -g @tmux-popups-config-file '~/.tmux.conf'
set -g @tmux-popups-default-width '80%'
set -g @tmux-popups-default-height '80%'
set -g @tmux-popups-local-registry '~/.config/tmux-popups/popups.local.tsv'
set -g @tmux-popups-enable-vscode 'on'
set -g @tmux-popups-vscode-command 'code .'
set -g @tmux-popups-yazi-mode 'window'
set -g @tmux-popups-profile 'small'
set -g @tmux-popups-availability 'show-disabled'

@tmux-popups-config-file is the tmux config file reloaded by the reload binding (@tmux-popups-reload-key, default R) and the Quick Menu "reload tmux" entry. Defaults to ~/.tmux.conf. Set this if your config lives elsewhere, for example:

set -g @tmux-popups-config-file '~/.config/tmux/tmux.conf'

Config paths, local registry paths and PROJECTS_DIR accept a literal ~/ prefix or bare ~. They expand to the home directory once. Absolute and ordinary relative paths stay as supplied; variable expressions and command substitutions in these settings are treated as text.

Generation, dispatch, help and doctor select the same local registry. A nonempty TMUX_POPUPS_LOCAL_REGISTRY environment value takes precedence over @tmux-popups-local-registry, followed by the optional XDG config default. An explicitly selected registry must be a readable file; a missing optional default is allowed. Set the environment value when running a CLI helper against a particular registry.

Use - in a row's width or height to inherit the default width/height options.

Registry format

Rows are tab-separated:

id	direct_key	menu_key	title	width	height	command	mode	completion	enabled	deps	kind	arguments
Column Meaning
id Unique popup id used by scripts/run-popup.sh
direct_key Direct tmux binding after prefix, or - for none
menu_key Key in quick menu, or - for none
title Popup title
width tmux popup width, e.g. 80%, or - for default
height tmux popup height, e.g. 80%, or - for default
command Executable path interpreted by kind, or - for the legacy interactive shell
mode Optional popup, window, command, or - for compatibility defaults
completion Optional foreground, background, or - for foreground
enabled Optional on or off; old rows default to on
deps Optional auto, -, or space-separated command groups with `
kind Optional plugin, absolute, user, shell, or - for plugin
arguments Optional JSON array of strings, or - for no arguments

Blank lines and lines beginning with # are ignored. Seven-column rows remain valid. Eight-column rows add mode; nine-column rows add completion. Ten columns add enabled; eleven add dependency metadata. Twelve add execution kind; thirteen add explicit arguments. Supplied fields must be nonempty, so use - for a default. --tsv emits nine columns with resolved mode and completion; --deps-tsv appends dependencies and status. --state-tsv emits those nine columns followed by enabled, dependencies and status. --execution-tsv appends kind and arguments to --state-tsv. The existing output layouts remain unchanged. All listing modes retain disabled and unavailable rows.

auto checks the effective shipped adapter path, never the entry ID. Renaming an adapter entry keeps its dependency checks; replacing its command changes them. Unknown custom executables have no inferred external requirements. Their file must still exist and be executable. Declare requirements explicitly when needed, for example python3 fzf|sk, or use - to bypass external checks. This metadata does not evaluate shell expressions or change command arguments.

User scripts and arguments

plugin resolves an executable relative to the plugin checkout. absolute requires an absolute executable path and expands a literal ~/ prefix once. user resolves a relative executable beneath the user-script directory. These kinds require a regular executable file, including for disabled rows, and check it again at dispatch. Paths are single fields; do not add shell quotes around them. Spaces and apostrophes are part of the filename. Relative paths are joined to their selected directory; .. is allowed, so this is not a sandbox.

The user-script directory defaults to ${XDG_CONFIG_HOME:-$HOME/.config}/tmux-popups/scripts. A nonempty TMUX_POPUPS_USER_SCRIPT_DIRECTORY environment value overrides @tmux-popups-user-script-directory, which overrides that default. The selected directory must be absolute after literal home expansion. The plugin never creates, updates or removes user scripts or the local registry. Keep these files outside the plugin checkout and ${XDG_CACHE_HOME:-$HOME/.cache}/tmux-popups, which contains generated data. Reload after changing the registry or options.

set -g @tmux-popups-user-script-directory '~/my scripts'

For example, these rows use actual tabs between fields:

personal	X	x	Personal script	80%	80%	record user's argv	popup	foreground	on	-	user	["two words", "it's literal", ""]
external	-	-	External script	80%	80%	/home/me/my scripts/task	command	foreground	on	-	absolute	["--project", "project with spaces"]
expression	-	-	Explicit shell	80%	80%	printf hello | cat	command	foreground	on	-	shell	-

JSON arguments require Python 3. Each string is exactly one argument, including empty strings, spaces, apostrophes and JSON-escaped tabs or newlines. NUL and surrogate code points are rejected. Arguments never expand ~, variables, globs or command substitutions. Keep the array on one physical TSV line. Missing Python or malformed arguments fail validation before cache publication. Old rows with no arguments do not gain a Python dependency.

shell explicitly opts into ${SHELL:-bash} -c and treats command as a shell expression. It requires arguments to be -; put any shell syntax in the expression itself. Shell expressions are evaluated only during execution. auto infers dependencies only for plugin adapters. Other kinds have no inferred requirements; declare their dependencies explicitly. The legacy command value - with kind plugin still opens an interactive shell and accepts no arguments. Existing modes, completion policies, invoking context, enabled controls and registry precedence apply to all execution kinds.

Availability policies apply to enabled rows:

  • show-disabled, the default, omits unavailable direct bindings and shows a disabled menu label with missing:<commands>.
  • hide-unavailable omits unavailable direct and menu bindings. Help and CLI dependency output still show the reason.
  • ignore launches without a dependency preflight, for explicit compatibility or user-managed environments.

Dispatch rechecks availability, so a removed dependency cannot launch silently from a stale menu. It reports the missing group to the origin client and stderr and returns 127. Dependency checks establish presence, not provider credentials, configuration or terminal compatibility. khal also needs a successful --version, matching its adapter's fallback to cal.

The editor resolver honors a nonempty EDITOR as one executable name or path. Otherwise it chooses the first available nvim, vim, then vi. The shell resolver honors a nonempty SHELL, otherwise it uses bash. An explicit missing editor or shell is reported as unavailable rather than replaced by another command. Execution and dependency checks use these same resolvers.

Legacy rows and rows with mode - default to popup. The four shipped adapters yazi.sh, home.sh, projects.sh and downloads.sh under scripts/tools/ default to window and honor @tmux-popups-yazi-mode, which accepts popup or window. This compatibility rule follows the executable path, including renamed entries. A legacy local override pointing at another adapter defaults to popup. Add an explicit mode to keep a different launch policy. Explicit modes take precedence over the legacy option, even for Yazi adapters. Entry IDs never choose a mode.

Popup uses the row dimensions, inherits configured dimensions for -, and closes when the adapter exits through display-popup -E. Window creates a window in the origin session, uses the session's dimensions and normal tmux remain-on-exit behavior. Width and height remain validated in every mode but only affect popup. Both run the adapter in the invoking directory.

Command runs the executable directly without a popup or window, in the invoking directory with the same origin variables. Foreground is the default. The tmux run-shell action waits for completion and displays output through tmux; it does not provide an interactive terminal. A nonzero exit produces tmux's normal run-shell diagnostic view, dismissed with q. A CLI call returns the adapter's exit code. Background starts the adapter with stdin, stdout and stderr connected to /dev/null, then returns success after starting it. It neither waits nor reports the later exit status. The adapter owns any durable output it needs. Background is valid only for command mode; command mode requires an executable, not -. For executable kinds, the command column is one path and arguments come from the JSON argument column. Only the explicit shell kind evaluates an expression.

Choose an adapter that does not require terminal input for command entries. run-popup.sh <id> executes adapters directly for CLI callers. --launch is the binding/menu entrypoint that creates popup or window presentation. Command completion policy applies to both entrypoints. CLI calls without a running tmux server validate registry keys using a private, temporary server, then remove that server and its socket before dispatch.

Local override registry

Default path:

~/.config/tmux-popups/popups.local.tsv

The plugin merges:

popups.tsv
  + popups.local.tsv
  -> generated bindings

If a local row has the same id as a default row, the local row overrides it while keeping the original order. New local ids are appended.

Example local override: move chat to another key and use default popup size:

chat	C-a	a	quick chat	-	-	scripts/tools/chat.sh

Example local-only scratch shell:

scratch	C-s	s	scratch shell	80%	80%	-

Reload tmux after edits:

tmux source-file ~/.tmux.conf

Create a popup script

Keep personal scripts outside the plugin checkout so updates preserve them:

mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/tmux-popups/scripts"
cat > "${XDG_CONFIG_HOME:-$HOME/.config}/tmux-popups/scripts/hello.sh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
printf 'Hello from tmux-popups.\n\nPress Enter to close... '
read -r _ || true
SH
chmod +x "${XDG_CONFIG_HOME:-$HOME/.config}/tmux-popups/scripts/hello.sh"

Add this row to your local registry, with actual tabs between fields:

hello	F8	8	hello	75%	75%	hello.sh	popup	foreground	on	-	user	-

If you configured @tmux-popups-user-script-directory, put hello.sh there instead. Reload through the plugin entrypoint, then press Prefix+F8 or select 8 from Prefix+Enter.

Optional examples

Extra rows live in:

examples/popups.optional.tsv

Copy selected rows into your local registry, install matching tools, then reload through the plugin entrypoint. Keep the shipped popups.tsv unchanged for updates.

Examples:

sessions	C-j	j	sessions	80%	80%	scripts/tools/sessions.sh
chat	O	g	quick chat	80%	80%	scripts/tools/chat.sh
yazi	Y	r	yazi	80%	80%	scripts/tools/yazi.sh

chat needs ocq.

List and dependency helpers

scripts/list-popups.sh --pretty
scripts/list-popups.sh --deps
scripts/list-popups.sh --tsv
scripts/list-popups.sh --deps-tsv

Prefix+C-h shows the same dependency status inside tmux.

Dependency groups can contain alternatives, e.g. glow|bat|less means one of those commands is enough.

Doctor

Run:

scripts/doctor.sh

It checks:

  • tmux availability/version
  • plugin root
  • default/local registries
  • TPM path and local plugin entry
  • TPM registration in tmux config
  • temporary generated config and tmux source-file -n validation
  • effective options, entry provenance, dependencies, close policy and binding conflicts

Ordinary diagnosis leaves the shared cache, prefix bindings and ownership unchanged. See effective diagnosis for details and the explicit bounded input check.

Warnings are informational. Failures exit nonzero.

Validate and apply settings

Run these commands from the plugin checkout inside the affected tmux server:

scripts/doctor.sh
scripts/generate-config.sh
scripts/apply-config.sh --explain "${XDG_CACHE_HOME:-$HOME/.cache}/tmux-popups/generated.conf"
./tmux-popups.tmux

Doctor diagnoses without applying settings. Generation publishes a validated cache but does not install bindings. Explanation previews conflicts against that cache. The plugin entrypoint regenerates and applies bindings with ownership checks. Source your configured tmux config first if you changed options there. Do not source the generated cache directly; that bypasses conflict reconciliation.

CI

The canonical Gitea workflow, .gitea/workflows/ci.yml, runs scripts/check.sh for pull requests and pushes to main. It runs Bash syntax checks, ShellCheck and the isolated regression suite in node:24-bookworm-slim, installing Git, Python 3, ShellCheck and tmux. The GitHub mirror workflow runs the same script on an Ubuntu runner.

Locally, install those verification tools plus GNU timeout, then run:

scripts/check.sh

tests/run.sh runs the Python regression suite alone with a 360-second bound. See the verification guide for fixture details. Automated PTY checks, actual attached terminal checks and real provider checks are separate evidence. Optional Yazi, OpenCode and provider adapters are not qualified merely because their dependency check or CI passes. See qualification records.

Optional tools used by default/extra rows

  • ocq for quick OpenCode chat
  • opencode
  • task / Taskwarrior
  • nvim, vim, vi, or $EDITOR
  • tldr, man, less
  • khal, cal
  • python3
  • ssh, fzf
  • cliphist, wl-copy, wl-paste
  • newsboat, curl
  • journalctl, tail, watch
  • glow, bat
  • lazygit, yazi, ferrosonic

Environment variables

Some tool scripts respect environment variables:

Variable Script Default
NOTES_DIR notes popup ~/Notes/daily
PROJECTS_DIR projects popup ~/Projects
TMUX_POPUPS_YAZI_SAFE yazi launcher on

Yazi rows open in a normal tmux window by default. Yazi can report a terminal response timeout inside display-popup, so window mode is the safer default.

Yazi still runs in safe mode by default. tmux popups do not reliably support image-preview passthrough, so the launcher disables Yazi preview/preload plugins via a generated cache config. Set TMUX_POPUPS_YAZI_SAFE=off before launch to use your full Yazi config and accept the tmux risk.

To force popup mode:

set -g @tmux-popups-yazi-mode 'popup'

Warning: popup mode may trigger Yazi terminal response timeouts; tmux-popups shows a warning before launching a Yazi adapter in popup mode.

How it works

popups.tsv + optional local registry
  -> scripts/list-popups.sh --tsv
  -> scripts/generate-config.sh
  -> ~/.cache/tmux-popups/generated.conf
  -> scripts/apply-config.sh
  -> reconciled tmux prefix bindings

scripts/run-popup.sh <id> reads the merged registry and executes the matching adapter. Bindings and menu actions use --launch to apply its presentation mode.

Files

  • popups.tsv: default popup registry
  • ~/.config/tmux-popups/popups.local.tsv: optional local overrides
  • examples/popups.optional.tsv: optional popup row examples
  • tmux-popups.tmux: TPM/manual entrypoint
  • scripts/list-popups.sh: merged registry and dependency listing
  • scripts/generate-config.sh: generates tmux bindings
  • scripts/apply-config.sh: validates and reconciles binding ownership
  • scripts/run-popup.sh: dispatches popup ids to tool scripts
  • scripts/doctor.sh: diagnostics
  • scripts/popup-help.sh: help popup
  • scripts/lib.sh: shared shell helpers
  • scripts/tools/*.sh: popup tool implementations

License

MIT

Verification

Run scripts/check.sh for syntax, ShellCheck and isolated launcher regressions. See the verification guide for required tools, fixture isolation and the separate real-terminal release gates.

Diagnose effective settings without applying changes

Run scripts/doctor.sh inside the affected tmux session. The report includes effective option values and their sources, the selected registry and user-script directory, each entry's file/line, launch mode, enablement, executable and dependency status, and the proposed binding plan. It uses the shared registry validator and execution resolver. Explicit executable arguments and shell expressions are not printed.

Ordinary diagnosis generates into an owned temporary directory. It leaves the shared cache, effective bindings and ownership unchanged, including when a registry is invalid. A missing or stale cache is reported for a later reload through tmux-popups.tmux. TPM registration checks remain read-only.

For a bounded input check, run this explicitly in the affected popup or window:

scripts/doctor.sh --input-check 10

Type tmux-popups and press Enter. The check prints the received byte values, TERM and tmux version. It waits at most the requested 1–30 seconds, defaults to 10 seconds and requires an attached terminal. Ctrl-C cancels the check. A successful input check does not qualify an application's terminal queries or a provider. Compare popup and window modes and record terminal/application versions for failures. See the separate qualification gates in #12 and #24.

Binding restoration on reload

The plugin saves each effective prefix binding and its prior binding on the tmux server. Changing or removing a direct, menu or reload key restores the prior binding, including its note and repeat flag. If the key had no prior binding, it is unbound. Restoration only happens when the current binding still matches what the plugin installed. Other key tables are untouched.

A user replacement or explicit unbind stays in effect across repeated reloads, even while that plugin key remains configured. Removing the key from the plugin configuration ends its ownership record. Configuring it again later starts a new record. Bindings from versions that did not record ownership cannot be safely identified or restored automatically.

Reload through tmux-popups.tmux so generation and reconciliation both run. The reconciler validates new bindings in a temporary key table before changing prefix keys, serializes reloads on the server, and rolls back touched keys and ownership state if applying the transition fails.

Binding conflicts and ownership

@tmux-popups-conflicts controls a new plugin key that already has a binding:

Value Behavior
skip (default) Keep the existing binding and report the conflict.
warn Report the conflict, then replace the existing binding.
replace Replace the existing binding without a conflict warning.

Reports include the registry file, line and entry ID, or the built-in menu/reload name, followed by the previous binding. Skipped keys have no plugin ownership record. If you unbind one and reload, the plugin can take the now-free key. Owned keys update normally on reload. Later user replacements and explicit unbinds stay protected under every policy.

To explain the effective bindings without applying changes, use the existing generated configuration:

scripts/apply-config.sh --explain "${XDG_CACHE_HOME:-$HOME/.cache}/tmux-popups/generated.conf"

The report shows each proposed key's source, effective binding and action. Installed bindings also carry source notes visible in tmux list-keys -N. Explanation uses disposable validation tables and leaves the cache, effective bindings and ownership unchanged. Reload through tmux-popups.tmux to apply settings. Sourcing generated.conf directly bypasses conflict and restoration policies.

A duplicate shortcut within the plugin, including reserved menu q, R and optional v, remains a validation error under every policy. Change one of the registry shortcuts instead of relying on one entry to overwrite another.

Registry validation and cache publication

Every non-comment registry row must have seven through thirteen nonempty TSV fields. IDs accept letters, digits, underscores and hyphens. Keys accept a single printable character or a named tmux key with C-, M- or S- modifiers. Use - to disable a shortcut. Dimensions accept cell counts from 1 through 2147483647, 1% through 100%, or - for the configured default. Executable commands must name an existing regular executable resolved by their plugin, absolute or user kind. The legacy - command opens a shell. The explicit shell kind evaluates an expression only during dispatch. Optional tool availability is checked separately, so a wrapper can be valid while its tool is not installed. Disabled rows still undergo full validation, including metadata and shortcuts. Titles cannot contain tmux formats or carriage returns.

Errors identify the registry file and line, and listing emits no partial TSV. All source rows are checked, including rows replaced by an intentional same-ID override. Shortcuts are checked on the final merged rows. Duplicate direct or menu shortcuts and collisions with the built-in menu, reload, Exit and enabled vscode shortcuts fail generation. Direct and menu keys use separate namespaces. Listing checks every source key against the selected tmux server before merging. Target-normalized collisions identify both source rows or the built-in slot.

Generation requires a running target tmux server. Listing can use a private temporary server when called outside tmux. Generation writes an owned temporary file beside generated.conf, parses it on that server, and validates bindings in disposable key tables without changing effective keys or ownership. Only a successful validation replaces the cache with an atomic rename. Failure leaves the last valid cache intact and removes owned temporary files and tables. Overlapping writers use separate files and tables; the last successful rename wins. Readers see a complete previous or new config. A failed writer cannot replace a successful writer's config. Binding application remains the separate server-serialized transaction described above.

The configurable vscode menu command runs through run-shell with a shell-quoted pane directory, including on tmux 3.3a. It retains shell command semantics.

Keep failures readable and retry

The per-entry option @tmux-popups-<id>-close controls popup/window completion:

Value Behavior
always (default) Keep the previous behavior: exit when the tool exits.
on-success Exit on status 0 and retain other failures for recovery.
never Retain the view after success or failure.

For example, keep a Yazi failure visible in its default window mode:

set -g @tmux-popups-yazi-enabled 'on'
set -g @tmux-popups-yazi-close 'on-success'

Retained policies require Python 3 for foreground-terminal control. They apply to popup/window entries; command entries keep their existing completion and exit-status behavior. Invalid policies or command-mode combinations fail shared validation before the generated cache changes. Doctor reports the effective close policy.

After the application ends, recovery shows its exit status. Press r to start the same entry again, c or Enter to close, or w from a popup to start again in a new window. Reopening starts a new process; it does not move the old live process or promise application session recovery. The original invoking context is retained. Retry revalidates the current registry/executable while inheriting the recovery view's existing environment. Close and relaunch to pick up a new server environment or a change to command mode.

While the application runs, its input stays with the application. Ctrl-C cancels and closes the launch, and owned process-group cleanup completes before retry. A crashed application's terminal settings are restored before recovery reads a key. Closing a retained failure preserves its exit status, so tmux may show its usual run-shell error view afterward; q closes that view. A piped/nonterminal call keeps the application's status without a recovery prompt. Tmux's separate remain-on-exit setting can still retain a completed window.

Add project-aware actions

Copy selected rows from examples/popups.project.tsv into your local registry and reload through tmux-popups.tmux. The examples provide Git status, a shell, project notes and OpenCode. They use explicit JSON arguments and declare their optional dependencies. No provider account or background service is required by the adapters.

The shell stays in the exact invoking directory. Git, notes and coding tools use the containing Git repository's root. Outside a repository, notes and coding tools use the invoking directory; Git status reports Git's normal non-repository error. The original TMUX_POPUPS_DIRECTORY remains the invoking directory even when the adapter changes its working directory to the project root.

Project notes default to NOTES.md at that root. Configure a relative path under the root in your tmux configuration:

set-environment -g TMUX_POPUPS_PROJECT_NOTES 'docs/project notes.md'

The value can also be an absolute path or a literal ~/ path. An explicit notes argument takes precedence, for example ["notes", "docs/project notes.md"]. Listing, help and doctor never create the file or its parent directory. Selecting notes opens the configured editor and creates its parent directory only after an editor is available.

The coding example uses ["code", "opencode"]. To use another installed tool, change the binary, dependency field and argument array together, for example ["code", "nvim", "."] with dependencies nvim. Arguments stay separate; shell expressions are not evaluated.

To reuse a project coding window, set:

set -g @tmux-popups-project_code-reuse 'directory'

This reuses by the exact invoking directory under the existing reuse contract. Different nested directories in the same Git repository still get separate windows, even though their adapters start at the same repository root. The example shell remains in its invoking directory and can use the same reuse setting under @tmux-popups-project_shell-reuse.

Add a searchable launcher

Copy the search row from examples/popups.optional.tsv into your local registry and reload through tmux-popups.tmux. The example uses Prefix+M-Space and menu /. Change those keys if they conflict with your existing bindings. The original quick menu stays available.

The search list shows each enabled entry's title, direct/menu shortcuts, launch mode and availability. With fzf installed, type a query and press Enter to select. Escape or Ctrl-C closes the picker without launching an entry. Pressing Enter with no matches also returns without dispatch. Without fzf, the same command opens a native tmux menu. No additional runtime is required for the fallback.

The search row uses foreground command mode because the launcher manages its own picker popup. The picker closes before the selected entry launches. Selection uses the original invoking client, session, window, pane and directory and goes through run-popup.sh, including its enablement, availability and reuse rules. Disabled entries are omitted; hide-unavailable also omits missing tools. Under show-disabled, a selected missing tool is rejected by the same dispatcher used for direct launches. The launcher excludes its own row from the choices.

The example's dependency field is - so the native fallback remains available without fzf. Invoke it through the registry binding/menu, or supply the explicit origin environment when calling the script yourself. It never guesses which attached client to target.

Reuse a tool window

Reuse is opt-in for entries in window mode. For example:

set -g @tmux-popups-yazi-enabled 'on'
set -g @tmux-popups-yazi-reuse 'session'

off (default) creates a new window on each launch. session returns to a live window for that entry in the invoking session. directory also requires the exact invoking directory to match, so different directories get separate tool windows. This scope uses the directory, without Git-root detection. Reuse with popup or command mode is a validation error.

The dispatcher marks its windows with the entry ID, scope, directory and tool pane. Renaming a tool window does not break reuse, and an unrelated window with the same title is never adopted. Concurrent launches of an entry are serialized within its invoking session. Closed windows are recreated. If a tool exits in a remain-on-exit window, the old pane stays available for inspection and the next launch creates a new tool window. Moving the marked tool pane to another window also retires the original window's reuse marker; the moved pane and any user panes stay untouched.

A reused process keeps its original launch environment and working directory. The origin helpers still reject a vanished client or pane. Reuse follows tmux's normal window selection: clients attached to the same session share its selected window. Other sessions have independent tool windows. CLI dispatch without the --launch context runs the adapter in the current terminal as before.

Invoking context

Direct bindings and menu actions capture the invoking context when tmux dispatches an entry. Both popup and window launches export TMUX_POPUPS_CLIENT, TMUX_POPUPS_SESSION, TMUX_POPUPS_WINDOW, TMUX_POPUPS_PANE and TMUX_POPUPS_DIRECTORY. The IDs refer to the origin, even when an adapter runs in a newly created window. The directory remains literal data, including spaces and shell expressions. Popup launches target the origin client and pane; new windows target the origin session explicitly.

Local scripts may read these variables or source scripts/lib.sh and call close_origin_popup. The optional sessions adapter targets the origin pane and switches only the invoking client. If an origin vanishes, these helpers report an error and cancel the action. They never fall back to an unrelated client. Exiting an adapter still exits normally. run-popup.sh <id> also accepts the contract through its environment. Without it, the CLI uses TMUX_PANE for pane, session and window IDs and $PWD for the directory, leaving the client unset. Client actions then cancel. Calls outside tmux can still run ordinary adapters. This contract applies to popup, window and command launch modes.

Chat failure and cancellation behavior

The optional chat adapter requires ocq and Node.js. Invalid, empty or failed responses leave the last valid session intact and return to the prompt with a short error. Clipboard failures keep chat open. q and /quit close chat at an input prompt; Ctrl-C cancels its request process group and closes chat while waiting. Copy preserves response bytes, including trailing newlines. Automated provider substitutes do not establish compatibility with a real provider.

S
Description
A quick menu for your tmux tools. One TSV registry for the menu and direct bindings, with local overrides and dependency checks.
https://tmux-popups.subcult.tv/ Readme MIT
67 MiB
0 Stars 1 Watchers 0 Forks
Languages
Python 57.8%
Shell 28.1%
HTML 9.3%
CSS 3.8%
JavaScript 0.8%
Other 0.2%