set +m only suppresses notifications for jobs started after it runs. Jobs that complete between prompts still trigger Done output. Using disown removes jobs from bash job table entirely so bash never prints completion notifications for them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
262 lines
9.9 KiB
Bash
262 lines
9.9 KiB
Bash
# cmux shell integration for bash
|
|
|
|
_cmux_send() {
|
|
local payload="$1"
|
|
if command -v ncat >/dev/null 2>&1; then
|
|
printf '%s\n' "$payload" | ncat -w 1 -U "$CMUX_SOCKET_PATH" --send-only
|
|
elif command -v socat >/dev/null 2>&1; then
|
|
printf '%s\n' "$payload" | socat -T 1 - "UNIX-CONNECT:$CMUX_SOCKET_PATH"
|
|
elif command -v nc >/dev/null 2>&1; then
|
|
# Some nc builds don't support unix sockets, but keep as a last-ditch fallback.
|
|
#
|
|
# Important: macOS/BSD nc will often wait for the peer to close the socket
|
|
# after it has finished writing. cmux keeps the connection open, so
|
|
# a plain `nc -U` can hang indefinitely and leak background processes.
|
|
#
|
|
# Prefer flags that guarantee we exit after sending, and fall back to a
|
|
# short timeout so we never block sidebar updates.
|
|
if printf '%s\n' "$payload" | nc -N -U "$CMUX_SOCKET_PATH" >/dev/null 2>&1; then
|
|
:
|
|
else
|
|
printf '%s\n' "$payload" | nc -w 1 -U "$CMUX_SOCKET_PATH" >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
_cmux_restore_scrollback_once() {
|
|
local path="${CMUX_RESTORE_SCROLLBACK_FILE:-}"
|
|
[[ -n "$path" ]] || return 0
|
|
unset CMUX_RESTORE_SCROLLBACK_FILE
|
|
|
|
if [[ -r "$path" ]]; then
|
|
/bin/cat -- "$path" 2>/dev/null || true
|
|
/bin/rm -f -- "$path" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
_cmux_restore_scrollback_once
|
|
|
|
# Throttle heavy work to avoid prompt latency.
|
|
_CMUX_PWD_LAST_PWD="${_CMUX_PWD_LAST_PWD:-}"
|
|
_CMUX_GIT_LAST_PWD="${_CMUX_GIT_LAST_PWD:-}"
|
|
_CMUX_GIT_LAST_RUN="${_CMUX_GIT_LAST_RUN:-0}"
|
|
_CMUX_GIT_JOB_PID="${_CMUX_GIT_JOB_PID:-}"
|
|
_CMUX_GIT_JOB_STARTED_AT="${_CMUX_GIT_JOB_STARTED_AT:-0}"
|
|
_CMUX_PR_LAST_PWD="${_CMUX_PR_LAST_PWD:-}"
|
|
_CMUX_PR_LAST_RUN="${_CMUX_PR_LAST_RUN:-0}"
|
|
_CMUX_PR_JOB_PID="${_CMUX_PR_JOB_PID:-}"
|
|
_CMUX_PR_JOB_STARTED_AT="${_CMUX_PR_JOB_STARTED_AT:-0}"
|
|
_CMUX_ASYNC_JOB_TIMEOUT="${_CMUX_ASYNC_JOB_TIMEOUT:-20}"
|
|
|
|
_CMUX_PORTS_LAST_RUN="${_CMUX_PORTS_LAST_RUN:-0}"
|
|
_CMUX_TTY_NAME="${_CMUX_TTY_NAME:-}"
|
|
_CMUX_TTY_REPORTED="${_CMUX_TTY_REPORTED:-0}"
|
|
|
|
_cmux_report_tty_once() {
|
|
# Send the TTY name to the app once per session so the batched port scanner
|
|
# knows which TTY belongs to this panel.
|
|
(( _CMUX_TTY_REPORTED )) && return 0
|
|
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
|
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
|
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
|
[[ -n "$_CMUX_TTY_NAME" ]] || return 0
|
|
_CMUX_TTY_REPORTED=1
|
|
{
|
|
_cmux_send "report_tty $_CMUX_TTY_NAME --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
|
} >/dev/null 2>&1 & disown
|
|
}
|
|
|
|
_cmux_ports_kick() {
|
|
# Lightweight: just tell the app to run a batched scan for this panel.
|
|
# The app coalesces kicks across all panels and runs a single ps+lsof.
|
|
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
|
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
|
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
|
_CMUX_PORTS_LAST_RUN=$SECONDS
|
|
{
|
|
_cmux_send "ports_kick --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
|
} >/dev/null 2>&1 & disown
|
|
}
|
|
|
|
_cmux_prompt_command() {
|
|
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
|
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
|
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
|
|
|
local now=$SECONDS
|
|
local pwd="$PWD"
|
|
|
|
# Post-wake socket writes can occasionally leave a probe process wedged.
|
|
# If one probe is stale, clear the guard so fresh async probes can resume.
|
|
if [[ -n "$_CMUX_GIT_JOB_PID" ]]; then
|
|
if ! kill -0 "$_CMUX_GIT_JOB_PID" 2>/dev/null; then
|
|
_CMUX_GIT_JOB_PID=""
|
|
_CMUX_GIT_JOB_STARTED_AT=0
|
|
elif (( _CMUX_GIT_JOB_STARTED_AT > 0 )) && (( now - _CMUX_GIT_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT )); then
|
|
_CMUX_GIT_JOB_PID=""
|
|
_CMUX_GIT_JOB_STARTED_AT=0
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$_CMUX_PR_JOB_PID" ]]; then
|
|
if ! kill -0 "$_CMUX_PR_JOB_PID" 2>/dev/null; then
|
|
_CMUX_PR_JOB_PID=""
|
|
_CMUX_PR_JOB_STARTED_AT=0
|
|
elif (( _CMUX_PR_JOB_STARTED_AT > 0 )) && (( now - _CMUX_PR_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT )); then
|
|
_CMUX_PR_JOB_PID=""
|
|
_CMUX_PR_JOB_STARTED_AT=0
|
|
fi
|
|
fi
|
|
|
|
# Resolve TTY name once.
|
|
if [[ -z "$_CMUX_TTY_NAME" ]]; then
|
|
local t
|
|
t="$(tty 2>/dev/null || true)"
|
|
t="${t##*/}"
|
|
[[ "$t" != "not a tty" ]] && _CMUX_TTY_NAME="$t"
|
|
fi
|
|
|
|
_cmux_report_tty_once
|
|
|
|
# CWD: keep the app in sync with the actual shell directory.
|
|
if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then
|
|
_CMUX_PWD_LAST_PWD="$pwd"
|
|
{
|
|
local qpwd="${pwd//\"/\\\"}"
|
|
_cmux_send "report_pwd \"${qpwd}\" --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
|
} >/dev/null 2>&1 & disown
|
|
fi
|
|
|
|
# Git branch/dirty can change without a directory change (e.g. `git checkout`),
|
|
# so update on every prompt (still async + de-duped by the running-job check).
|
|
# When pwd changes (cd into a different repo), kill the old probe and start fresh
|
|
# so the sidebar picks up the new branch immediately.
|
|
if [[ -n "$_CMUX_GIT_JOB_PID" ]] && kill -0 "$_CMUX_GIT_JOB_PID" 2>/dev/null; then
|
|
if [[ "$pwd" != "$_CMUX_GIT_LAST_PWD" ]]; then
|
|
kill "$_CMUX_GIT_JOB_PID" >/dev/null 2>&1 || true
|
|
_CMUX_GIT_JOB_PID=""
|
|
_CMUX_GIT_JOB_STARTED_AT=0
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$_CMUX_GIT_JOB_PID" ]] || ! kill -0 "$_CMUX_GIT_JOB_PID" 2>/dev/null; then
|
|
_CMUX_GIT_LAST_PWD="$pwd"
|
|
_CMUX_GIT_LAST_RUN=$now
|
|
{
|
|
local branch dirty_opt=""
|
|
branch=$(git branch --show-current 2>/dev/null)
|
|
if [[ -n "$branch" ]]; then
|
|
local first
|
|
first=$(git status --porcelain -uno 2>/dev/null | head -1)
|
|
[[ -n "$first" ]] && dirty_opt="--status=dirty"
|
|
_cmux_send "report_git_branch $branch $dirty_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
|
else
|
|
_cmux_send "clear_git_branch --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
|
fi
|
|
} >/dev/null 2>&1 &
|
|
_CMUX_GIT_JOB_PID=$!
|
|
disown
|
|
_CMUX_GIT_JOB_STARTED_AT=$now
|
|
fi
|
|
|
|
# Pull request metadata (number/state/url):
|
|
# refresh on cwd change and periodically to avoid stale status.
|
|
if [[ -n "$_CMUX_PR_JOB_PID" ]] && kill -0 "$_CMUX_PR_JOB_PID" 2>/dev/null; then
|
|
if [[ "$pwd" != "$_CMUX_PR_LAST_PWD" ]]; then
|
|
kill "$_CMUX_PR_JOB_PID" >/dev/null 2>&1 || true
|
|
_CMUX_PR_JOB_PID=""
|
|
_CMUX_PR_JOB_STARTED_AT=0
|
|
fi
|
|
fi
|
|
|
|
if [[ "$pwd" != "$_CMUX_PR_LAST_PWD" ]] || (( now - _CMUX_PR_LAST_RUN >= 60 )); then
|
|
if [[ -z "$_CMUX_PR_JOB_PID" ]] || ! kill -0 "$_CMUX_PR_JOB_PID" 2>/dev/null; then
|
|
_CMUX_PR_LAST_PWD="$pwd"
|
|
_CMUX_PR_LAST_RUN=$now
|
|
{
|
|
local branch pr_tsv number state url status_opt=""
|
|
branch=$(git branch --show-current 2>/dev/null)
|
|
if [[ -z "$branch" ]] || ! command -v gh >/dev/null 2>&1; then
|
|
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
|
else
|
|
pr_tsv="$(gh pr view --json number,state,url --jq '[.number, .state, .url] | @tsv' 2>/dev/null || true)"
|
|
if [[ -z "$pr_tsv" ]]; then
|
|
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
|
else
|
|
IFS=$'\t' read -r number state url <<< "$pr_tsv"
|
|
if [[ -z "$number" || -z "$url" ]]; then
|
|
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
|
else
|
|
case "$state" in
|
|
MERGED) status_opt="--state=merged" ;;
|
|
OPEN) status_opt="--state=open" ;;
|
|
CLOSED) status_opt="--state=closed" ;;
|
|
*) status_opt="" ;;
|
|
esac
|
|
_cmux_send "report_pr $number $url $status_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
|
fi
|
|
fi
|
|
fi
|
|
} >/dev/null 2>&1 &
|
|
_CMUX_PR_JOB_PID=$!
|
|
disown
|
|
_CMUX_PR_JOB_STARTED_AT=$now
|
|
fi
|
|
fi
|
|
|
|
# Ports: lightweight kick to the app's batched scanner every ~10s.
|
|
if (( now - _CMUX_PORTS_LAST_RUN >= 10 )); then
|
|
_cmux_ports_kick
|
|
fi
|
|
|
|
}
|
|
|
|
_cmux_install_prompt_command() {
|
|
[[ -n "${_CMUX_PROMPT_INSTALLED:-}" ]] && return 0
|
|
_CMUX_PROMPT_INSTALLED=1
|
|
|
|
local decl
|
|
decl="$(declare -p PROMPT_COMMAND 2>/dev/null || true)"
|
|
if [[ "$decl" == "declare -a"* ]]; then
|
|
local existing=0
|
|
local item
|
|
for item in "${PROMPT_COMMAND[@]}"; do
|
|
[[ "$item" == "_cmux_prompt_command" ]] && existing=1 && break
|
|
done
|
|
if (( existing == 0 )); then
|
|
PROMPT_COMMAND=("_cmux_prompt_command" "${PROMPT_COMMAND[@]}")
|
|
fi
|
|
else
|
|
case ";$PROMPT_COMMAND;" in
|
|
*";_cmux_prompt_command;"*) ;;
|
|
*)
|
|
if [[ -n "$PROMPT_COMMAND" ]]; then
|
|
PROMPT_COMMAND="_cmux_prompt_command;$PROMPT_COMMAND"
|
|
else
|
|
PROMPT_COMMAND="_cmux_prompt_command"
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
}
|
|
|
|
# Ensure Resources/bin is at the front of PATH, and remove the app's
|
|
# Contents/MacOS entry so the GUI cmux binary cannot shadow the CLI cmux.
|
|
# Shell init (.bashrc/.bash_profile) may prepend other dirs after launch.
|
|
_cmux_fix_path() {
|
|
if [[ -n "${GHOSTTY_BIN_DIR:-}" ]]; then
|
|
local gui_dir="${GHOSTTY_BIN_DIR%/}"
|
|
local bin_dir="${gui_dir%/MacOS}/Resources/bin"
|
|
if [[ -d "$bin_dir" ]]; then
|
|
local new_path=":${PATH}:"
|
|
new_path="${new_path//:${bin_dir}:/:}"
|
|
new_path="${new_path//:${gui_dir}:/:}"
|
|
new_path="${new_path#:}"
|
|
new_path="${new_path%:}"
|
|
PATH="${bin_dir}:${new_path}"
|
|
fi
|
|
fi
|
|
}
|
|
_cmux_fix_path
|
|
unset -f _cmux_fix_path
|
|
|
|
_cmux_install_prompt_command
|