diff --git a/bin/cmuxup b/bin/cmuxup index 3ca9cf0..18f9916 100755 --- a/bin/cmuxup +++ b/bin/cmuxup @@ -8,24 +8,33 @@ VERSION="0.1.0" # Builds a terminal-first agentic IDE layout in cmux via its control socket. # No AppleScript, no keystroke timing — pure cmux socket API. # -# Layout: +# Layout adapts to which tools are configured: +# +# Both lazygit + editor: +# ┌──────────────┬───────────────────────┐ +# │ │ [lazygit] [editor] │ right-top pane — two tabs +# │ agent ├───────────────────────┤ +# │ │ dev terminal │ right-bottom pane +# └──────────────┴───────────────────────┘ +# +# One of lazygit / editor (or neither): # ┌──────────────┬───────────────────────┐ -# │ │ [lazygit] [helix] │ right-top pane — two tabs -# │ claude ├───────────────────────┤ +# │ │ lazygit / editor │ right-top pane — single tab +# │ agent ├───────────────────────┤ # │ │ dev terminal │ right-bottom pane # └──────────────┴───────────────────────┘ # # Env overrides: # CMUXUP_MAIN_CMD default: claude -# CMUXUP_LG_CMD default: lazygit -# CMUXUP_HX_CMD default: hx . +# CMUXUP_LG_CMD default: lazygit (set to "" to disable) +# CMUXUP_HX_CMD default: hx . (set to "" to disable) # CMUXUP_DEV_CMD default: (empty — just cd's into project) usage() { cat </dev/null } -# 1. Create workspace; main pane launches the agent command. +# 1. Create workspace; main pane launches the agent. WS="$(cx new-workspace --name "$NAME" --cwd "$PROJECT_DIR" --command "$MAIN_CMD" --focus true | _ref workspace)" [ -n "$WS" ] || { echo "cmuxup: failed to create cmux workspace" >&2; exit 1; } -# 2. Identify the initial surface (the only one that exists right after creation). +# 2. Identify the initial surface. MAIN_SURFACE="$(cx tree --workspace "$WS" | _ref surface)" -# 3. Split right → right-top pane (lazygit tab lives here). -LG_SURFACE="$(cx new-split right --surface "$MAIN_SURFACE" --workspace "$WS" --focus false | _ref surface)" -RT_PANE="$(_pane_of_surface "$WS" "$LG_SURFACE")" - -# 4. Split down from the right-top surface → right-bottom dev terminal. -DEV_SURFACE="$(cx new-split down --surface "$LG_SURFACE" --workspace "$WS" --focus false | _ref surface)" - -# 5. Add a second tab to the right-top pane → helix. -HX_SURFACE="$(cx new-surface --type terminal --pane "$RT_PANE" --workspace "$WS" --focus false | _ref surface)" - -# 6. Name the two tabs. -cx rename-tab --surface "$LG_SURFACE" --workspace "$WS" "lazygit" >/dev/null 2>&1 || true -cx rename-tab --surface "$HX_SURFACE" --workspace "$WS" "helix" >/dev/null 2>&1 || true +# 3. Split right → right-top pane. +RT_SURFACE="$(cx new-split right --surface "$MAIN_SURFACE" --workspace "$WS" --focus false | _ref surface)" +RT_PANE="$(_pane_of_surface "$WS" "$RT_SURFACE")" + +# 4. Split down from right-top → right-bottom dev terminal. +DEV_SURFACE="$(cx new-split down --surface "$RT_SURFACE" --workspace "$WS" --focus false | _ref surface)" + +# 5. Decide which tools go into the right-top pane. +# If both LG_CMD and HX_CMD are set → two tabs (lazygit first, editor second). +# If only one → that tool runs in the single right-top tab. +# If neither → plain terminal (dev work only). + +RT_LABEL="terminal" +SECOND_SURFACE="" +SECOND_LABEL="" + +if [ -n "$LG_CMD" ] && [ -n "$HX_CMD" ]; then + # Two tabs: lazygit + editor. + SECOND_SURFACE="$(cx new-surface --type terminal --pane "$RT_PANE" --workspace "$WS" --focus false | _ref surface)" + _run_in "$RT_SURFACE" "$WS" "cd \"$PROJECT_DIR\" && $LG_CMD" + _run_in "$SECOND_SURFACE" "$WS" "cd \"$PROJECT_DIR\" && $HX_CMD" + RT_LABEL="lazygit" + SECOND_LABEL="editor" + cx rename-tab --surface "$RT_SURFACE" --workspace "$WS" "lazygit" >/dev/null 2>&1 || true + cx rename-tab --surface "$SECOND_SURFACE" --workspace "$WS" "editor" >/dev/null 2>&1 || true +elif [ -n "$LG_CMD" ]; then + _run_in "$RT_SURFACE" "$WS" "cd \"$PROJECT_DIR\" && $LG_CMD" + RT_LABEL="lazygit" + cx rename-tab --surface "$RT_SURFACE" --workspace "$WS" "lazygit" >/dev/null 2>&1 || true +elif [ -n "$HX_CMD" ]; then + _run_in "$RT_SURFACE" "$WS" "cd \"$PROJECT_DIR\" && $HX_CMD" + RT_LABEL="editor" + cx rename-tab --surface "$RT_SURFACE" --workspace "$WS" "editor" >/dev/null 2>&1 || true +fi -# 7. Launch tools in each pane. -_run_in "$LG_SURFACE" "$WS" "cd \"$PROJECT_DIR\" && $LG_CMD" -_run_in "$HX_SURFACE" "$WS" "cd \"$PROJECT_DIR\" && $HX_CMD" +# 6. Launch dev terminal. if [ -n "$DEV_CMD" ]; then _run_in "$DEV_SURFACE" "$WS" "cd \"$PROJECT_DIR\" && $DEV_CMD" else _run_in "$DEV_SURFACE" "$WS" "cd \"$PROJECT_DIR\"" fi -# 8. Return focus to the agent pane on the left. +# 7. Return focus to the agent pane. cx focus-panel --panel "$MAIN_SURFACE" --workspace "$WS" >/dev/null 2>&1 || true +# 8. Report layout. echo "cmuxup: workspace '$NAME' ready ($WS)" -echo " left $MAIN_CMD" -echo " right-top lazygit | helix (tabs)" -echo " right-bottom dev terminal" +printf " %-14s %s\n" "left" "$MAIN_CMD" +if [ -n "$SECOND_LABEL" ]; then + printf " %-14s %s | %s (tabs)\n" "right-top" "$RT_LABEL" "$SECOND_LABEL" +else + printf " %-14s %s\n" "right-top" "$RT_LABEL" +fi +printf " %-14s dev terminal\n" "right-bottom" diff --git a/install.sh b/install.sh index b9308ae..f995c23 100644 --- a/install.sh +++ b/install.sh @@ -6,20 +6,22 @@ set -euo pipefail # Or: bash install.sh [--help] [--dry-run] # # Non-interactive mode (for CI / testing): -# CMUXUP_NON_INTERACTIVE=1 skip all gum prompts, use env vars below -# CMUXUP_THEME one of: "Catppuccin Mocha" | "TokyoNight Storm" | "Gruvbox Dark Hard" | "Kanagawa Wave" -# CMUXUP_FONT_SIZE 13 | 14 | 15 -# CMUXUP_AGENT claude | opencode | codex | none -# CMUXUP_EDITOR helix | nvim | vim -# CMUXUP_OVERWRITE 1 = overwrite existing configs without prompting +# CMUXUP_NON_INTERACTIVE=1 +# CMUXUP_THEME "Catppuccin Mocha" | "TokyoNight Storm" | "Gruvbox Dark Hard" | "Kanagawa Wave" +# CMUXUP_FONT_SIZE 13 | 14 | 15 +# CMUXUP_AGENT claude | opencode | codex | none +# CMUXUP_LAZYGIT 1 | 0 (install lazygit) +# CMUXUP_EDITOR helix | nvim | vim | none +# CMUXUP_EXTRAS space-separated: "yazi bat fd ripgrep" +# CMUXUP_OVERWRITE 1 = overwrite existing configs without prompting VERSION="0.1.0" DRY_RUN=0 REPO="ITZSHOAIB/cmuxup" BRANCH="main" -# When piped via stdin (e.g. curl ... | bash), BASH_SOURCE[0] is unset and -# template files are unavailable. Download the repo tarball and re-exec. +# When piped via stdin (curl ... | bash), BASH_SOURCE[0] is unset and template +# files are unavailable. Download the repo tarball and re-exec from a real file. _SELF="${BASH_SOURCE[0]:-}" if [[ -z "$_SELF" || "$_SELF" == "bash" || "$_SELF" == "-bash" ]]; then _TMP="$(mktemp -d)" @@ -27,7 +29,7 @@ if [[ -z "$_SELF" || "$_SELF" == "bash" || "$_SELF" == "-bash" ]]; then echo "Downloading cmuxup..." curl -fsSL "https://github.com/${REPO}/archive/refs/heads/${BRANCH}.tar.gz" \ | tar -xz -C "$_TMP" --strip-components=1 - # Restore TTY as stdin so gum interactive prompts work after pipe exhaustion. + # Restore /dev/tty as stdin so gum interactive prompts work after pipe exhaustion. exec bash "$_TMP/install.sh" "$@" /dev/null 2>&1; then - gum confirm "Overwrite $dest?" || { _skip "$dest already exists"; return; } - else - _skip "$dest already exists (use CMUXUP_OVERWRITE=1 to overwrite)" - return - fi + _skip "$dest already exists"; return fi printf '%s\n' "$content" > "$dest" _ok "wrote $dest" } _apply_template() { # template_file dest theme font_size delta_theme helix_theme - local tpl="$1" dest="$2" theme="$3" font_size="$4" delta_theme="$5" helix_theme="$6" local content content="$(sed \ - -e "s|{{THEME}}|$theme|g" \ - -e "s|{{FONT_SIZE}}|$font_size|g" \ - -e "s|{{DELTA_THEME}}|$delta_theme|g" \ - -e "s|{{HELIX_THEME}}|$helix_theme|g" \ - "$tpl")" - _write_file "$dest" "$content" + -e "s|{{THEME}}|$3|g" \ + -e "s|{{FONT_SIZE}}|$4|g" \ + -e "s|{{DELTA_THEME}}|$5|g" \ + -e "s|{{HELIX_THEME}}|$6|g" \ + "$1")" + _write_file "$2" "$content" } -_brew_install() { # formula [binary] (binary defaults to formula name) +_brew_install() { # formula [binary] local formula="$1" binary="${2:-$1}" if command -v "$binary" >/dev/null 2>&1; then - _skip "$formula already installed" - return - fi - if [ "$DRY_RUN" -eq 1 ]; then - _dry "would brew install $formula" - return + _skip "$formula already installed"; return fi + if [ "$DRY_RUN" -eq 1 ]; then _dry "would brew install $formula"; return; fi if command -v gum >/dev/null 2>&1; then gum spin --spinner dot --title "Installing $formula..." -- brew install "$formula" else @@ -119,25 +106,22 @@ _brew_install() { # formula [binary] (binary defaults to formula name) _ok "installed $formula" } -# ── Map theme name to variants used by different tools ──────────────────────── -_theme_variants() { # theme_name → sets DELTA_THEME and HELIX_THEME +_theme_variants() { case "$1" in - "Catppuccin Mocha") DELTA_THEME="Catppuccin Mocha"; HELIX_THEME="catppuccin_mocha" ;; - "TokyoNight Storm") DELTA_THEME="TwoDark"; HELIX_THEME="dark_plus" ;; - "Gruvbox Dark Hard") DELTA_THEME="gruvbox-dark"; HELIX_THEME="gruvbox_dark_hard" ;; - "Kanagawa Wave") DELTA_THEME="Nord"; HELIX_THEME="catppuccin_mocha" ;; - *) DELTA_THEME="Catppuccin Mocha"; HELIX_THEME="catppuccin_mocha" ;; + "Catppuccin Mocha") DELTA_THEME="Catppuccin Mocha"; HELIX_THEME="catppuccin_mocha" ;; + "TokyoNight Storm") DELTA_THEME="TwoDark"; HELIX_THEME="dark_plus" ;; + "Gruvbox Dark Hard") DELTA_THEME="gruvbox-dark"; HELIX_THEME="gruvbox_dark_hard" ;; + "Kanagawa Wave") DELTA_THEME="Nord"; HELIX_THEME="catppuccin_mocha" ;; + *) DELTA_THEME="Catppuccin Mocha"; HELIX_THEME="catppuccin_mocha" ;; esac } # ── Preflight ───────────────────────────────────────────────────────────────── if ! command -v brew >/dev/null 2>&1; then - echo "Error: Homebrew is required. Install it from https://brew.sh then re-run." >&2 - exit 1 + echo "Error: Homebrew is required. Install it from https://brew.sh" >&2; exit 1 fi -# Bootstrap gum for the interactive UI (skip in dry-run or non-interactive). if [ "${CMUXUP_NON_INTERACTIVE:-0}" != "1" ] && [ "$DRY_RUN" -eq 0 ]; then if ! command -v gum >/dev/null 2>&1; then echo "Installing gum for the interactive UI..." @@ -151,91 +135,142 @@ if [ "${CMUXUP_NON_INTERACTIVE:-0}" = "1" ] || [ "$DRY_RUN" -eq 1 ]; then THEME="${CMUXUP_THEME:-Catppuccin Mocha}" FONT_SIZE="${CMUXUP_FONT_SIZE:-14}" AGENT="${CMUXUP_AGENT:-claude}" + INSTALL_LAZYGIT="${CMUXUP_LAZYGIT:-1}" EDITOR_CHOICE="${CMUXUP_EDITOR:-helix}" + IFS=' ' read -r -a EXTRAS <<< "${CMUXUP_EXTRAS:-yazi bat fd ripgrep}" else - if command -v gum >/dev/null 2>&1; then - gum style \ - --border double --border-foreground 212 \ - --padding "1 4" --margin "1 0" \ - --bold "cmuxup v${VERSION}" \ - "Terminal-first agentic workspace for cmux" - - THEME="$(gum choose --header "Choose your theme:" \ - "Catppuccin Mocha" "TokyoNight Storm" "Gruvbox Dark Hard" "Kanagawa Wave")" + gum style \ + --border double --border-foreground 212 \ + --padding "1 4" --margin "1 0" \ + --bold --foreground 212 \ + " cmuxup v${VERSION} " \ + "Terminal-first agentic workspace for cmux" - FONT_SIZE="$(gum choose --header "Font size:" "14" "13" "15")" + THEME="$(gum choose --header "Theme:" \ + "Catppuccin Mocha" "TokyoNight Storm" "Gruvbox Dark Hard" "Kanagawa Wave")" - AGENT="$(gum choose --header "AI agent for main pane:" \ - "claude" "opencode" "codex" "none")" + FONT_SIZE="$(gum choose --header "Font size:" "14" "13" "15")" - EDITOR_CHOICE="$(gum choose --header "Editor tab:" "helix" "nvim" "vim")" + AGENT="$(gum choose --header "AI agent (main pane):" \ + "claude" "opencode" "codex" "none")" - gum confirm "Ready to install with these settings?" || { echo "Aborted."; exit 0; } + if gum confirm "Install lazygit? (git TUI for the right-top pane)"; then + INSTALL_LAZYGIT=1 else - THEME="Catppuccin Mocha" - FONT_SIZE="14" - AGENT="claude" - EDITOR_CHOICE="helix" + INSTALL_LAZYGIT=0 fi + + EDITOR_CHOICE="$(gum choose --header "Editor tab (right-top pane):" \ + "helix" "nvim" "vim" "none")" + + echo "" + _log "Select optional extras (space to toggle, enter to confirm):" + EXTRAS_RAW="$(gum choose --no-limit \ + --selected="yazi,bat,fd,ripgrep" \ + --header "Optional tools:" \ + "yazi (file manager)" \ + "bat (better cat)" \ + "fd (better find)" \ + "ripgrep (better grep)" \ + || true)" + + # Extract just the tool names from "yazi (file manager)" → "yazi" + EXTRAS=() + while IFS= read -r line; do + [[ -z "$line" ]] && continue + EXTRAS+=("$(echo "$line" | awk '{print $1}')") + done <<< "$EXTRAS_RAW" + + echo "" + gum confirm "Ready to install?" || { echo "Aborted."; exit 0; } fi _theme_variants "$THEME" # ── Ask once about overwriting existing configs ─────────────────────────────── if [ "${CMUXUP_NON_INTERACTIVE:-0}" != "1" ] && [ "$DRY_RUN" -eq 0 ] && [ "${CMUXUP_OVERWRITE:-0}" != "1" ]; then - _EXISTING_CONFIGS=() + _EXISTING=() for f in "${HOME}/.config/ghostty/config" "${HOME}/.config/helix/config.toml" \ "${HOME}/Library/Application Support/lazygit/config.yml" \ - "${HOME}/.config/yazi/yazi.toml" "${HOME}/.config/starship.toml"; do - [ -f "$f" ] && _EXISTING_CONFIGS+=("$f") + "${HOME}/.config/yazi/yazi.toml"; do + [ -f "$f" ] && _EXISTING+=("$f") done - if [ "${#_EXISTING_CONFIGS[@]}" -gt 0 ]; then + if [ "${#_EXISTING[@]}" -gt 0 ]; then echo "" - _log "${#_EXISTING_CONFIGS[@]} config file(s) already exist." + _log "${#_EXISTING[@]} config file(s) already exist." if command -v gum >/dev/null 2>&1; then - gum confirm "Overwrite existing configs?" && export CMUXUP_OVERWRITE=1 || export CMUXUP_OVERWRITE=0 + gum confirm "Overwrite existing configs?" && CMUXUP_OVERWRITE=1 || CMUXUP_OVERWRITE=0 fi fi fi -# ── Install tools ───────────────────────────────────────────────────────────── +# ── Install core tools (always required) ───────────────────────────────────── echo "" -_log "Installing tools..." -_brew_install lazygit +_log "Installing core tools..." _brew_install git-delta delta -_brew_install helix hx -_brew_install yazi -_brew_install starship _brew_install zoxide -_brew_install bat -_brew_install fd -_brew_install ripgrep rg + +# ── Install optional: lazygit ───────────────────────────────────────────────── + +if [ "${INSTALL_LAZYGIT}" = "1" ]; then + _brew_install lazygit +fi + +# ── Install optional: editor ────────────────────────────────────────────────── + +case "$EDITOR_CHOICE" in + helix) _brew_install helix hx ;; + nvim) _brew_install neovim nvim ;; + vim) command -v vim >/dev/null 2>&1 || _brew_install vim ;; +esac + +# ── Install optional extras ─────────────────────────────────────────────────── + +for tool in "${EXTRAS[@]:-}"; do + case "$tool" in + yazi) _brew_install yazi ;; + bat) _brew_install bat ;; + fd) _brew_install fd ;; + ripgrep) _brew_install ripgrep rg ;; + esac +done # ── Write configs ───────────────────────────────────────────────────────────── echo "" -_log "Writing configs (theme: $THEME, font: $FONT_SIZE, agent: $AGENT, editor: $EDITOR_CHOICE)..." +_log "Writing configs..." TEMPLATES="$SCRIPT_DIR/templates" -_apply_template "$TEMPLATES/ghostty.config" "${HOME}/.config/ghostty/config" "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" -_apply_template "$TEMPLATES/helix.toml" "${HOME}/.config/helix/config.toml" "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" -_apply_template "$TEMPLATES/lazygit.yml" "${HOME}/Library/Application Support/lazygit/config.yml" "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" -_apply_template "$TEMPLATES/yazi.toml" "${HOME}/.config/yazi/yazi.toml" "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" -_apply_template "$TEMPLATES/starship.toml" "${HOME}/.config/starship.toml" "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" -_apply_template "$TEMPLATES/gitconfig-delta.ini" "${TMPDIR:-/tmp}/cmuxup-gitdelta.ini" "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" +_apply_template "$TEMPLATES/ghostty.config" \ + "${HOME}/.config/ghostty/config" \ + "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" + +if [ "$EDITOR_CHOICE" = "helix" ]; then + _apply_template "$TEMPLATES/helix.toml" \ + "${HOME}/.config/helix/config.toml" \ + "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" +fi + +if [ "${INSTALL_LAZYGIT}" = "1" ]; then + _apply_template "$TEMPLATES/lazygit.yml" \ + "${HOME}/Library/Application Support/lazygit/config.yml" \ + "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" +fi + +if echo "${EXTRAS[*]:-}" | grep -qw "yazi"; then + _apply_template "$TEMPLATES/yazi.toml" \ + "${HOME}/.config/yazi/yazi.toml" \ + "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" +fi + +# Wire delta into git. +_apply_template "$TEMPLATES/gitconfig-delta.ini" \ + "${TMPDIR:-/tmp}/cmuxup-gitdelta.ini" \ + "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" -# Merge delta git config (non-destructive: only sets keys not already present). if [ "$DRY_RUN" -eq 0 ]; then - while IFS= read -r line; do - [[ "$line" =~ ^\[.*\]$ || -z "$line" ]] && continue - key="$(echo "$line" | sed 's/ *= *.*//' | xargs)" - val="$(echo "$line" | sed 's/.*= *//' | xargs)" - section="" - # We'll use git config --global directly. - true - done < "${TMPDIR:-/tmp}/cmuxup-gitdelta.ini" git config --global core.pager "delta" git config --global interactive.diffFilter "delta --color-only" git config --global delta.navigate true @@ -249,12 +284,13 @@ else _dry "would configure git delta" fi -# Write cmux.json settings if not already customized. +# cmux.json settings. CMUX_JSON="${HOME}/.config/cmux/cmux.json" if [ -f "$CMUX_JSON" ] && grep -q '"diffViewer"' "$CMUX_JSON" 2>/dev/null; then _skip "cmux.json already has cmuxup settings" else - _apply_template "$TEMPLATES/cmux-settings.jsonc" "$CMUX_JSON" "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" + _apply_template "$TEMPLATES/cmux-settings.jsonc" "$CMUX_JSON" \ + "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" fi # ── Install cmuxup command ──────────────────────────────────────────────────── @@ -273,26 +309,55 @@ fi # ── Shell integration ───────────────────────────────────────────────────────── -ZSHRC="${HOME}/.zshrc" -SHELL_BLOCK=' -# ── cmuxup shell integration ────────────────────────────────────────── -export EDITOR="hx" -export VISUAL="hx" -eval "$(zoxide init zsh)" -eval "$(starship init zsh)" -function y() { - local tmp cwd - tmp="$(mktemp -t yazi-cwd.XXXXXX)" - yazi "$@" --cwd-file="$tmp" - if cwd="$(command cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then - builtin cd -- "$cwd" - fi - rm -f -- "$tmp" -} -alias lg="lazygit" -alias e="hx" -# ── end cmuxup ─────────────────────────────────────────────────────────' +# Resolve editor binary for EDITOR env var. +case "$EDITOR_CHOICE" in + helix) _EDITOR_BIN="hx" ;; + nvim) _EDITOR_BIN="nvim" ;; + vim) _EDITOR_BIN="vim" ;; + *) _EDITOR_BIN="" ;; +esac + +# Build env overrides for cmuxup command defaults. +# Build the shell integration block line by line to avoid set -e on empty conditionals. +_SB=() +_SB+=("# ── cmuxup shell integration ──────────────────────────────────────────") +[ -n "$_EDITOR_BIN" ] && _SB+=("export EDITOR=\"$_EDITOR_BIN\"") || true +[ -n "$_EDITOR_BIN" ] && _SB+=("export VISUAL=\"$_EDITOR_BIN\"") || true +[ "$AGENT" != "none" ] && _SB+=("export CMUXUP_MAIN_CMD=\"$AGENT\"") || true +[ "${INSTALL_LAZYGIT}" = "1" ] && _SB+=('export CMUXUP_LG_CMD="lazygit"') || true +case "$EDITOR_CHOICE" in + helix) _SB+=('export CMUXUP_HX_CMD="hx ."') ;; + nvim) _SB+=('export CMUXUP_HX_CMD="nvim ."') ;; + vim) _SB+=('export CMUXUP_HX_CMD="vim ."') ;; + none) _SB+=('export CMUXUP_HX_CMD=""') ;; +esac +_SB+=('eval "$(zoxide init zsh)"') + +# Add yazi shell function if installed. +if echo "${EXTRAS[*]:-}" | grep -qw "yazi"; then + _SB+=('function y() {') + _SB+=(' local tmp cwd') + _SB+=(' tmp="$(mktemp -t yazi-cwd.XXXXXX)"') + _SB+=(' yazi "$@" --cwd-file="$tmp"') + _SB+=(' if cwd="$(command cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then') + _SB+=(' builtin cd -- "$cwd"') + _SB+=(' fi') + _SB+=(' rm -f -- "$tmp"') + _SB+=("}") +fi + +_SB+=('alias lg="lazygit"') +[ -n "$_EDITOR_BIN" ] && _SB+=("alias e=\"$_EDITOR_BIN\"") || true +_SB+=("# ── end cmuxup ─────────────────────────────────────────────────────────") +# Join the array with newlines. +SHELL_BLOCK="" +for _line in "${_SB[@]}"; do + SHELL_BLOCK="${SHELL_BLOCK} +${_line}" +done + +ZSHRC="${HOME}/.zshrc" if [ "$DRY_RUN" -eq 1 ]; then _dry "would append shell integration to $ZSHRC" elif grep -q "cmuxup shell integration" "$ZSHRC" 2>/dev/null; then @@ -313,21 +378,18 @@ if command -v gum >/dev/null 2>&1 && [ "$DRY_RUN" -eq 0 ] && [ "${CMUXUP_NON_INT " ✦ cmuxup is ready ✦" gum style --foreground 245 --margin "0 2" \ "Theme: $THEME" \ - "Font: $FONT_SIZE pt | Editor: $EDITOR_CHOICE | Agent: $AGENT" + "Font: ${FONT_SIZE}pt | Agent: $AGENT | Editor: $EDITOR_CHOICE | Lazygit: $([ "$INSTALL_LAZYGIT" = "1" ] && echo yes || echo no)" echo "" gum style --foreground 212 --bold --margin "0 2" " Next steps:" gum style --foreground 255 --margin "0 2" \ - " 1. Open a new shell (or run: source ~/.zshrc)" \ - " 2. Launch your workspace: cmuxup ~/your-project" \ - " 3. Reload cmux config: cmux reload-config" + " 1. Open a new shell (or: source ~/.zshrc)" \ + " 2. Launch a workspace: cmuxup ~/your-project" \ + " 3. Reload cmux config: cmux reload-config" echo "" gum style --foreground 240 --margin "0 2" \ - " Docs → https://github.com/ITZSHOAIB/cmuxup" + " https://github.com/ITZSHOAIB/cmuxup" else echo "✦ cmuxup is ready ✦" - echo "" - echo " Theme: $THEME | Font: ${FONT_SIZE}pt | Editor: $EDITOR_CHOICE | Agent: $AGENT" - echo "" + echo " Theme: $THEME | Font: ${FONT_SIZE}pt | Agent: $AGENT | Editor: $EDITOR_CHOICE" echo " Next: source ~/.zshrc && cmuxup ~/your-project" - echo " Docs: https://github.com/ITZSHOAIB/cmuxup" fi diff --git a/templates/starship.toml b/templates/starship.toml deleted file mode 100644 index c8d9959..0000000 --- a/templates/starship.toml +++ /dev/null @@ -1,285 +0,0 @@ -"$schema" = 'https://starship.rs/config-schema.json' - -format = """ -[](red)\ -$os\ -$username\ -[](bg:peach fg:red)\ -$directory\ -[](bg:yellow fg:peach)\ -$git_branch\ -$git_status\ -[](fg:yellow bg:green)\ -$c\ -$rust\ -$golang\ -$nodejs\ -$bun\ -$php\ -$java\ -$kotlin\ -$haskell\ -$python\ -[](fg:green bg:sapphire)\ -$conda\ -[](fg:sapphire bg:lavender)\ -$time\ -[ ](fg:lavender)\ -$cmd_duration\ -$line_break\ -$character""" - -palette = 'catppuccin_mocha' - -[os] -disabled = false -style = "bg:red fg:crust" - -[os.symbols] -Windows = "" -Ubuntu = "󰕈" -SUSE = "" -Raspbian = "󰐿" -Mint = "󰣭" -Macos = "󰀵" -Manjaro = "" -Linux = "󰌽" -Gentoo = "󰣨" -Fedora = "󰣛" -Alpine = "" -Amazon = "" -Android = "" -AOSC = "" -Arch = "󰣇" -Artix = "󰣇" -CentOS = "" -Debian = "󰣚" -Redhat = "󱄛" -RedHatEnterprise = "󱄛" - -[username] -show_always = true -style_user = "bg:red fg:crust" -style_root = "bg:red fg:crust" -format = '[ $user]($style)' - -[directory] -style = "bg:peach fg:crust" -format = "[ $path ]($style)" -truncation_length = 3 -truncation_symbol = "…/" - -[directory.substitutions] -"Documents" = "󰈙 " -"Downloads" = " " -"Music" = "󰝚 " -"Pictures" = " " -"Developer" = "󰲋 " - -[git_branch] -symbol = "" -style = "bg:yellow" -format = '[[ $symbol $branch ](fg:crust bg:yellow)]($style)' - -[git_status] -style = "bg:yellow" -format = '[[($all_status$ahead_behind )](fg:crust bg:yellow)]($style)' - -[nodejs] -symbol = "" -style = "bg:green" -format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' - -[bun] -symbol = "" -style = "bg:green" -format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' - -[c] -symbol = " " -style = "bg:green" -format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' - -[rust] -symbol = "" -style = "bg:green" -format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' - -[golang] -symbol = "" -style = "bg:green" -format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' - -[php] -symbol = "" -style = "bg:green" -format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' - -[java] -symbol = " " -style = "bg:green" -format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' - -[kotlin] -symbol = "" -style = "bg:green" -format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' - -[haskell] -symbol = "" -style = "bg:green" -format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)' - -[python] -symbol = "" -style = "bg:green" -format = '[[ $symbol( $version)(\(#$virtualenv\)) ](fg:crust bg:green)]($style)' - -[docker_context] -symbol = "" -style = "bg:sapphire" -format = '[[ $symbol( $context) ](fg:crust bg:sapphire)]($style)' - -[conda] -symbol = "  " -style = "fg:crust bg:sapphire" -format = '[$symbol$environment ]($style)' -ignore_base = false - -[time] -disabled = false -time_format = "%R" -style = "bg:lavender" -format = '[[  $time ](fg:crust bg:lavender)]($style)' - -[line_break] -disabled = true - -[character] -disabled = false -success_symbol = '[❯](bold fg:green)' -error_symbol = '[❯](bold fg:red)' -vimcmd_symbol = '[❮](bold fg:green)' -vimcmd_replace_one_symbol = '[❮](bold fg:lavender)' -vimcmd_replace_symbol = '[❮](bold fg:lavender)' -vimcmd_visual_symbol = '[❮](bold fg:yellow)' - -[cmd_duration] -show_milliseconds = true -format = " in $duration " -style = "bg:lavender" -disabled = false -show_notifications = true -min_time_to_notify = 45000 - -[palettes.catppuccin_mocha] -rosewater = "#f5e0dc" -flamingo = "#f2cdcd" -pink = "#f5c2e7" -mauve = "#cba6f7" -red = "#f38ba8" -maroon = "#eba0ac" -peach = "#fab387" -yellow = "#f9e2af" -green = "#a6e3a1" -teal = "#94e2d5" -sky = "#89dceb" -sapphire = "#74c7ec" -blue = "#89b4fa" -lavender = "#b4befe" -text = "#cdd6f4" -subtext1 = "#bac2de" -subtext0 = "#a6adc8" -overlay2 = "#9399b2" -overlay1 = "#7f849c" -overlay0 = "#6c7086" -surface2 = "#585b70" -surface1 = "#45475a" -surface0 = "#313244" -base = "#1e1e2e" -mantle = "#181825" -crust = "#11111b" - -[palettes.catppuccin_frappe] -rosewater = "#f2d5cf" -flamingo = "#eebebe" -pink = "#f4b8e4" -mauve = "#ca9ee6" -red = "#e78284" -maroon = "#ea999c" -peach = "#ef9f76" -yellow = "#e5c890" -green = "#a6d189" -teal = "#81c8be" -sky = "#99d1db" -sapphire = "#85c1dc" -blue = "#8caaee" -lavender = "#babbf1" -text = "#c6d0f5" -subtext1 = "#b5bfe2" -subtext0 = "#a5adce" -overlay2 = "#949cbb" -overlay1 = "#838ba7" -overlay0 = "#737994" -surface2 = "#626880" -surface1 = "#51576d" -surface0 = "#414559" -base = "#303446" -mantle = "#292c3c" -crust = "#232634" - -[palettes.catppuccin_latte] -rosewater = "#dc8a78" -flamingo = "#dd7878" -pink = "#ea76cb" -mauve = "#8839ef" -red = "#d20f39" -maroon = "#e64553" -peach = "#fe640b" -yellow = "#df8e1d" -green = "#40a02b" -teal = "#179299" -sky = "#04a5e5" -sapphire = "#209fb5" -blue = "#1e66f5" -lavender = "#7287fd" -text = "#4c4f69" -subtext1 = "#5c5f77" -subtext0 = "#6c6f85" -overlay2 = "#7c7f93" -overlay1 = "#8c8fa1" -overlay0 = "#9ca0b0" -surface2 = "#acb0be" -surface1 = "#bcc0cc" -surface0 = "#ccd0da" -base = "#eff1f5" -mantle = "#e6e9ef" -crust = "#dce0e8" - -[palettes.catppuccin_macchiato] -rosewater = "#f4dbd6" -flamingo = "#f0c6c6" -pink = "#f5bde6" -mauve = "#c6a0f6" -red = "#ed8796" -maroon = "#ee99a0" -peach = "#f5a97f" -yellow = "#eed49f" -green = "#a6da95" -teal = "#8bd5ca" -sky = "#91d7e3" -sapphire = "#7dc4e4" -blue = "#8aadf4" -lavender = "#b7bdf8" -text = "#cad3f5" -subtext1 = "#b8c0e0" -subtext0 = "#a5adcb" -overlay2 = "#939ab7" -overlay1 = "#8087a2" -overlay0 = "#6e738d" -surface2 = "#5b6078" -surface1 = "#494d64" -surface0 = "#363a4f" -base = "#24273a" -mantle = "#1e2030" -crust = "#181926" diff --git a/test/cmuxup.bats b/test/cmuxup.bats index dba5843..ff2ed78 100644 --- a/test/cmuxup.bats +++ b/test/cmuxup.bats @@ -1,7 +1,7 @@ #!/usr/bin/env bats # Tests for bin/cmuxup — run with: bats test/cmuxup.bats -CONJURE="$BATS_TEST_DIRNAME/../bin/cmuxup" +CMUXUP="$BATS_TEST_DIRNAME/../bin/cmuxup" setup() { # Prepend mock cmux to PATH so cmuxup never touches the real cmux socket. @@ -13,21 +13,21 @@ setup() { # ── 1. --help ───────────────────────────────────────────────────────────────── @test "cmuxup --help exits 0 and shows Usage" { - run "$CONJURE" --help + run "$CMUXUP" --help [ "$status" -eq 0 ] [[ "$output" == *"Usage"* ]] } # ── 2. --version ────────────────────────────────────────────────────────────── @test "cmuxup --version exits 0 and prints a semver" { - run "$CONJURE" --version + run "$CMUXUP" --version [ "$status" -eq 0 ] [[ "$output" =~ [0-9]+\.[0-9]+\.[0-9]+ ]] } # ── 3. bad directory ────────────────────────────────────────────────────────── @test "cmuxup /nonexistent exits 1 with error on stderr" { - run "$CONJURE" /this-does-not-exist-cmuxup + run "$CMUXUP" /this-does-not-exist-cmuxup [ "$status" -eq 1 ] [[ "$stderr" == *"not a directory"* ]] || [[ "$output" == *"not a directory"* ]] } @@ -35,7 +35,7 @@ setup() { # ── 4. calls new-workspace with correct --cwd ───────────────────────────────── @test "cmuxup calls cmux new-workspace with --cwd set to project dir" { TMPDIR_PROJ="$(mktemp -d)" - run "$CONJURE" "$TMPDIR_PROJ" + run "$CMUXUP" "$TMPDIR_PROJ" [ "$status" -eq 0 ] grep -q "new-workspace" "${BATS_TMPDIR}/cmux_calls" grep -q -- "--cwd" "${BATS_TMPDIR}/cmux_calls" @@ -44,9 +44,9 @@ setup() { } # ── 5. calls new-split right ────────────────────────────────────────────────── -@test "cmuxup calls cmux new-split right for the git/editor pane" { +@test "cmuxup calls cmux new-split right for the right-top pane" { TMPDIR_PROJ="$(mktemp -d)" - run "$CONJURE" "$TMPDIR_PROJ" + run "$CMUXUP" "$TMPDIR_PROJ" [ "$status" -eq 0 ] grep -q "new-split right" "${BATS_TMPDIR}/cmux_calls" rm -rf "$TMPDIR_PROJ" @@ -55,37 +55,37 @@ setup() { # ── 6. calls new-split down ─────────────────────────────────────────────────── @test "cmuxup calls cmux new-split down for the dev terminal pane" { TMPDIR_PROJ="$(mktemp -d)" - run "$CONJURE" "$TMPDIR_PROJ" + run "$CMUXUP" "$TMPDIR_PROJ" [ "$status" -eq 0 ] grep -q "new-split down" "${BATS_TMPDIR}/cmux_calls" rm -rf "$TMPDIR_PROJ" } -# ── 7. calls new-surface --pane for the helix tab ───────────────────────────── -@test "cmuxup calls cmux new-surface --type terminal --pane for helix tab" { +# ── 7. creates second tab when both LG and HX are set (default) ─────────────── +@test "cmuxup calls new-surface --pane for second tab when both tools configured" { TMPDIR_PROJ="$(mktemp -d)" - run "$CONJURE" "$TMPDIR_PROJ" + run env CMUXUP_LG_CMD="lazygit" CMUXUP_HX_CMD="hx ." "$CMUXUP" "$TMPDIR_PROJ" [ "$status" -eq 0 ] grep -q "new-surface" "${BATS_TMPDIR}/cmux_calls" grep -q -- "--pane" "${BATS_TMPDIR}/cmux_calls" rm -rf "$TMPDIR_PROJ" } -# ── 8. renames both tabs ────────────────────────────────────────────────────── -@test "cmuxup renames tabs to lazygit and helix" { +# ── 8. renames both tabs when both tools configured ─────────────────────────── +@test "cmuxup renames tabs to lazygit and editor when both tools configured" { TMPDIR_PROJ="$(mktemp -d)" - run "$CONJURE" "$TMPDIR_PROJ" + run env CMUXUP_LG_CMD="lazygit" CMUXUP_HX_CMD="hx ." "$CMUXUP" "$TMPDIR_PROJ" [ "$status" -eq 0 ] grep -q "rename-tab" "${BATS_TMPDIR}/cmux_calls" grep -q "lazygit" "${BATS_TMPDIR}/cmux_calls" - grep -q "helix" "${BATS_TMPDIR}/cmux_calls" + grep -q "editor" "${BATS_TMPDIR}/cmux_calls" rm -rf "$TMPDIR_PROJ" } # ── 9. sends commands via send + send-key Enter ─────────────────────────────── @test "cmuxup sends commands to panes via cmux send and send-key Enter" { TMPDIR_PROJ="$(mktemp -d)" - run "$CONJURE" "$TMPDIR_PROJ" + run "$CMUXUP" "$TMPDIR_PROJ" [ "$status" -eq 0 ] grep -q "^send " "${BATS_TMPDIR}/cmux_calls" grep -q "send-key" "${BATS_TMPDIR}/cmux_calls" @@ -96,8 +96,37 @@ setup() { # ── 10. CMUXUP_MAIN_CMD override ───────────────────────────────────────────── @test "CMUXUP_MAIN_CMD env override is used instead of claude" { TMPDIR_PROJ="$(mktemp -d)" - run env CMUXUP_MAIN_CMD="my-custom-agent" "$CONJURE" "$TMPDIR_PROJ" + run env CMUXUP_MAIN_CMD="my-custom-agent" "$CMUXUP" "$TMPDIR_PROJ" [ "$status" -eq 0 ] grep -q "my-custom-agent" "${BATS_TMPDIR}/cmux_calls" rm -rf "$TMPDIR_PROJ" } + +# ── 11. adaptive: no second tab when LG_CMD is empty ───────────────────────── +@test "cmuxup skips second tab when CMUXUP_LG_CMD is empty" { + TMPDIR_PROJ="$(mktemp -d)" + run env CMUXUP_LG_CMD="" CMUXUP_HX_CMD="hx ." "$CMUXUP" "$TMPDIR_PROJ" + [ "$status" -eq 0 ] + # new-surface --pane is only called for the second tab — should NOT appear. + ! grep -q "new-surface" "${BATS_TMPDIR}/cmux_calls" + rm -rf "$TMPDIR_PROJ" +} + +# ── 12. adaptive: no second tab when HX_CMD is empty ───────────────────────── +@test "cmuxup skips second tab when CMUXUP_HX_CMD is empty" { + TMPDIR_PROJ="$(mktemp -d)" + run env CMUXUP_LG_CMD="lazygit" CMUXUP_HX_CMD="" "$CMUXUP" "$TMPDIR_PROJ" + [ "$status" -eq 0 ] + ! grep -q "new-surface" "${BATS_TMPDIR}/cmux_calls" + rm -rf "$TMPDIR_PROJ" +} + +# ── 13. adaptive: no tabs or rename when both LG and HX are empty ──────────── +@test "cmuxup creates no tool tabs when both LG_CMD and HX_CMD are empty" { + TMPDIR_PROJ="$(mktemp -d)" + run env CMUXUP_LG_CMD="" CMUXUP_HX_CMD="" "$CMUXUP" "$TMPDIR_PROJ" + [ "$status" -eq 0 ] + ! grep -q "new-surface" "${BATS_TMPDIR}/cmux_calls" + ! grep -q "rename-tab" "${BATS_TMPDIR}/cmux_calls" + rm -rf "$TMPDIR_PROJ" +} diff --git a/test/install.bats b/test/install.bats index 8c77968..8e4012f 100644 --- a/test/install.bats +++ b/test/install.bats @@ -1,5 +1,5 @@ #!/usr/bin/env bats -# Tests for install.sh +# Tests for install.sh — run with: bats test/install.bats INSTALL="$BATS_TEST_DIRNAME/../install.sh" @@ -8,8 +8,11 @@ setup() { export CMUXUP_THEME="Catppuccin Mocha" export CMUXUP_FONT_SIZE="14" export CMUXUP_AGENT="claude" + export CMUXUP_LAZYGIT="1" export CMUXUP_EDITOR="helix" - # Use an isolated HOME so we never touch the real ~/.gitconfig etc. + export CMUXUP_EXTRAS="" + export CMUXUP_OVERWRITE="0" + # Isolated HOME so we never touch the real ~/.gitconfig etc. export HOME="$(mktemp -d)" mkdir -p "$HOME/.local/bin" } @@ -24,18 +27,14 @@ teardown() { [ "$status" -eq 0 ] } -# ── 2. non-interactive dry-run doesn't prompt ───────────────────────────────── -@test "install.sh non-interactive mode exits 0 without user prompts" { +# ── 2. non-interactive dry-run succeeds ─────────────────────────────────────── +@test "install.sh non-interactive dry-run exits 0 without user prompts" { run bash "$INSTALL" --dry-run [ "$status" -eq 0 ] } # ── 3. template substitution replaces {{THEME}} ─────────────────────────────── -@test "install.sh substitutes {{THEME}} placeholder in ghostty config" { - run bash "$INSTALL" --dry-run - [ "$status" -eq 0 ] - # After a real install the theme should appear in the written config. - # In dry-run we just confirm the script can perform sed substitution. +@test "install.sh sed substitution replaces {{THEME}} placeholder" { TMPL="$(mktemp)" echo 'theme = {{THEME}}' > "$TMPL" RESULT="$(sed 's|{{THEME}}|Catppuccin Mocha|g' "$TMPL")" @@ -43,21 +42,54 @@ teardown() { rm "$TMPL" } -# ── 4. installs cmuxup to ~/.local/bin ─────────────────────────────────────── +# ── 4. dry-run reports cmuxup install path ──────────────────────────────────── @test "install.sh --dry-run reports it would install cmuxup to ~/.local/bin" { run bash "$INSTALL" --dry-run [ "$status" -eq 0 ] - # dry-run should mention the install path [[ "$output" == *"local/bin"* ]] || [[ "$output" == *"cmuxup"* ]] } -# ── 5. does not overwrite existing ghostty config without confirmation ───────── -@test "install.sh skips ghostty config when it already exists and not confirmed" { +# ── 5. skips existing ghostty config when CMUXUP_OVERWRITE=0 ───────────────── +@test "install.sh skips ghostty config when it exists and CMUXUP_OVERWRITE=0" { mkdir -p "$HOME/.config/ghostty" echo "existing-content" > "$HOME/.config/ghostty/config" - # Non-interactive mode with CMUXUP_OVERWRITE unset should not overwrite. run bash "$INSTALL" --dry-run [ "$status" -eq 0 ] - # File should still have original content. [[ "$(cat "$HOME/.config/ghostty/config")" == "existing-content" ]] } + +# ── 6. no lazygit config written when CMUXUP_LAZYGIT=0 ─────────────────────── +@test "install.sh does not write lazygit config when CMUXUP_LAZYGIT=0" { + export CMUXUP_LAZYGIT="0" + run bash "$INSTALL" --dry-run + [ "$status" -eq 0 ] + # dry-run would only print the skip; real lazygit config path should not appear + [[ "$output" != *"lazygit/config.yml"* ]] || [[ "$output" == *"dry-run"* ]] || true + # Key check: lazygit config must not have been written + [ ! -f "$HOME/Library/Application Support/lazygit/config.yml" ] +} + +# ── 7. no helix config written when CMUXUP_EDITOR=none ─────────────────────── +@test "install.sh does not write helix config when CMUXUP_EDITOR=none" { + export CMUXUP_EDITOR="none" + run bash "$INSTALL" --dry-run + [ "$status" -eq 0 ] + [ ! -f "$HOME/.config/helix/config.toml" ] +} + +# ── 8. shell block written to .zshrc on first install ───────────────────────── +@test "install.sh appends shell integration block to .zshrc" { + touch "$HOME/.zshrc" + run bash "$INSTALL" + [ "$status" -eq 0 ] + grep -q "cmuxup shell integration" "$HOME/.zshrc" +} + +# ── 9. shell block not duplicated on re-run ──────────────────────────────────── +@test "install.sh does not duplicate shell integration block on re-run" { + touch "$HOME/.zshrc" + bash "$INSTALL" + bash "$INSTALL" + COUNT="$(grep -c "cmuxup shell integration" "$HOME/.zshrc" || true)" + [ "$COUNT" -eq 1 ] +}