From 5e9a58d1d03f004ac70263b1f7a103c73bf317ca Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Sat, 21 Mar 2026 02:22:59 -0700 Subject: [PATCH] Sync CMUX environment through tmux automatically --- .../cmux-bash-integration.bash | 109 ++++++++++++++++++ .../cmux-zsh-integration.zsh | 108 +++++++++++++++++ 2 files changed, 217 insertions(+) diff --git a/Resources/shell-integration/cmux-bash-integration.bash b/Resources/shell-integration/cmux-bash-integration.bash index fe66dbfa..1ab8f258 100644 --- a/Resources/shell-integration/cmux-bash-integration.bash +++ b/Resources/shell-integration/cmux-bash-integration.bash @@ -54,6 +54,111 @@ _CMUX_PORTS_LAST_RUN="${_CMUX_PORTS_LAST_RUN:-0}" _CMUX_SHELL_ACTIVITY_LAST="${_CMUX_SHELL_ACTIVITY_LAST:-}" _CMUX_TTY_NAME="${_CMUX_TTY_NAME:-}" _CMUX_TTY_REPORTED="${_CMUX_TTY_REPORTED:-0}" +_CMUX_TMUX_PUSH_SIGNATURE="${_CMUX_TMUX_PUSH_SIGNATURE:-}" +_CMUX_TMUX_PULL_SIGNATURE="${_CMUX_TMUX_PULL_SIGNATURE:-}" +_CMUX_TMUX_SYNC_KEYS=( + CMUX_BUNDLED_CLI_PATH + CMUX_BUNDLE_ID + CMUXD_UNIX_PATH + CMUXTERM_REPO_ROOT + CMUX_DEBUG_LOG + CMUX_LOAD_GHOSTTY_ZSH_INTEGRATION + CMUX_PANEL_ID + CMUX_PORT + CMUX_PORT_END + CMUX_PORT_RANGE + CMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD + CMUX_SHELL_INTEGRATION + CMUX_SHELL_INTEGRATION_DIR + CMUX_SOCKET_ENABLE + CMUX_SOCKET_MODE + CMUX_SOCKET_PATH + CMUX_SURFACE_ID + CMUX_TAB_ID + CMUX_TAG + CMUX_WORKSPACE_ID +) + +_cmux_tmux_shell_env_signature() { + local key value first=1 + for key in "${_CMUX_TMUX_SYNC_KEYS[@]}"; do + value="${!key}" + [[ -n "$value" ]] || continue + if (( first )); then + printf '%s=%s' "$key" "$value" + first=0 + else + printf '\037%s=%s' "$key" "$value" + fi + done +} + +_cmux_tmux_publish_cmux_environment() { + [[ -z "$TMUX" ]] || return 0 + command -v tmux >/dev/null 2>&1 || return 0 + + local signature + signature="$(_cmux_tmux_shell_env_signature)" + [[ -n "$signature" ]] || return 0 + [[ "$signature" == "$_CMUX_TMUX_PUSH_SIGNATURE" ]] && return 0 + + local key value + for key in "${_CMUX_TMUX_SYNC_KEYS[@]}"; do + value="${!key}" + [[ -n "$value" ]] || continue + tmux set-environment -g "$key" "$value" >/dev/null 2>&1 || return 0 + done + + _CMUX_TMUX_PUSH_SIGNATURE="$signature" +} + +_cmux_tmux_refresh_cmux_environment() { + [[ -n "$TMUX" ]] || return 0 + command -v tmux >/dev/null 2>&1 || return 0 + + local output filtered line key value did_change=0 + output="$(tmux show-environment -g 2>/dev/null)" || return 0 + + while IFS= read -r line; do + [[ "$line" == CMUX_* ]] || continue + filtered+="${line}"$'\n' + done <<< "$output" + + [[ -n "$filtered" ]] || return 0 + [[ "$filtered" == "$_CMUX_TMUX_PULL_SIGNATURE" ]] && return 0 + + while IFS= read -r line; do + [[ "$line" == CMUX_* ]] || continue + key="${line%%=*}" + value="${line#*=}" + if [[ "${!key}" != "$value" ]]; then + printf -v "$key" '%s' "$value" + export "$key" + did_change=1 + fi + done <<< "$filtered" + + _CMUX_TMUX_PULL_SIGNATURE="$filtered" + if (( did_change )); then + _CMUX_TTY_REPORTED=0 + _CMUX_SHELL_ACTIVITY_LAST="" + _CMUX_PWD_LAST_PWD="" + _CMUX_GIT_LAST_PWD="" + _CMUX_GIT_HEAD_LAST_PWD="" + _CMUX_GIT_HEAD_PATH="" + _CMUX_GIT_HEAD_SIGNATURE="" + _CMUX_PR_FORCE=1 + _cmux_stop_pr_poll_loop + fi +} + +_cmux_tmux_sync_cmux_environment() { + if [[ -n "$TMUX" ]]; then + _cmux_tmux_refresh_cmux_environment + else + _cmux_tmux_publish_cmux_environment + fi +} _cmux_git_resolve_head_path() { # Resolve the HEAD file path without invoking git (fast; works for worktrees). @@ -348,6 +453,8 @@ _cmux_bash_cleanup() { } _cmux_preexec_command() { + _cmux_tmux_sync_cmux_environment + [[ -S "$CMUX_SOCKET_PATH" ]] || return 0 [[ -n "$CMUX_TAB_ID" ]] || return 0 [[ -n "$CMUX_PANEL_ID" ]] || return 0 @@ -370,6 +477,8 @@ _cmux_bash_preexec_hook() { } _cmux_prompt_command() { + _cmux_tmux_sync_cmux_environment + [[ -S "$CMUX_SOCKET_PATH" ]] || return 0 [[ -n "$CMUX_TAB_ID" ]] || return 0 [[ -n "$CMUX_PANEL_ID" ]] || return 0 diff --git a/Resources/shell-integration/cmux-zsh-integration.zsh b/Resources/shell-integration/cmux-zsh-integration.zsh index b468984c..c384ba92 100644 --- a/Resources/shell-integration/cmux-zsh-integration.zsh +++ b/Resources/shell-integration/cmux-zsh-integration.zsh @@ -60,6 +60,111 @@ typeset -g _CMUX_TTY_NAME="" typeset -g _CMUX_TTY_REPORTED=0 typeset -g _CMUX_GHOSTTY_SEMANTIC_PATCHED=0 typeset -g _CMUX_WINCH_GUARD_INSTALLED=0 +typeset -g _CMUX_TMUX_PUSH_SIGNATURE="" +typeset -g _CMUX_TMUX_PULL_SIGNATURE="" +typeset -ga _CMUX_TMUX_SYNC_KEYS=( + CMUX_BUNDLED_CLI_PATH + CMUX_BUNDLE_ID + CMUXD_UNIX_PATH + CMUXTERM_REPO_ROOT + CMUX_DEBUG_LOG + CMUX_LOAD_GHOSTTY_ZSH_INTEGRATION + CMUX_PANEL_ID + CMUX_PORT + CMUX_PORT_END + CMUX_PORT_RANGE + CMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD + CMUX_SHELL_INTEGRATION + CMUX_SHELL_INTEGRATION_DIR + CMUX_SOCKET_ENABLE + CMUX_SOCKET_MODE + CMUX_SOCKET_PATH + CMUX_SURFACE_ID + CMUX_TAB_ID + CMUX_TAG + CMUX_WORKSPACE_ID +) + +_cmux_tmux_shell_env_signature() { + local key value + local -a parts + for key in "${_CMUX_TMUX_SYNC_KEYS[@]}"; do + value="${(P)key}" + [[ -n "$value" ]] || continue + parts+=("${key}=${value}") + done + print -r -- "${(j:\x1f:)parts}" +} + +_cmux_tmux_publish_cmux_environment() { + [[ -z "$TMUX" ]] || return 0 + command -v tmux >/dev/null 2>&1 || return 0 + + local signature + signature="$(_cmux_tmux_shell_env_signature)" + [[ -n "$signature" ]] || return 0 + [[ "$signature" == "$_CMUX_TMUX_PUSH_SIGNATURE" ]] && return 0 + + local key value + for key in "${_CMUX_TMUX_SYNC_KEYS[@]}"; do + value="${(P)key}" + [[ -n "$value" ]] || continue + tmux set-environment -g "$key" "$value" >/dev/null 2>&1 || return 0 + done + + _CMUX_TMUX_PUSH_SIGNATURE="$signature" +} + +_cmux_tmux_refresh_cmux_environment() { + [[ -n "$TMUX" ]] || return 0 + command -v tmux >/dev/null 2>&1 || return 0 + + local output + output="$(tmux show-environment -g 2>/dev/null)" || return 0 + + local line filtered="" did_change=0 + while IFS= read -r line; do + [[ "$line" == CMUX_* ]] || continue + filtered+="${line}"$'\n' + done <<< "$output" + + [[ -n "$filtered" ]] || return 0 + [[ "$filtered" == "$_CMUX_TMUX_PULL_SIGNATURE" ]] && return 0 + + local key value + while IFS= read -r line; do + [[ "$line" == CMUX_* ]] || continue + key="${line%%=*}" + value="${line#*=}" + if [[ "${(P)key}" != "$value" ]]; then + export "$key=$value" + did_change=1 + fi + done <<< "$filtered" + + _CMUX_TMUX_PULL_SIGNATURE="$filtered" + if (( did_change )); then + _CMUX_TTY_REPORTED=0 + _CMUX_SHELL_ACTIVITY_LAST="" + _CMUX_PWD_LAST_PWD="" + _CMUX_GIT_LAST_PWD="" + _CMUX_GIT_HEAD_LAST_PWD="" + _CMUX_GIT_HEAD_PATH="" + _CMUX_GIT_HEAD_SIGNATURE="" + _CMUX_GIT_FORCE=1 + _CMUX_PR_FORCE=1 + _cmux_stop_pr_poll_loop + _cmux_stop_git_head_watch + fi +} + +_cmux_tmux_sync_cmux_environment() { + if [[ -n "$TMUX" ]]; then + _cmux_tmux_refresh_cmux_environment + else + _cmux_tmux_publish_cmux_environment + fi +} _cmux_ensure_ghostty_preexec_strips_both_marks() { local fn_name="$1" @@ -506,6 +611,8 @@ _cmux_start_git_head_watch() { } _cmux_preexec() { + _cmux_tmux_sync_cmux_environment + if [[ -z "$_CMUX_TTY_NAME" ]]; then local t t="$(tty 2>/dev/null || true)" @@ -533,6 +640,7 @@ _cmux_preexec() { _cmux_precmd() { _cmux_stop_git_head_watch + _cmux_tmux_sync_cmux_environment # Skip if socket doesn't exist yet [[ -S "$CMUX_SOCKET_PATH" ]] || return 0