diff --git a/container/.devcontainer/CHANGELOG.md b/container/.devcontainer/CHANGELOG.md index 313949d..cfb6309 100644 --- a/container/.devcontainer/CHANGELOG.md +++ b/container/.devcontainer/CHANGELOG.md @@ -4,6 +4,10 @@ ### Terminal +- **Zsh completion stack** — new `zsh-completions` feature installs fzf, carapace-bin, zsh-autosuggestions, zsh-syntax-highlighting, and fzf-tab at build time. Tab completion now works for all CLI tools (docker, npm, git flags, codeforge subcommands, claude flags) with fuzzy matching via fzf-tab. +- **Default shell set to zsh** — `chsh` runs at build time so `$SHELL` is `/usr/bin/zsh`. Tmux `default-shell` also set to zsh, so new panes spawn zsh regardless of entry method. +- **Carapace multi-shell bridge** — `setup-terminal.sh` configures carapace with `CARAPACE_BRIDGES='zsh,fish,bash,inshellisense'` after OMZ sourcing, providing completions for tools that only ship fish/bash completers. Installed via GitHub releases `.deb` with tarball fallback (apt.fury.io GPG key is defunct). +- **OMZ plugins expanded** — plugins list now includes `docker`, `docker-compose`, `npm`, `node`, `python`, `pip`, `fzf-tab`, `zsh-autosuggestions`, and `zsh-syntax-highlighting` (was just `git`). - **Alt+Enter newline keybinding** — added `alt+enter` → `chat:newline` to the default Claude Code keybindings. Windows Terminal doesn't support the Kitty keyboard protocol, so Shift+Enter and Ctrl+Enter send identical bytes to plain Enter. Alt+Enter sends ESC+CR, which is universally distinct and works reliably as a newline key. - **Shell terminal keybinds hardened** — disabled `Ctrl+Z` (suspend, which closes Docker-attached panes), `Ctrl+S/Q` (flow control freeze), and `Ctrl+W` (conflicts with Windows Terminal close-tab). Rebound `Ctrl+\` (SIGQUIT) to `Ctrl+]` and `Ctrl+D` (EOF) to `Ctrl+^` as emergency-only alternatives. Also unbound zsh's `Alt+W` (copy-region-as-kill) and `Alt+Q` (push-line) to free those keys for terminal use. diff --git a/container/.devcontainer/devcontainer.json b/container/.devcontainer/devcontainer.json index 231691c..de5a283 100755 --- a/container/.devcontainer/devcontainer.json +++ b/container/.devcontainer/devcontainer.json @@ -116,6 +116,7 @@ "./features/claude-code-native", "./features/codex-cli", "./features/hermes-agent", + "./features/zsh-completions", "./features/tmux", "./features/agent-browser", "./features/claude-monitor", @@ -161,6 +162,7 @@ "./features/claude-code-native": {}, "./features/codex-cli": {}, "./features/hermes-agent": {}, + "./features/zsh-completions": {}, "./features/tmux": {}, "./features/ccusage": { "version": "latest", diff --git a/container/.devcontainer/features/tmux/install.sh b/container/.devcontainer/features/tmux/install.sh index b564c33..449c738 100755 --- a/container/.devcontainer/features/tmux/install.sh +++ b/container/.devcontainer/features/tmux/install.sh @@ -32,6 +32,7 @@ cat > "$TMUX_CONF" << 'EOF' # Theme: Catppuccin Mocha # ── Core Settings ────────────────────────────────────────────── +set -g default-shell /usr/bin/zsh set -g mouse on set -g base-index 1 setw -g pane-base-index 1 diff --git a/container/.devcontainer/features/zsh-completions/devcontainer-feature.json b/container/.devcontainer/features/zsh-completions/devcontainer-feature.json new file mode 100644 index 0000000..7281299 --- /dev/null +++ b/container/.devcontainer/features/zsh-completions/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "zsh-completions", + "version": "1.0.0", + "name": "Zsh Completion Stack", + "description": "Installs fzf, carapace, zsh-autosuggestions, zsh-syntax-highlighting, fzf-tab, and sets zsh as default shell", + "options": { + "version": { + "type": "string", + "default": "latest", + "description": "Set to 'none' to skip installation" + }, + "username": { + "type": "string", + "default": "automatic", + "description": "Target user for shell config" + } + } +} diff --git a/container/.devcontainer/features/zsh-completions/install.sh b/container/.devcontainer/features/zsh-completions/install.sh new file mode 100755 index 0000000..8a504c1 --- /dev/null +++ b/container/.devcontainer/features/zsh-completions/install.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-3.0-only +# Copyright (c) 2026 Marcus Krueger +# Installs zsh completion stack: fzf, carapace, zsh-autosuggestions, +# zsh-syntax-highlighting, fzf-tab. Sets zsh as default shell. +set -euo pipefail + +VERSION="${VERSION:-latest}" +USERNAME="${USERNAME:-automatic}" + +# Skip installation if version is "none" +if [ "${VERSION}" = "none" ]; then + echo "[zsh-completions] Skipping installation (version=none)" + exit 0 +fi + +# Resolve username +if [ "${USERNAME}" = "automatic" ] || [ "${USERNAME}" = "auto" ]; then + if [ -n "${_REMOTE_USER:-}" ]; then + USERNAME="${_REMOTE_USER}" + elif [ -n "${_CONTAINER_USER:-}" ]; then + USERNAME="${_CONTAINER_USER}" + else + USERNAME="vscode" + fi +fi + +USER_HOME=$(eval echo "~${USERNAME}") +ZSH_CUSTOM="${USER_HOME}/.oh-my-zsh/custom" + +echo "[zsh-completions] Installing completion stack for user: ${USERNAME}" + +# ── Install fzf via apt ─────────────────────────────────────────── +echo "[zsh-completions] Installing fzf..." +apt-get update -y +apt-get install -y fzf + +# ── Install carapace from GitHub releases ───────────────────────── +echo "[zsh-completions] Installing carapace..." +CARAPACE_INSTALLED=false + +# Try GitHub releases .deb (most reliable) +CARAPACE_VERSION=$(curl -fsSL https://api.github.com/repos/carapace-sh/carapace-bin/releases/latest 2>/dev/null | grep '"tag_name"' | sed 's/.*"v\(.*\)".*/\1/') +if [ -n "$CARAPACE_VERSION" ]; then + CARAPACE_DEB_URL="https://github.com/carapace-sh/carapace-bin/releases/download/v${CARAPACE_VERSION}/carapace-bin_${CARAPACE_VERSION}_linux_amd64.deb" + if curl -fsSL "$CARAPACE_DEB_URL" -o /tmp/carapace.deb; then + dpkg -i /tmp/carapace.deb + rm -f /tmp/carapace.deb + CARAPACE_INSTALLED=true + echo "[zsh-completions] carapace ${CARAPACE_VERSION} installed via .deb" + fi +fi + +# Fallback: tarball extract +if [ "$CARAPACE_INSTALLED" = false ] && [ -n "$CARAPACE_VERSION" ]; then + CARAPACE_TAR_URL="https://github.com/carapace-sh/carapace-bin/releases/download/v${CARAPACE_VERSION}/carapace-bin_${CARAPACE_VERSION}_linux_amd64.tar.gz" + if curl -fsSL "$CARAPACE_TAR_URL" -o /tmp/carapace.tar.gz; then + tar xzf /tmp/carapace.tar.gz -C /usr/local/bin carapace + rm -f /tmp/carapace.tar.gz + CARAPACE_INSTALLED=true + echo "[zsh-completions] carapace ${CARAPACE_VERSION} installed via tarball" + fi +fi + +if [ "$CARAPACE_INSTALLED" = false ]; then + echo "[zsh-completions] WARNING: carapace install failed — completions will degrade gracefully" +fi + +# ── Clone zsh plugins (shallow, no .git for image size) ─────────── +echo "[zsh-completions] Installing zsh plugins..." +mkdir -p "${ZSH_CUSTOM}/plugins" + +clone_plugin() { + local repo="$1" + local name="$2" + local target="${ZSH_CUSTOM}/plugins/${name}" + + if [ -d "${target}" ]; then + echo "[zsh-completions] ${name} already present, skipping" + return 0 + fi + + echo "[zsh-completions] Cloning ${name}..." + git clone --depth=1 "https://github.com/${repo}.git" "${target}" + rm -rf "${target}/.git" +} + +clone_plugin "zsh-users/zsh-autosuggestions" "zsh-autosuggestions" +clone_plugin "zsh-users/zsh-syntax-highlighting" "zsh-syntax-highlighting" +clone_plugin "Aloxaf/fzf-tab" "fzf-tab" + +# ── Fix ownership ──────────────────────────────────────────────── +chown -R "${USERNAME}:${USERNAME}" "${ZSH_CUSTOM}/plugins/" + +# ── Set zsh as default shell ────────────────────────────────────── +echo "[zsh-completions] Setting default shell to /usr/bin/zsh for ${USERNAME}..." +chsh -s /usr/bin/zsh "${USERNAME}" + +# ── Clean up apt cache ──────────────────────────────────────────── +apt-get clean +rm -rf /var/lib/apt/lists/* + +echo "[zsh-completions] Installation complete" +echo " - fzf: $(fzf --version 2>/dev/null || echo 'installed')" +echo " - carapace: $(carapace --version 2>/dev/null || echo 'not available')" +echo " - Plugins: zsh-autosuggestions, zsh-syntax-highlighting, fzf-tab" +echo " - Default shell: /usr/bin/zsh" diff --git a/container/.devcontainer/scripts/setup-terminal.sh b/container/.devcontainer/scripts/setup-terminal.sh index abc82ec..45b16c1 100755 --- a/container/.devcontainer/scripts/setup-terminal.sh +++ b/container/.devcontainer/scripts/setup-terminal.sh @@ -1,50 +1,116 @@ #!/bin/bash # SPDX-License-Identifier: GPL-3.0-only # Copyright (c) 2026 Marcus Krueger -# Configure VS Code Shift+Enter → newline for Claude Code terminal input -# Writes to ~/.config/Code/User/keybindings.json (same path /terminal-setup uses) +# Configure terminal environment: +# 1. VS Code Shift+Enter → newline for Claude Code terminal input +# 2. Zsh completion stack (carapace, fzf-tab, autosuggestions, syntax-highlighting) +# Both operations are idempotent via marker blocks / existence checks. -echo "[setup-terminal] Configuring Shift+Enter keybinding for Claude Code..." +echo "[setup-terminal] Configuring terminal environment..." + +# ══════════════════════════════════════════════════════════════════ +# 1. VS Code Shift+Enter keybinding +# ══════════════════════════════════════════════════════════════════ KEYBINDINGS_DIR="$HOME/.config/Code/User" KEYBINDINGS_FILE="$KEYBINDINGS_DIR/keybindings.json" -# === Create directory if needed === mkdir -p "$KEYBINDINGS_DIR" -# === Check if already configured === if [ -f "$KEYBINDINGS_FILE" ] && grep -q "workbench.action.terminal.sendSequence" "$KEYBINDINGS_FILE" 2>/dev/null; then echo "[setup-terminal] Shift+Enter binding already present, skipping" - exit 0 -fi - -# === Merge or create keybindings === -BINDING='{"key":"shift+enter","command":"workbench.action.terminal.sendSequence","args":{"text":"\\u001b\\r"},"when":"terminalFocus"}' +else + BINDING='{"key":"shift+enter","command":"workbench.action.terminal.sendSequence","args":{"text":"\\u001b\\r"},"when":"terminalFocus"}' -if [ -f "$KEYBINDINGS_FILE" ] && command -v jq >/dev/null 2>&1; then - # Merge into existing keybindings - if jq empty "$KEYBINDINGS_FILE" 2>/dev/null; then - jq ". + [$BINDING]" "$KEYBINDINGS_FILE" >"$KEYBINDINGS_FILE.tmp" && - mv "$KEYBINDINGS_FILE.tmp" "$KEYBINDINGS_FILE" - echo "[setup-terminal] Merged binding into existing keybindings" + if [ -f "$KEYBINDINGS_FILE" ] && command -v jq >/dev/null 2>&1; then + if jq empty "$KEYBINDINGS_FILE" 2>/dev/null; then + jq ". + [$BINDING]" "$KEYBINDINGS_FILE" >"$KEYBINDINGS_FILE.tmp" && + mv "$KEYBINDINGS_FILE.tmp" "$KEYBINDINGS_FILE" + echo "[setup-terminal] Merged binding into existing keybindings" + else + echo "[$BINDING]" | jq '.' >"$KEYBINDINGS_FILE" + echo "[setup-terminal] Replaced invalid keybindings file" + fi else - # Invalid JSON — overwrite - echo "[$BINDING]" | jq '.' >"$KEYBINDINGS_FILE" - echo "[setup-terminal] Replaced invalid keybindings file" - fi -else - # No existing file — write fresh - cat >"$KEYBINDINGS_FILE" <<'EOF' + cat >"$KEYBINDINGS_FILE" <<'EOF' [ { "key": "shift+enter", "command": "workbench.action.terminal.sendSequence", "args": { - "text": "\u001b\r" + "text": "\r" }, "when": "terminalFocus" } ] EOF - echo "[setup-terminal] Created keybindings file at $KEYBINDINGS_FILE" + echo "[setup-terminal] Created keybindings file at $KEYBINDINGS_FILE" + fi +fi + +# ══════════════════════════════════════════════════════════════════ +# 2. Zsh completion stack configuration +# ══════════════════════════════════════════════════════════════════ + +ZSHRC="$HOME/.zshrc" +MARKER_START="# >>> CodeForge terminal completions >>>" +MARKER_END="# <<< CodeForge terminal completions <<<" + +if [ ! -f "$ZSHRC" ]; then + echo "[setup-terminal] No .zshrc found, skipping completion config" + exit 0 +fi + +# ── Remove existing marker block (idempotent) ───────────────────── +if grep -qF "$MARKER_START" "$ZSHRC"; then + echo "[setup-terminal] Removing existing completion block for re-application..." + sed -i "/${MARKER_START//\//\\/}/,/${MARKER_END//\//\\/}/d" "$ZSHRC" +fi + +# ── Replace plugins=(git) with full plugin list ─────────────────── +PLUGINS_LINE='plugins=(git docker docker-compose npm node python pip fzf-tab zsh-autosuggestions zsh-syntax-highlighting)' + +if grep -q '^plugins=(' "$ZSHRC"; then + sed -i "s/^plugins=(.*)/${PLUGINS_LINE}/" "$ZSHRC" + echo "[setup-terminal] Updated plugins list in .zshrc" +else + echo "[setup-terminal] No plugins=() line found, skipping plugins update" +fi + +# ── Insert carapace init block AFTER 'source $ZSH/oh-my-zsh.sh' ── +CARAPACE_BLOCK="${MARKER_START} +# (managed by setup-terminal.sh — do not edit) + +# Completion system fallback (OMZ handles primary compinit) +autoload -Uz compinit +compinit -C + +# Carapace multi-shell completions +if command -v carapace >/dev/null 2>&1; then + export CARAPACE_BRIDGES='zsh,fish,bash,inshellisense' + source <(carapace _carapace) fi + +${MARKER_END}" + +# Find the OMZ source line and insert after it +if grep -q 'source \$ZSH/oh-my-zsh.sh' "$ZSHRC"; then + # Use awk to insert the block after the source line + awk -v block="$CARAPACE_BLOCK" ' + /source \$ZSH\/oh-my-zsh\.sh/ { + print + print "" + print block + next + } + { print } + ' "$ZSHRC" > "$ZSHRC.tmp" && mv "$ZSHRC.tmp" "$ZSHRC" + echo "[setup-terminal] Inserted completion block after OMZ source" +else + # Fallback: append to end of file + echo "" >> "$ZSHRC" + echo "$CARAPACE_BLOCK" >> "$ZSHRC" + echo "[setup-terminal] Appended completion block to .zshrc (OMZ source line not found)" +fi + +echo "[setup-terminal] Terminal configuration complete"