Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions container/.devcontainer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions container/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -161,6 +162,7 @@
"./features/claude-code-native": {},
"./features/codex-cli": {},
"./features/hermes-agent": {},
"./features/zsh-completions": {},
"./features/tmux": {},
"./features/ccusage": {
"version": "latest",
Expand Down
1 change: 1 addition & 0 deletions container/.devcontainer/features/tmux/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
107 changes: 107 additions & 0 deletions container/.devcontainer/features/zsh-completions/install.sh
Original file line number Diff line number Diff line change
@@ -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"
116 changes: 91 additions & 25 deletions container/.devcontainer/scripts/setup-terminal.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading