Last updated: 2026-03-07 Based on: parity-tmuxinator.md, parity-teamocil.md, import-tmuxinator.md, import-teamocil.md
- Blocker: libtmux has no method wrapping
select-pane -T <title>. Thepane_titleformat variable is excluded from libtmux's bulk format queries (formats.py:70, commented out with note "removed in 3.1+"), but this is a libtmux-side exclusion β tmux itself still supports both#{pane_title}(informat.c:205) andselect-pane -T(added in tmux 2.6). libtmux already knows about the display options (pane_border_status,pane_border_formatinconstants.py:163-173) but has no setter for the title itself. - Blocks: Pane titles (tmuxinator feature: named pane syntax
pane_name: commandβselect-pane -T). Also blocksenable_pane_titles,pane_title_position,pane_title_formatsession-level config. - Required: Add
Pane.set_title(title: str)method that callsself.cmd("select-pane", "-T", title). This is a simple wrapper βPane.cmd()already exists (pane.py:177) andselect-paneis already used forPane.select()(pane.py:601). - Non-breaking: Pure addition, no existing API changes.
- Blocker:
shutil.which("tmux")is hardcoded in two independent code paths:common.py:252βtmux_cmd.__init__(), the class through which all libtmux commands flow (called byServer.cmd()atserver.py:311)server.py:223βServer.raise_if_dead(), a separate code path that callssubprocess.check_call()directly There is no way to use a custom tmux binary (wemux, byobu, or custom-built tmux).
- Blocks: Wemux support (tmuxinator
tmux_command: wemux). Also blocks CI/container use with non-standard tmux locations. - Required: Add optional
tmux_binparameter toServer.__init__()that propagates totmux_cmd. Both code paths must be updated. Default remainsshutil.which("tmux"). - Non-breaking: Optional parameter with backward-compatible default. Existing code is unaffected.
- Blocker:
tmux_cmd(common.py:252-296) always executes commands. Debug logging exists (logger.debugat line 291) but logs the command and its stdout after execution, not before. There is no pre-execution logging or facility to collect commands without executing them. - Blocks:
--debug/ dry-run mode (both tmuxinator and teamocil have this). tmuxinator generates a bash script that can be previewed; teamocil's--debugoutputs the tmux command list. - Required: Either (a) add a
dry_runflag totmux_cmdthat collects commands instead of executing, or (b) add pre-execution logging at DEBUG level that logs the full command beforesubprocess.run(). Option (b) is simpler and doesn't change behavior. - Non-breaking: Logging change only. tmuxp would implement the user-facing
--debugflag by capturing log output. - Note: Since tmuxp uses libtmux API calls (not command strings), a true dry-run would require a recording layer in
WorkspaceBuilderthat logs each API call. This is architecturally different from tmuxinator/teamocil's approach and may not be worth full parity.
These libtmux APIs already exist and do NOT need changes:
| API | Location | Supports |
|---|---|---|
Session.rename_session(name) |
session.py:412 |
teamocil session rename mode |
Window.rename_window(name) |
window.py:462 |
teamocil --here flag |
Pane.resize(height, width) |
pane.py:217 |
teamocil v0.x pane width |
Pane.send_keys(cmd, enter) |
pane.py:423 |
All command sending |
Pane.select() |
pane.py:577 |
Pane focus |
Window.set_option(key, val) |
options.py:578 (OptionsMixin) |
synchronize-panes, window options |
Session.set_hook(hook, cmd) |
hooks.py:111 (HooksMixin) |
Lifecycle hooks (client-detached, etc.) |
Session.set_option(key, val) |
options.py:578 (OptionsMixin) |
pane-border-status, pane-border-format |
HooksMixin on Session/Window/Pane |
session.py:55, window.py:56, pane.py:51 |
All entities inherit hooks |
HooksMixin.set_hooks() (bulk) |
hooks.py:430 |
Efficient multi-hook setup (dict/list input) |
Session.set_environment(key, val) |
session.py:53 (EnvironmentMixin) |
Session-level env vars (teamocil with_env_var) |
Pane.clear() |
pane.py:818 |
Sends reset to clear pane (teamocil clear) |
Pane.reset() |
pane.py:823 |
send-keys -R \; clear-history (full reset) |
Pane.split(target=...) |
pane.py:625 |
Split targeting (teamocil v0.x target) |
- Blocker:
WorkspaceBuilder(builder.py) does not check for asynchronizekey on window configs. The key is silently ignored if present. - Blocks: Pane synchronization (tmuxinator
synchronize: true/before/after). Note: tmuxinator deprecatestrue/beforein favor ofafter(project.rb:21-29), but all three values still function. The import should honor original semantics of each value. - Required: Add
synchronizehandling inbuilder.py. Forbefore/true: callwindow.set_option("synchronize-panes", "on")before pane commands are sent. Forafter: call it inconfig_after_window(). Forfalse/omitted: no action. - Insertion point: In
build()around line 320 (afteron_window_createplugin hook, beforeiter_create_panes()loop) forbefore/true. Inconfig_after_window()around line 565 forafter. Note: in tmux 3.2+ (tmuxp's minimum),synchronize-panesis a dual-scope option (window|pane,options-table.c:1423). Setting it at window level viawindow.set_option()makes all panes inherit it, including those created later by split. - Non-breaking: New optional config key. Existing configs are unaffected.
- Blocker:
WorkspaceBuilderhas no handling for panetitlekey or session-levelenable_pane_titles/pane_title_position/pane_title_format. - Blocks: Pane titles (tmuxinator named pane syntax).
- Required:
- Session-level: set
pane-border-statusandpane-border-formatoptions viasession.set_option()inbuild()alongside other session options (lines 303-309). - Pane-level: call
pane.cmd("select-pane", "-T", title)after commands are sent initer_create_panes(), before focus handling (around line 535). Requires L1 (libtmuxset_title()), or can usepane.cmd()directly.
- Session-level: set
- Config keys:
enable_pane_titles: true,pane_title_position: top,pane_title_format: "..."(session-level).title: "my-title"(pane-level). - Non-breaking: New optional config keys.
- Blocker: The teamocil importer produces
shell_command_afteron the window dict (fromfilters.after,importers.py:149), butWorkspaceBuildernever reads it. Thetrickle()function inloader.pyhas no logic for it either. - Blocks: teamocil v0.x
filters.afterβ commands run after all pane commands in a window. - Required: Add handling in
config_after_window()(around line 565) or inbuild()after theiter_create_panes()loop (around line 331). Readwindow_config.get("shell_command_after", [])and send each command to every pane viapane.send_keys(). Note: this is a window-level key set by the teamocil importer, not per-pane. - Non-breaking: New optional config key.
- Blocker:
tmuxp load(cli/load.py) has no--hereflag.WorkspaceBuilder.iter_create_windows()always creates new windows viasession.new_window()(line 406). Additionally, teamocil always renames the current session (session.rb:18-20), regardless of--here; the--hereflag only affects window behavior (reuse current window for first window instead of creating new). tmuxp's--appendflag partially covers session rename mode, but does not rename the session. - Blocks: teamocil
--here(reuse current window for first window) and teamocil session rename (always active, not conditional on--here). - Required:
- Add
--hereflag tocli/load.py(around line 516, near--append). - Pass
here=Truethrough toWorkspaceBuilder.build(). - In
iter_create_windows(), whenhere=Trueand first window: usewindow.rename_window(name)instead ofsession.new_window(), and sendcd <root>viapane.send_keys()for directory change. - Adjust
first_window_pass()logic (line 584). - For session rename: when
--hereis used, also callsession.rename_session(name)(line 262 area inbuild()).
- Add
- Depends on: libtmux
Window.rename_window()andSession.rename_session()(both already exist, L4). - Non-breaking: New optional CLI flag.
- Blocker: tmuxp has no
stopcommand. The CLI modules (cli/__init__.py) only register:load,freeze,ls,search,shell,convert,import,edit,debug-info. - Blocks: tmuxinator
stop/stop-allβ kill session with cleanup hooks. - Required: Add
tmuxp stop <session>command. Implementation: find session by name viaserver.sessions, callsession.kill(). For hook support, runon_project_stophook before kill. - Non-breaking: New CLI command.
- Blocker: tmuxp's plugin system (
plugin.py:216-292) has 5 hooks:before_workspace_builder,on_window_create,after_window_finished,before_script,reattach. These are Python plugin hooks, not config-driven shell command hooks. There are no config keys foron_project_start,on_project_exit, etc. - Blocks: tmuxinator lifecycle hooks (
on_project_start,on_project_first_start,on_project_restart,on_project_exit,on_project_stop). - Required: Add config-level hook keys. Mapping:
on_project_startβ run shell command at start ofbuild(), beforebefore_scripton_project_first_startβ already partially covered bybefore_scripton_project_restartβ run when reattaching (currently only pluginreattach()hook)on_project_exitβ use tmuxset-hook client-detachedviasession.set_hook()(libtmux L4)on_project_stopβ run in newtmuxp stopcommand (T5)
- Depends on: T5 for
on_project_stop. - Non-breaking: New optional config keys.
- Blocker:
tmuxp loadhas no flag to skipshell_command_before. Thetrickle()function (loader.py:245-256) always prepends these commands. - Blocks: tmuxinator
--no-pre-windowβ skip per-pane pre-commands for debugging. - Required: Add
--no-shell-command-beforeflag tocli/load.py. When set, clearshell_command_beforefrom all levels before callingtrickle(). - Non-breaking: New optional CLI flag.
- Blocker: tmuxp has no user-defined variable interpolation. Environment variable expansion (
$VARviaos.path.expandvars()) already works in most config values βsession_name,window_name,start_directory,before_script,environment,options,global_options(seeloader.py:108-160). But there is no way to pass customkey=valuevariables at load time. - Blocks: tmuxinator ERB templating (
<%= @settings["key"] %>). - Required: Add a Jinja2 or Python
string.Templatepass before YAML parsing. Allowkey=valueCLI args to set template variables. This is a significant architectural addition. - Non-breaking: Opt-in feature, existing configs are unaffected.
- Blocker:
tmuxp loadhas no dry-run mode. Since tmuxp uses libtmux API calls rather than generating command strings, there's no natural command list to preview. - Blocks: tmuxinator
debugand teamocil--debug/--show. - Required: Either (a) add a recording proxy layer around libtmux calls that logs what would be done, or (b) add verbose logging that shows each tmux command before execution (depends on L3).
- Non-breaking: New optional CLI flag.
- Blocker: tmuxp only has
edit. Missing:new(create from template),copy(duplicate config),delete(remove config with confirmation). - Blocks: tmuxinator
new,copy,delete,implodecommands. - Required: Add CLI commands. These are straightforward file operations.
- Non-breaking: New CLI commands.
Keys produced by importers but silently ignored by the builder:
| Key | Producer | Importer Line | Builder Handling | Issue |
|---|---|---|---|---|
shell_command (session-level) |
tmuxinator importer | importers.py:60 |
Not a valid session key | Bug (I1 Bug B): pre commands lost when both pre and pre_window exist |
config |
tmuxinator importer | importers.py:37,44 |
Never read | Dead data β extracted -f path goes nowhere |
socket_name |
tmuxinator importer | importers.py:52 |
Never read | Dead data β CLI uses -L flag |
clear |
teamocil importer | importers.py:141 |
Never read | Dead data β builder doesn't read it, but libtmux has Pane.clear() (L4) |
height (pane) |
teamocil importer | passthrough (not popped) | Never read | Dead data β width is popped but height passes through silently |
target (pane) |
teamocil importer | passthrough (not popped) | Never read | Dead data β accidentally preserved via dict mutation, but libtmux has Pane.split(target=...) (L4) |
shell_command_after |
teamocil importer | importers.py:149 |
Never read | Dead data β tmuxp has no after-command support |
Two bugs in importers.py:59-70, covering both code paths for the pre key:
- Bug: When only
preexists (nopre_window) (importers.py:66-70), it maps toshell_command_beforeβ a per-pane key that runs before each pane's commands. But tmuxinator'spreis a session-level hook that runs once before any windows are created. The correct target isbefore_script. - Effect: Instead of running once at session start, the
precommands run N times (once per pane) as pane setup commands. This changes both the semantics (pre-session β per-pane) and the execution count.
- Bug: When both
preandpre_windowexist (importers.py:59-65):premaps toshell_command(line 60) β invalid session-level key, silently ignored by builder. Theprecommands are lost entirely (see Dead Config Keys table).- The
isinstancecheck on line 62 testsworkspace_dict["pre"]type to decide how to wrapworkspace_dict["pre_window"]β it should checkpre_window's type, notpre's. Whenpreis a string butpre_windowis a list,pre_windowgets double-wrapped as[["cmd1", "cmd2"]](nested list). Whenpreis a list butpre_windowis a string,pre_windowwon't be wrapped in a list β leaving a bare string where a list is expected.
preβbefore_script(session-level, runs once before windows)pre_windowβshell_command_before(per-pane, runs before each pane's commands)
before_script is executed via subprocess.Popen after shlex.split() in util.py:27-32 β without shell=True. This means shell constructs (pipes |, &&, redirects >, subshells $(...)) won't work in before_script values. For inline shell commands, the forward path is the on_project_start config key (T6), which would use shell=True or write a temp script.
- Bug:
str.replace("-f", "").strip()(importers.py:41,48) does a global string replacement, not flag-aware parsing. A value like"-f ~/.tmux.conf -L mysocket"would produce"~/.tmux.conf -L mysocket"as theconfigvalue (including the-Lflag in a file path). Also ignores-L(socket name) and-S(socket path) flags entirely. - Fix: Use proper argument parsing (e.g.,
shlex.split()+ iterate to find-fflag and its value).
- Bug:
for _b in w["filters"]["before"]:loops (importers.py:145-149) iterate N times but set the same value each time. - Fix: Replace with direct assignment.
- Bug: Importer assumes v0.x format. String panes cause incorrect behavior (
"cmd" in "git status"checks substring, not dict key).commandskey (v1.x) not mapped. - Fix: Add format detection. Handle string panes,
commandskey,focus, andoptions. - Also: v0.x pane
widthis silently dropped (importers.py:161-163) with a TODO but no user warning.heightis not even popped β it passes through as a dead key. Since libtmux'sPane.resize()exists (L4), the importer could preserve bothwidthandheightand the builder could callpane.resize(width=value)orpane.resize(height=value)after split. Alternatively, warn the user.
Not imported but translatable:
rvmβshell_command_before: ["rvm use {value}"]pre_tabβshell_command_before(deprecated predecessor topre_window)startup_windowβ find matching window, setfocus: truestartup_paneβ find matching pane, setfocus: trueon_project_first_startβbefore_script(only if value is a single command or script path; multi-command strings joined by;won't work sincebefore_scriptusesPopenwithoutshell=True)postβ deprecated predecessor toon_project_exit; runs after windows are built on every invocation. No tmuxp equivalent until T6 lifecycle hooks exist.socket_pathβ warn user to use CLI-Sflagattach: falseβ warn user to use CLI-dflag
Not imported but translatable:
v1.x keys (same key names in tmuxp):
commandsβshell_commandfocus(window) βfocus(pass-through)focus(pane) βfocus(pass-through)options(window) βoptions(pass-through)- String pane shorthand β
shell_command: [command]
v0.x keys:
with_env_varβenvironment: { TEAMOCIL: "1" }(defaulttruein v0.x; maps to session-levelenvironmentkey)height(pane) β should be popped likewidth(currently passes through as dead key)
importers.py:121,123 lists with_env_var and cmd_separator as TODOs (with clear at line 122 in between). Both are verified v0.x features (present in teamocil's 0.4-stable branch at lib/teamocil/layout/window.rb), not stale references:
with_env_var(line 121): Whentrue(the default in v0.x), exportsTEAMOCIL=1in each pane. Should map toenvironment: { TEAMOCIL: "1" }(tmuxp'senvironmentkey works at session level viaSession.set_environment(), L4). Implement, don't remove.clear(line 122): Already imported at line 141 but builder ignores it. libtmux hasPane.clear()(L4), so builder support is feasible.cmd_separator(line 123): Per-window string (default"; ") used to join commands beforesend-keys. Irrelevant for tmuxp since it sends commands individually. Remove TODO.
Current importer test fixtures cover ~40% of real-world config patterns. Key gaps by severity:
- v1.x teamocil string panes:
panes: ["git status"]βTypeError(importer tries"cmd" in pon string) - v1.x teamocil
commandskey:commands: [...]β silently dropped (onlycmdrecognized) - tmuxinator
rvm: Completely ignored by importer (onlyrbenvhandled) - tmuxinator
prescope bug: Tests pass because fixtures don't verify execution semantics
- YAML aliases/anchors: Real tmuxinator configs use
&defaults/*defaultsβ no test coverage - Numeric/emoji window names:
222:,true:,π©:β YAML type coercion edge cases untested - Pane title syntax:
pane_name: commanddict form β no fixtures startup_window/startup_pane: Not testedpre_tab(deprecated): Not tested- Window-level
rootwith relative paths: Not tested tmux_optionswith non--fflags: Not tested (importer bug I2)
When implementing Phase 1 import fixes, each item needs corresponding test fixtures. See tests/fixtures/import_tmuxinator/ and tests/fixtures/import_teamocil/ for existing patterns.
tmuxinator fixtures needed: YAML aliases, emoji names, numeric names, rvm, pre_tab, startup_window/startup_pane, pane titles, socket_path, multi-flag tmux_options
teamocil fixtures needed: v1.x format (commands, string panes, window focus/options), pane height, with_env_var, mixed v0.x/v1.x detection
These fix existing bugs and add missing translations without touching the builder:
- I3: Fix redundant filter loops (teamocil)
- I4: Add v1.x teamocil format support
- I6: Import teamocil v1.x keys (
commands,focus,options, string panes) - I5: Import missing tmuxinator keys (
rvm,pre_tab,startup_window,startup_pane) - I1: Fix
pre/pre_windowmapping (tmuxinator) - I2: Fix
cli_argsparsing (tmuxinator) - I7: Triage importer TODOs (implement
with_env_var, removecmd_separator)
These add new config key handling to the builder. Each also needs a corresponding importer update:
- T1:
synchronizeconfig key β straightforwardset_option()call- Then update tmuxinator importer to import
synchronizekey (pass-through, same name)
- Then update tmuxinator importer to import
- T3:
shell_command_afterconfig key β straightforwardsend_keys()loop- teamocil importer already produces this key (I3 fixes the loop); builder just needs to read it
- T4:
--hereCLI flag β moderate complexity, uses existing libtmux APIs
These require changes to the libtmux package:
- L1:
Pane.set_title()β simple wrapper, needed for T2 - T2: Pane title config keys β depends on L1
- Then update tmuxinator importer to import
enable_pane_titles,pane_title_position,pane_title_format, and named pane syntax (pane_name: commandβtitle+shell_command)
- Then update tmuxinator importer to import
- T5:
tmuxp stopcommand - T10:
tmuxp new,tmuxp copy,tmuxp deletecommands
- T6: Lifecycle hook config keys β complex, needs design
- T7:
--no-shell-command-beforeflag β simple - T8: Config templating β significant architectural addition
- L3: Pre-execution command logging in libtmux β prerequisite for T9
- T9:
--debug/ dry-run mode β depends on L3 - L2: Custom tmux binary β requires libtmux changes