Last active
March 13, 2026 09:32
-
-
Save YukiMatsumura/410c35d6a3c5a6887e915ec8af04f1ad to your computer and use it in GitHub Desktop.
difit-cmux
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================= | |
| # difit-auto-detect.sh | |
| # Automatically detects git state and opens appropriate diff view | |
| # with difit in a cmux browser split pane. | |
| # | |
| # Dependencies: | |
| # - cmux https://cmux.app/ | |
| # - difit https://github.com/yoshiko-pg/difit | |
| # | |
| # Setup: | |
| # 1. Install cmux | |
| # Download from https://cmux.app/ and move to /Applications/ | |
| # | |
| # 2. Install difit | |
| # npm install -g difit | |
| # | |
| # 3. Make this script executable and place it somewhere on your PATH | |
| # chmod +x difit-auto-detect.sh | |
| # # Example: mv difit-auto-detect.sh /usr/local/bin/difit-cmux | |
| # | |
| # 4. (Optional) Bind to a global shortcut via your preferred launcher | |
| # e.g. Alfred, cmux shortcuts, Raycast, etc. | |
| # | |
| # Usage: | |
| # Run this script while cmux is focused on a git repository pane. | |
| # The script will: | |
| # - Detect the current branch and default branch | |
| # - Show diff against merge-base (for feature branches) | |
| # - Show uncommitted diff (for default branch or detached HEAD) | |
| # - Open the result in a cmux browser split at http://127.0.0.1:<port>/ | |
| # | |
| # Optional env vars: | |
| # - DIFIT_PORT: preferred starting port (default: 4966) | |
| # - DIFIT_CLEAN=1: pass --clean to difit | |
| # - DIFIT_TTL_SEC: keep difit alive for N seconds, then auto-stop (default: 1800) | |
| # - DIFIT_EXIT_DELAY_SEC: seconds before stop when TTL is not set (default: 2) | |
| # ============================================================================= | |
| set -euo pipefail | |
| CMUX="/Applications/cmux.app/Contents/Resources/bin/cmux" | |
| PREFERRED_PORT="${DIFIT_PORT:-4966}" | |
| TTL_SEC="${DIFIT_TTL_SEC:-1800}" | |
| EXIT_DELAY_SEC="${DIFIT_EXIT_DELAY_SEC:-2}" | |
| find_available_port() { | |
| local port="$1" | |
| while lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; do | |
| port=$((port + 1)) | |
| done | |
| echo "$port" | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Get the working directory of the currently focused cmux pane | |
| # --------------------------------------------------------------------------- | |
| targetDir=$("$CMUX" sidebar-state 2>/dev/null | awk -F= '/^focused_cwd=/{print $2}' || true) | |
| if [[ -z "$targetDir" ]]; then | |
| echo " Note: cmux sidebar-state unavailable; using current directory" | |
| targetDir="$(pwd)" | |
| fi | |
| cd "$targetDir" || exit 1 | |
| echo "Start Difit in cmux for git repository at: $targetDir" | |
| if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then | |
| echo "Error: $targetDir is not a git repository" >&2 | |
| exit 1 | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # Detect current branch and default remote branch | |
| # --------------------------------------------------------------------------- | |
| currentBranch=$(git rev-parse --abbrev-ref HEAD) | |
| defaultBranch="" | |
| # Try to resolve origin/HEAD first, then fall back to origin/main or origin/master | |
| if git remote get-url origin >/dev/null 2>&1; then | |
| defaultBranch=$(git symbolic-ref refs/remotes/origin/HEAD --short 2>/dev/null || true) | |
| if [[ -z "$defaultBranch" ]]; then | |
| if git rev-parse --verify origin/main >/dev/null 2>&1; then | |
| defaultBranch="origin/main" | |
| elif git rev-parse --verify origin/master >/dev/null 2>&1; then | |
| defaultBranch="origin/master" | |
| fi | |
| fi | |
| else | |
| echo " Note: origin remote is not configured; fallback to uncommitted diff mode" | |
| fi | |
| echo " Current branch: $currentBranch (default branch: ${defaultBranch:-none})" | |
| # --------------------------------------------------------------------------- | |
| # Ensure the difit server process is cleaned up on exit | |
| # --------------------------------------------------------------------------- | |
| DIFIT_PID="" | |
| cleanup() { | |
| if [[ -n "$DIFIT_PID" ]]; then | |
| echo "Stopping difit server..." | |
| kill "$DIFIT_PID" 2>/dev/null || true | |
| fi | |
| } | |
| trap cleanup EXIT INT TERM | |
| # --------------------------------------------------------------------------- | |
| # Start difit in background, choosing the right diff range | |
| # --------------------------------------------------------------------------- | |
| echo "Starting difit server..." | |
| port=$(find_available_port "$PREFERRED_PORT") | |
| echo " Port: $port" | |
| difitArgs=(--mode unified --no-open --port "$port" --include-untracked) | |
| if [[ "${DIFIT_CLEAN:-0}" == "1" ]]; then | |
| echo " Option: --clean enabled via DIFIT_CLEAN=1" | |
| difitArgs+=(--clean) | |
| fi | |
| if [[ -z "$defaultBranch" ]] || [[ "$currentBranch" == "${defaultBranch#origin/}" ]]; then | |
| # On default branch, detached HEAD, or no remote found | |
| echo " Mode: uncommitted diff (default branch, detached HEAD, or no remote)" | |
| difit . "${difitArgs[@]}" & | |
| else | |
| # On a feature branch — diff from the merge-base with the default branch | |
| mergeBase=$(git merge-base HEAD "$defaultBranch" 2>/dev/null || true) | |
| if [[ -z "$mergeBase" ]]; then | |
| echo " Mode: could not find merge-base; falling back to uncommitted diff" | |
| difit . "${difitArgs[@]}" & | |
| else | |
| echo " Mode: branch diff from merge-base ($currentBranch vs ${defaultBranch#origin/})" | |
| difit . "$mergeBase" "${difitArgs[@]}" & | |
| fi | |
| fi | |
| DIFIT_PID=$! | |
| # Poll until the difit HTTP server is ready | |
| echo " Waiting for difit server to be ready..." | |
| for _ in {1..120}; do | |
| if curl -fsS "http://localhost:${port}/" > /dev/null 2>&1; then | |
| break | |
| fi | |
| sleep 0.5 | |
| done | |
| if ! curl -fsS "http://localhost:${port}/" > /dev/null 2>&1; then | |
| echo "Error: difit server did not become ready within timeout" >&2 | |
| exit 1 | |
| fi | |
| # Open the diff in a cmux browser pane, falling back to default browser | |
| open_in_cmux() { | |
| local ws | |
| ws=$("$CMUX" identify --no-caller 2>/dev/null | grep workspace_ref | head -1 | sed 's/.*: "//;s/".*//') | |
| if [[ -n "$ws" ]]; then | |
| "$CMUX" new-pane --type browser --workspace "$ws" --url "http://localhost:${port}/" 2>/dev/null | |
| else | |
| return 1 | |
| fi | |
| } | |
| if ! open_in_cmux; then | |
| echo " Note: cmux browser pane unavailable; opening in default browser" | |
| open "http://localhost:${port}/" | |
| fi | |
| if [[ -n "$TTL_SEC" ]]; then | |
| if [[ ! "$TTL_SEC" =~ ^[0-9]+$ ]]; then | |
| echo "Error: DIFIT_TTL_SEC must be a non-negative integer" >&2 | |
| exit 1 | |
| fi | |
| echo " Auto-stop: keep alive for ${TTL_SEC}s" | |
| # Detach auto-stop so this script can exit without killing difit immediately. | |
| nohup sh -c "sleep '$TTL_SEC'; kill '$DIFIT_PID' 2>/dev/null || true" >/dev/null 2>&1 & | |
| DIFIT_PID="" | |
| else | |
| if [[ ! "$EXIT_DELAY_SEC" =~ ^[0-9]+$ ]]; then | |
| echo "Error: DIFIT_EXIT_DELAY_SEC must be a non-negative integer" >&2 | |
| exit 1 | |
| fi | |
| # Give the browser enough time to load the page before shutting down the server. | |
| sleep "$EXIT_DELAY_SEC" | |
| kill "$DIFIT_PID" 2>/dev/null || true | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment