From f37e51ace07ca76b1c8593148a18bf6f927ac004 Mon Sep 17 00:00:00 2001 From: Sohab Sk Date: Sat, 20 Jun 2026 14:54:49 +0530 Subject: [PATCH 1/2] refactor(installer): replace gum with fzf + pure ANSI bash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove gum entirely — one fewer dep, zero TUI-mode lag between prompts. Interactive prompts now use fzf (single-select via _pick, multi-select via _pick_multi with start:select-all pre-selection) and plain read for y/n confirmations. Themed with Catppuccin Mocha fzf colour scheme. Banner and post-install message use printf + raw ANSI escapes (no external binary needed). brew install progress uses \r\033[K inline overwrite rather than gum spin. fzf is bootstrapped via brew if absent (same pattern as gum was). Non-interactive / CI / dry-run paths are unchanged; all 22 tests pass. Refs: cmuxup low-deps-installer --- install.sh | 266 ++++++++++++++++++++++++++++------------------------- 1 file changed, 142 insertions(+), 124 deletions(-) diff --git a/install.sh b/install.sh index f995c23..6187e59 100644 --- a/install.sh +++ b/install.sh @@ -10,7 +10,7 @@ set -euo pipefail # 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_LAZYGIT 1 | 0 # CMUXUP_EDITOR helix | nvim | vim | none # CMUXUP_EXTRAS space-separated: "yazi bat fd ripgrep" # CMUXUP_OVERWRITE 1 = overwrite existing configs without prompting @@ -22,6 +22,7 @@ BRANCH="main" # 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. +# The &2; } _write_file() { # dest content local dest="$1" content="$2" @@ -98,12 +111,12 @@ _brew_install() { # formula [binary] _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" + printf " ${_C_SUBTEXT}○ installing %s...${_C_RESET}" "$formula" + if brew install "$formula" >/dev/null 2>&1; then + printf "\r\033[K"; _ok "installed $formula" else - brew install "$formula" + printf "\r\033[K"; _err "failed to install $formula"; return 1 fi - _ok "installed $formula" } _theme_variants() { @@ -116,6 +129,67 @@ _theme_variants() { esac } +# Single-select via fzf. Prints the chosen item. +_pick() { # "Header text" option1 option2 ... + local header="$1"; shift + printf '%s\n' "$@" | fzf \ + --prompt " > " \ + --header " ${header}" \ + --height="~$((${#@} + 5))" \ + --layout=reverse --border=rounded \ + --no-info --no-sort \ + --color="$_FZF_COLORS" +} + +# Multi-select via fzf (TAB toggles, CTRL-A all/none). All pre-selected. +_pick_multi() { # "Header text" option1 option2 ... + local header="$1"; shift + printf '%s\n' "$@" | fzf \ + --multi \ + --prompt " > " \ + --header " ${header} (TAB=toggle CTRL-A=all/none)" \ + --bind "start:select-all,tab:toggle,ctrl-a:toggle-all" \ + --height="~$((${#@} + 6))" \ + --layout=reverse --border=rounded \ + --no-info --no-sort \ + --marker="✓ " \ + --color="$_FZF_COLORS" \ + || true +} + +# Simple y/N prompt (reads from /dev/tty so it works after pipe re-exec). +_confirm() { # "Question?" + local answer + printf " %s [Y/n] " "$1" + read -r answer /dev/null 2>&1; then @@ -123,9 +197,9 @@ if ! command -v brew >/dev/null 2>&1; then fi 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..." - brew install gum >/dev/null 2>&1 + if ! command -v fzf >/dev/null 2>&1; then + printf " Installing fzf...\n" + brew install fzf >/dev/null 2>&1 fi fi @@ -139,55 +213,40 @@ if [ "${CMUXUP_NON_INTERACTIVE:-0}" = "1" ] || [ "$DRY_RUN" -eq 1 ]; then EDITOR_CHOICE="${CMUXUP_EDITOR:-helix}" IFS=' ' read -r -a EXTRAS <<< "${CMUXUP_EXTRAS:-yazi bat fd ripgrep}" else - 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" - - THEME="$(gum choose --header "Theme:" \ + _banner + + THEME="$(_pick "Theme:" \ "Catppuccin Mocha" "TokyoNight Storm" "Gruvbox Dark Hard" "Kanagawa Wave")" - FONT_SIZE="$(gum choose --header "Font size:" "14" "13" "15")" + FONT_SIZE="$(_pick "Font size:" "14" "13" "15")" - AGENT="$(gum choose --header "AI agent (main pane):" \ - "claude" "opencode" "codex" "none")" + AGENT="$(_pick "AI agent (main pane):" "claude" "opencode" "codex" "none")" - if gum confirm "Install lazygit? (git TUI for the right-top pane)"; then + if _confirm "Install lazygit? (git TUI for the right-top pane)"; then INSTALL_LAZYGIT=1 else 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" + EDITOR_CHOICE="$(_pick "Editor tab (right-top pane):" "helix" "nvim" "vim" "none")" + + EXTRAS_RAW="$(_pick_multi "Optional tools:" \ + "yazi" "bat" "fd" "ripgrep")" + EXTRAS=() while IFS= read -r line; do [[ -z "$line" ]] && continue - EXTRAS+=("$(echo "$line" | awk '{print $1}')") + EXTRAS+=("$line") done <<< "$EXTRAS_RAW" - echo "" - gum confirm "Ready to install?" || { echo "Aborted."; exit 0; } + printf "\n" + _confirm "Ready to install?" || { printf " Aborted.\n"; exit 0; } fi _theme_variants "$THEME" -# ── Ask once about overwriting existing configs ─────────────────────────────── +# ── Ask once about overwriting configs ──────────────────────────────────────── + if [ "${CMUXUP_NON_INTERACTIVE:-0}" != "1" ] && [ "$DRY_RUN" -eq 0 ] && [ "${CMUXUP_OVERWRITE:-0}" != "1" ]; then _EXISTING=() for f in "${HOME}/.config/ghostty/config" "${HOME}/.config/helix/config.toml" \ @@ -196,28 +255,21 @@ if [ "${CMUXUP_NON_INTERACTIVE:-0}" != "1" ] && [ "$DRY_RUN" -eq 0 ] && [ "${CMU [ -f "$f" ] && _EXISTING+=("$f") done if [ "${#_EXISTING[@]}" -gt 0 ]; then - echo "" + printf "\n" _log "${#_EXISTING[@]} config file(s) already exist." - if command -v gum >/dev/null 2>&1; then - gum confirm "Overwrite existing configs?" && CMUXUP_OVERWRITE=1 || CMUXUP_OVERWRITE=0 - fi + _confirm "Overwrite existing configs?" && CMUXUP_OVERWRITE=1 || CMUXUP_OVERWRITE=0 fi fi -# ── Install core tools (always required) ───────────────────────────────────── +# ── Install tools ───────────────────────────────────────────────────────────── + +printf "\n" +_log "Installing tools..." -echo "" -_log "Installing core tools..." _brew_install git-delta delta _brew_install zoxide -# ── Install optional: lazygit ───────────────────────────────────────────────── - -if [ "${INSTALL_LAZYGIT}" = "1" ]; then - _brew_install lazygit -fi - -# ── Install optional: editor ────────────────────────────────────────────────── +[ "${INSTALL_LAZYGIT}" = "1" ] && _brew_install lazygit || true case "$EDITOR_CHOICE" in helix) _brew_install helix hx ;; @@ -225,8 +277,6 @@ case "$EDITOR_CHOICE" in 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 ;; @@ -238,7 +288,7 @@ done # ── Write configs ───────────────────────────────────────────────────────────── -echo "" +printf "\n" _log "Writing configs..." TEMPLATES="$SCRIPT_DIR/templates" @@ -247,28 +297,17 @@ _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 +[ "$EDITOR_CHOICE" = "helix" ] && _apply_template "$TEMPLATES/helix.toml" \ + "${HOME}/.config/helix/config.toml" \ + "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" || true -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 +[ "${INSTALL_LAZYGIT}" = "1" ] && _apply_template "$TEMPLATES/lazygit.yml" \ + "${HOME}/Library/Application Support/lazygit/config.yml" \ + "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" || true -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" +echo "${EXTRAS[*]:-}" | grep -qw "yazi" && _apply_template "$TEMPLATES/yazi.toml" \ + "${HOME}/.config/yazi/yazi.toml" \ + "$THEME" "$FONT_SIZE" "$DELTA_THEME" "$HELIX_THEME" || true if [ "$DRY_RUN" -eq 0 ]; then git config --global core.pager "delta" @@ -284,7 +323,6 @@ else _dry "would configure git delta" fi -# 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" @@ -295,7 +333,7 @@ fi # ── Install cmuxup command ──────────────────────────────────────────────────── -echo "" +printf "\n" _log "Installing cmuxup command..." INSTALL_DIR="${HOME}/.local/bin" if [ "$DRY_RUN" -eq 1 ]; then @@ -309,7 +347,6 @@ fi # ── Shell integration ───────────────────────────────────────────────────────── -# Resolve editor binary for EDITOR env var. case "$EDITOR_CHOICE" in helix) _EDITOR_BIN="hx" ;; nvim) _EDITOR_BIN="nvim" ;; @@ -317,14 +354,13 @@ case "$EDITOR_CHOICE" in *) _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. +# Build block line by line — avoids set -e triggering on empty-variable $() subshells. _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 +[ -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 ."') ;; @@ -333,8 +369,7 @@ case "$EDITOR_CHOICE" in esac _SB+=('eval "$(zoxide init zsh)"') -# Add yazi shell function if installed. -if echo "${EXTRAS[*]:-}" | grep -qw "yazi"; then +echo "${EXTRAS[*]:-}" | grep -qw "yazi" && { _SB+=('function y() {') _SB+=(' local tmp cwd') _SB+=(' tmp="$(mktemp -t yazi-cwd.XXXXXX)"') @@ -344,13 +379,12 @@ if echo "${EXTRAS[*]:-}" | grep -qw "yazi"; then _SB+=(' fi') _SB+=(' rm -f -- "$tmp"') _SB+=("}") -fi +} || true _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} @@ -363,33 +397,17 @@ if [ "$DRY_RUN" -eq 1 ]; then elif grep -q "cmuxup shell integration" "$ZSHRC" 2>/dev/null; then _skip "shell integration already in $ZSHRC" else - echo "$SHELL_BLOCK" >> "$ZSHRC" + printf '%s\n' "$SHELL_BLOCK" >> "$ZSHRC" _ok "added shell integration to $ZSHRC" fi # ── Done ────────────────────────────────────────────────────────────────────── -echo "" -if command -v gum >/dev/null 2>&1 && [ "$DRY_RUN" -eq 0 ] && [ "${CMUXUP_NON_INTERACTIVE:-0}" != "1" ]; then - gum style \ - --border double --border-foreground 212 \ - --padding "1 4" --margin "1 0" \ - --bold --foreground 212 \ - " ✦ cmuxup is ready ✦" - gum style --foreground 245 --margin "0 2" \ - "Theme: $THEME" \ - "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: source ~/.zshrc)" \ - " 2. Launch a workspace: cmuxup ~/your-project" \ - " 3. Reload cmux config: cmux reload-config" - echo "" - gum style --foreground 240 --margin "0 2" \ - " https://github.com/ITZSHOAIB/cmuxup" +if [ "$DRY_RUN" -eq 0 ] && [ "${CMUXUP_NON_INTERACTIVE:-0}" != "1" ]; then + _done_msg else - echo "✦ cmuxup is ready ✦" - echo " Theme: $THEME | Font: ${FONT_SIZE}pt | Agent: $AGENT | Editor: $EDITOR_CHOICE" - echo " Next: source ~/.zshrc && cmuxup ~/your-project" + printf "\n✦ cmuxup is ready ✦\n" + printf " Theme: %s | Font: %spt | Agent: %s | Editor: %s\n" \ + "$THEME" "$FONT_SIZE" "$AGENT" "$EDITOR_CHOICE" + printf " Next: source ~/.zshrc && cmuxup ~/your-project\n" fi From f214b4623a9cbd206beadb74aa12b7d76bfe2fba Mon Sep 17 00:00:00 2001 From: Sohab Sk Date: Sat, 20 Jun 2026 14:55:46 +0530 Subject: [PATCH 2/2] fix(installer): back up existing configs before overwriting When CMUXUP_OVERWRITE=1 and a config file already exists, copy it to .bak before writing the new content. Prevents silent data loss on re-runs or accidental overwrites. Refs: cmuxup safe-overwrite --- install.sh | 6 ++++-- test/install.bats | 13 ++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 6187e59..87bbcf8 100644 --- a/install.sh +++ b/install.sh @@ -87,8 +87,10 @@ _write_file() { # dest content local dest="$1" content="$2" if [ "$DRY_RUN" -eq 1 ]; then _dry "would write $dest"; return; fi mkdir -p "$(dirname "$dest")" - if [ -f "$dest" ] && [ "${CMUXUP_OVERWRITE:-0}" != "1" ]; then - _skip "$dest already exists"; return + if [ -f "$dest" ]; then + [ "${CMUXUP_OVERWRITE:-0}" != "1" ] && { _skip "$dest already exists"; return; } + cp "$dest" "${dest}.bak" + _log "backed up ${dest} → ${dest}.bak" fi printf '%s\n' "$content" > "$dest" _ok "wrote $dest" diff --git a/test/install.bats b/test/install.bats index 8e4012f..62526d9 100644 --- a/test/install.bats +++ b/test/install.bats @@ -77,7 +77,18 @@ teardown() { [ ! -f "$HOME/.config/helix/config.toml" ] } -# ── 8. shell block written to .zshrc on first install ───────────────────────── +# ── 8. existing config is backed up before overwrite ───────────────────────── +@test "install.sh backs up existing config to .bak when CMUXUP_OVERWRITE=1" { + mkdir -p "$HOME/.config/ghostty" + echo "original-content" > "$HOME/.config/ghostty/config" + export CMUXUP_OVERWRITE="1" + run bash "$INSTALL" + [ "$status" -eq 0 ] + [ -f "$HOME/.config/ghostty/config.bak" ] + [[ "$(cat "$HOME/.config/ghostty/config.bak")" == "original-content" ]] +} + +# ── 9. shell block written to .zshrc on first install ───────────────────────── @test "install.sh appends shell integration block to .zshrc" { touch "$HOME/.zshrc" run bash "$INSTALL"