Author SHA1 Message Date
branderson 84b66dea7c Add laptop packages and beginning lua migration for nvim 2024-04-09 22:04:09 -07:00
123 changed files with 1794 additions and 6760 deletions
-6
View File
@@ -1,6 +0,0 @@
# The claude-sandbox build context is this whole repo (so it can COPY
# bin/gitea-pr into the image), but it only needs a couple of paths from it.
**
!bin/gitea-pr
!docker/claude-sandbox/**
!config/nvim/**
-8
View File
@@ -1,15 +1,7 @@
existing
minimal
install_local.sh
bin/dotfiles-install
config/i3/config.de
config/i3/config.local
config/gtk-3.0/bookmarks
config/xfce4/xfconf/xfce-perchannel-xml
config/xfce4/terminal
config/themes
config/icons
config/VSCodium
dependencies/dotfiles-local
packages/*.local
.claude
-6
View File
@@ -16,9 +16,3 @@
[submodule "dependencies/gruvbox"]
path = dependencies/gruvbox
url = https://github.com/morhetz/gruvbox
[submodule "config/tmux/plugins/tpm"]
path = config/tmux/plugins/tpm
url = https://github.com/tmux-plugins/tpm
[submodule "dependencies/fzf"]
path = dependencies/fzf
url = https://github.com/junegunn/fzf.git
-64
View File
@@ -1,64 +0,0 @@
#!/bin/bash
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Update all locally existing PGP keys in pacman's gnupg keyring, that are
# relevant for Arch Linux packaging using the distribution's Web Key Directory
# (WKD).
# This ensures, that new signatures on already existing keys are fetched before
# a new version of archlinux-keyring is installed. Fetching signatures early
# prevents marginal trust issues with packages that are signed by keys which
# only gain full trust when updating to a new version of archlinux-keyring in
# that same system upgrade action.
set -eu
readonly main_key_domain_match="@master-key.archlinux.org$"
readonly packager_domain_match="@archlinux.org$"
readonly homedir="$(pacman-conf GPGDir)"
domain_match=""
uid=""
gpg_locate_external=(
# force update a key using WKD
gpg
--homedir
"$homedir"
--quiet
--no-permission-warning
--auto-key-locate
"clear,nodefault,wkd"
--locate-external-keys
)
# a list of <fingerprint> <mbox> tuples of all keys in the keyring
# e.g.:
# C7E7849466FE2358343588377258734B41C31549 dvzrv@archlinux.org
# 8FC15A064950A99DD1BD14DD39E4B877E62EB915 svenstaro@gmail.com
fingerprint_mboxes="$(
gpg --homedir "$homedir" --no-permission-warning --list-keys --list-options show-only-fpr-mbox
)"
# a list of <fingerprints> of all revoked keys and keys that have no valid main
# key signatures
old_fingerprints="$(
gpg --homedir "$homedir" --no-permission-warning --list-keys --with-colons |
awk -F: '$1 == "pub" && $2 ~ /-|q|r/ { getline; print $10 }'
)"
if (( EUID != 0 )); then
printf "This script must be run as root.\n" >&2
exit 1
fi
# first update the main signing keys, then the packager keys
for domain_match in "$main_key_domain_match" "$packager_domain_match"; do
while read -ra fpr_email; do
if [[ ${fpr_email[1]} =~ $domain_match && ! "$old_fingerprints" =~ ${fpr_email[0]} ]]; then
printf "Refreshing key %s with UID %s...\n" "${fpr_email[0]}" "${fpr_email[1]}"
"${gpg_locate_external[@]}" "${fpr_email[1]}"
else
printf "Skipping key %s with UID %s...\n" "${fpr_email[0]}" "${fpr_email[1]}"
fi
done <<< "$fingerprint_mboxes"
done
-165
View File
@@ -1,165 +0,0 @@
#!/bin/bash
# Launch nvim inside the claude-sandbox container (see docker/claude-sandbox/),
# with a project directory bind-mounted at its real host path. Any `claude`
# invocation inside (a shell, or claudecode.nvim's terminal) runs with
# --dangerously-skip-permissions.
#
# Usage: claude-sandbox [path-to-project] [-- nvim args...]
# claude-sandbox # sandbox the current directory
# claude-sandbox ~/repos/myproject
# claude-sandbox ~/repos/myproject src/foo.py # open a specific file
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="$SCRIPT_DIR/../docker/claude-sandbox/docker-compose.yml"
WORKSPACE_DIR="${1:-$(pwd)}"
if [ ! -d "$WORKSPACE_DIR" ]; then
echo "Not a directory: $WORKSPACE_DIR" >&2
exit 1
fi
shift || true
if [ "${1:-}" = "--" ]; then
shift
fi
WORKSPACE_DIR="$(cd "$WORKSPACE_DIR" && pwd)"
SANDBOX_SSH_KEY="${SANDBOX_SSH_KEY:-}"
if [ -n "$SANDBOX_SSH_KEY" ] && [ ! -f "$SANDBOX_SSH_KEY" ]; then
echo "SANDBOX_SSH_KEY is set but not a file: $SANDBOX_SSH_KEY" >&2
exit 1
fi
if [ -n "$SANDBOX_SSH_KEY" ]; then
key_perms=$(stat -c '%a' "$SANDBOX_SSH_KEY")
if [ "${key_perms: -2}" != "00" ]; then
echo "Warning: $SANDBOX_SSH_KEY is readable by group/other (mode $key_perms)." >&2
echo "Recommended: chmod 600 $SANDBOX_SSH_KEY" >&2
fi
# SANDBOX_SSH_KEY is meant to be a dedicated, low-privilege deploy key
# (see README "Headless git push"), not your real identity - it stays
# loaded in an in-container agent for the whole session, unattended.
# Nothing enforces that convention, so at least catch the common case
# of pointing it at a default personal keyfile by mistake.
case "$(basename -- "$SANDBOX_SSH_KEY")" in
id_rsa|id_ecdsa|id_ed25519|id_dsa|id_ecdsa_sk|id_ed25519_sk)
echo "Warning: $SANDBOX_SSH_KEY looks like a default personal SSH key," \
"not a dedicated one." >&2
echo "It will be loaded into the sandbox for the whole unattended session -" \
"see README 'Headless git push'." >&2
;;
esac
fi
have_forwarded_agent=1
if [ -z "${SSH_AUTH_SOCK:-}" ] || [ ! -S "${SSH_AUTH_SOCK}" ]; then
have_forwarded_agent=0
fi
if [ "$have_forwarded_agent" = 0 ] && [ -z "$SANDBOX_SSH_KEY" ]; then
echo "SSH_AUTH_SOCK is not set or not a valid socket, and SANDBOX_SSH_KEY is not set." >&2
echo "Either start ssh-agent and run ssh-add, or set SANDBOX_SSH_KEY to a dedicated" >&2
echo "key for headless use - see README 'Headless git push' section." >&2
exit 1
fi
# A valid forwarded socket with zero identities loaded forwards fine (the
# container sees a live, working agent) but is useless for git-over-ssh -
# warn now rather than have that surface confusingly deep into a session as
# a failed push. Non-fatal: a task might not need git-over-ssh at all, and
# SANDBOX_SSH_KEY (if set) provides its own identity independently of this.
if [ "$have_forwarded_agent" = 1 ] && ! ssh-add -l >/dev/null 2>&1; then
echo "Warning: ssh-agent has no identities loaded (ssh-add -l shows none)." >&2
echo "git-over-ssh (e.g. pushing to Gitea) will fail until you ssh-add a key," >&2
echo "unless SANDBOX_SSH_KEY is also set." >&2
fi
if [ -z "${GITEA_TOKEN:-}" ]; then
GITEA_TOKEN_FILE="${GITEA_TOKEN_FILE:-$HOME/.config/gitea/token}"
if [ -f "$GITEA_TOKEN_FILE" ]; then
GITEA_TOKEN="$(<"$GITEA_TOKEN_FILE")"
fi
fi
if [ -z "${GH_TOKEN:-}" ]; then
# Deliberately not `gh auth token` / gh's own hosts.yml: a normal `gh
# auth login` session is write-capable, which would defeat the point.
# This should be a dedicated read-only fine-grained PAT (Contents +
# Metadata: read-only), generated separately for this sandbox.
GH_TOKEN_FILE="${GH_TOKEN_FILE:-$HOME/.config/gh/sandbox-token}"
if [ -f "$GH_TOKEN_FILE" ]; then
GH_TOKEN="$(<"$GH_TOKEN_FILE")"
fi
fi
# Read-write scratch space, separate from WORKSPACE_DIR, for passing files
# (screenshots, temp scripts) back and forth with a sandboxed session without
# touching the actual project checkout. Persists across sessions (it's a
# real host directory, not a container volume) since the point is sharing
# things with the host, not just scratch space for one run.
SANDBOX_SHARE_DIR="${SANDBOX_SHARE_DIR:-$HOME/.local/share/claude-sandbox/shared}"
mkdir -p "$SANDBOX_SHARE_DIR"
# ~/.claude/CLAUDE.md (carried into the container) tells agents to write
# specs to ~/specs/ rather than a project-local path - make sure that
# actually exists before it's bind-mounted, for a fresh machine that hasn't
# used the convention yet.
mkdir -p "$HOME/specs"
# Claude Code's own internal per-command sandboxing (~/.claude/settings.json's
# "sandbox" block) is redundant here - it can't even function inside this
# container (bwrap can't create a namespace, and getting it to would need
# privilege escalation approaching --privileged, undoing this container's own
# hardening), and this container is already a real, working sandbox on its
# own. settings.json is mounted read-only precisely so a session can't
# tamper with it, so it can't be patched from inside - generate a sandbox-
# specific copy here instead and mount that in place of the real one.
# Everything else in it (hooks, permissions, plugins) carries over unchanged.
SANDBOX_SETTINGS_FILE="$(mktemp "${TMPDIR:-/tmp}/claude-sandbox-settings.XXXXXX.json")"
jq '.sandbox.enabled = false' "$HOME/.claude/settings.json" > "$SANDBOX_SETTINGS_FILE"
cleanup() { rm -f "$SANDBOX_SETTINGS_FILE"; rm -rf "$SANDBOX_GITCONFIG_DIR"; }
trap cleanup EXIT
# Git commits inside the sandbox need a user.name/user.email - carry over a
# copy of the host's global gitconfig, read-write: entrypoint.sh's
# `gh auth setup-git` (what makes GitHub read-only from in here - see
# README) needs to *write* a credential-helper entry into it.
#
# A whole directory, not a single file bind-mounted at ~/.gitconfig: `git
# config` writes via the usual write-temp-then-rename-over-original pattern,
# which fails with "Device or resource busy" on a single-file bind mount -
# Linux won't let that mount point's backing inode be replaced, only a file
# *within* a mounted directory can be freely renamed over. Point git at the
# copy via GIT_CONFIG_GLOBAL instead of relying on it being at ~/.gitconfig.
# Nothing written in here ever reaches the user's real ~/.gitconfig either
# way, since it's a copy in its own directory, not the real file/parent.
SANDBOX_GITCONFIG_DIR="$(mktemp -d "${TMPDIR:-/tmp}/claude-sandbox-gitconfig.XXXXXX")"
if [ -f "$HOME/.gitconfig" ]; then
cp "$HOME/.gitconfig" "$SANDBOX_GITCONFIG_DIR/.gitconfig"
else
touch "$SANDBOX_GITCONFIG_DIR/.gitconfig"
fi
export WORKSPACE_DIR
# The image's user is built to match this uid/gid/username exactly (see
# Dockerfile) - this is what makes $HOME-based absolute paths (dotfiles,
# ~/.claude's plugin bookkeeping) resolve identically inside the container.
export SANDBOX_USER="$(id -un)"
export SANDBOX_UID="$(id -u)"
export SANDBOX_GID="$(id -g)"
export SANDBOX_SHARE_DIR
export SANDBOX_SETTINGS_FILE
export SANDBOX_GITCONFIG_DIR
export SSH_AUTH_SOCK
export SANDBOX_SSH_KEY_FILE="$SANDBOX_SSH_KEY"
export SSH_KNOWN_HOSTS="${SSH_KNOWN_HOSTS:-$HOME/.ssh/known_hosts}"
export GITEA_TOKEN="${GITEA_TOKEN:-}"
export GITEA_SSH_HOST="${GITEA_SSH_HOST:-}"
export GITEA_API="${GITEA_API:-}"
export GH_TOKEN="${GH_TOKEN:-}"
export TZ="${TZ:-$(readlink -f /etc/localtime 2>/dev/null | sed 's#.*/zoneinfo/##')}"
# Not `exec`: the EXIT trap above needs this shell to still be around
# afterward to actually clean up the generated settings file.
docker compose -f "$COMPOSE_FILE" run --rm --build claude "$@"
-109
View File
@@ -1,109 +0,0 @@
#!/bin/bash
# Add a domain to the egress firewall allowlist of a running claude-sandbox
# container, from outside it. The sandbox itself has no privilege to do this
# (node has no sudo/setuid path back to root - see docker/claude-sandbox's
# security notes); if a sandboxed agent hits a blocked domain it actually
# needs, it should tell you the exact command to run here rather than try
# to work around the firewall.
#
# Usage: claude-sandbox-allow <domain> [--global] [--container <name>]
# claude-sandbox-allow crates.io # this running session only
# claude-sandbox-allow crates.io --global # + persist for future sessions
set -euo pipefail
domain=""
global=0
container=""
while [ "$#" -gt 0 ]; do
case "$1" in
--global) global=1; shift ;;
--container) container="$2"; shift 2 ;;
-*) echo "Unknown option: $1" >&2; exit 1 ;;
*)
if [ -n "$domain" ]; then
echo "Usage: claude-sandbox-allow <domain> [--global] [--container <name>]" >&2
exit 1
fi
domain="$1"
shift
;;
esac
done
if [ -z "$domain" ]; then
echo "Usage: claude-sandbox-allow <domain> [--global] [--container <name>]" >&2
exit 1
fi
# Validated up front, not just passed as a positional arg below: this also
# has to be a well-formed hostname to be a valid dnsmasq server=/domain/...
# line (e.g. no embedded "/"), on top of ruling out shell metacharacters. A
# sandboxed agent can suggest this exact command to the user to copy-paste
# (see sandbox-context.sh), so a prompt-injected agent handing back a
# malicious "domain" is a real path here, not just a theoretical one.
if [[ ! "$domain" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,62}\.)*[a-zA-Z0-9-]{1,63}$ ]]; then
echo "Not a valid domain: $domain" >&2
exit 1
fi
if [ -z "$container" ]; then
containers=$(docker ps --filter "label=com.docker.compose.project=claude-sandbox" --filter "status=running" --format '{{.Names}}')
count=$(echo "$containers" | grep -c . || true)
if [ "$count" -eq 0 ]; then
echo "No running claude-sandbox container found." >&2
exit 1
elif [ "$count" -gt 1 ]; then
echo "Multiple running claude-sandbox containers found; specify one with --container:" >&2
echo "$containers" >&2
exit 1
fi
container="$containers"
fi
# The DNS proxy (dns-proxy.sh) NXDOMAINs anything not in its allowlist, so
# this has to happen *before* resolving the domain below - otherwise the dig
# right after this would itself get blocked. Append a server= line to
# dnsmasq's servers-file (idempotent - skip if already present from an
# earlier call) and SIGHUP it to reload just that file, no restart needed.
# $domain is passed as a positional arg to the inner script (`$1`, inside a
# single-quoted heredoc) rather than interpolated into the script text - it's
# validated above, but this keeps it that way even if the validation regex
# above is ever loosened later.
docker exec -u root "$container" bash -c '
domain="$1"
entry="server=/${domain}/127.0.0.11"
grep -qxF "$entry" /etc/dnsmasq-sandbox-servers.conf 2>/dev/null || echo "$entry" >> /etc/dnsmasq-sandbox-servers.conf
kill -HUP "$(cat /var/run/dnsmasq-sandbox.pid)"
' -- "$domain"
echo "Resolving $domain inside $container..."
ips=$(docker exec "$container" dig +short A "$domain" | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' || true)
if [ -z "$ips" ]; then
echo "Failed to resolve $domain inside the container." >&2
exit 1
fi
for ip in $ips; do
echo "Allowing $ip ($domain)"
docker exec -u root "$container" ipset add allowed-domains "$ip" -exist
done
echo "$domain allowed for the running session ($container)."
if [ "$global" -eq 1 ]; then
profile_file="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/config/profile"
if [ -f "$profile_file" ] && grep -q '^export ALLOWED_DOMAINS=' "$profile_file"; then
current=$(sed -nE 's/^export ALLOWED_DOMAINS="(.*)"$/\1/p' "$profile_file")
if [[ " $current " == *" $domain "* ]]; then
echo "$domain already in the global ALLOWED_DOMAINS (config/profile)."
else
new="${current:+$current }$domain"
sed -i "s|^export ALLOWED_DOMAINS=.*|export ALLOWED_DOMAINS=\"$new\"|" "$profile_file"
echo "Added $domain to config/profile's ALLOWED_DOMAINS (source your shell, or open a new one, for future sessions to pick it up)."
fi
else
echo "Couldn't find 'export ALLOWED_DOMAINS=' in $profile_file to update." >&2
exit 1
fi
fi
-45
View File
@@ -1,45 +0,0 @@
#!/bin/sh
# Copy stdin to the system clipboard, regardless of platform or how deep in
# ssh/tmux this shell is: prefer a local clipboard tool for the current
# display server, and fall back to a single plain OSC 52 write when no local
# tool is available (e.g. a headless remote host). Any tmux hops in between
# here and the real terminal relay that onward themselves, recursively, via
# the pane-set-clipboard hook in tmux.conf -- this doesn't need to know or
# guess how many hops there are.
if command -v pbcopy >/dev/null 2>&1; then
exec pbcopy
elif [ -n "$WAYLAND_DISPLAY" ] && command -v wl-copy >/dev/null 2>&1; then
# wl-copy/xclip both hold the selection by staying alive as a background
# process (X11/Wayland selections have no central store -- the owner has
# to serve paste requests live). setsid -f detaches that process from
# our session so tmux's copy-pipe job cleanup can't kill it right after.
setsid -f wl-copy
elif [ -n "$DISPLAY" ] && command -v xclip >/dev/null 2>&1; then
setsid -f xclip -selection clipboard
else
data=$(base64 | tr -d '\n')
# /dev/tty needs this process's own session to have a controlling
# terminal, but a job spawned by nvim's clipboard provider doesn't get
# one (nvim's job-spawning doesn't establish a ctty for children). Nvim
# itself also isn't a single process: the one actually spawning jobs is
# a `nvim --embed` core process with no tty of its own (shown as `?` in
# ps) -- the real terminal belongs to a separate parent UI process. Walk
# up the process tree past any tty-less ancestors to find it.
real_tty=""
pid="$PPID"
i=0
while [ "$i" -lt 10 ] && [ -n "$pid" ] && [ "$pid" != "0" ]; do
t=$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')
if [ -n "$t" ] && [ "$t" != "?" ]; then
real_tty="/dev/$t"
break
fi
pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
i=$((i + 1))
done
if [ -n "$real_tty" ] && [ -w "$real_tty" ]; then
printf '\033]52;c;%s\a' "$data" > "$real_tty"
else
printf '\033]52;c;%s\a' "$data" > /dev/tty
fi
fi
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
docker compose pull
docker compose down
docker compose up -d
-4
View File
@@ -1,4 +0,0 @@
#!/bin/bash
docker compose down
docker compose up -d
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
find . -type f -name "docker-compose.y*ml" -exec dirname {} \; | while read dir
do
if [[ "$dir" != *"DEPRECATED"* ]] && [[ "$dir" != *"MIGRATED"* ]] ; then
pushd "$dir"
docker compose down
docker compose pull
docker compose up -d
popd
fi
done
-209
View File
@@ -1,209 +0,0 @@
#!/bin/bash
set -euo pipefail
GITEA_TOKEN_FILE="${GITEA_TOKEN_FILE:-$HOME/.config/gitea/token}"
function usage() {
echo "Usage: gitea-pr create [--title TITLE] [--body BODY] [--base BRANCH]"
echo " gitea-pr comments PR_NUMBER"
echo " gitea-pr merge PR_NUMBER"
echo ""
echo "Requires GITEA_SSH_HOST and GITEA_API set in the environment (e.g. in" \
"your shell profile), and a token at $GITEA_TOKEN_FILE (or GITEA_TOKEN" \
"set for this invocation only)."
}
function require_config() {
if [ -z "${GITEA_SSH_HOST:-}" ]; then
echo "GITEA_SSH_HOST is not set. Add 'export GITEA_SSH_HOST=<gitea ssh hostname>'" \
"(e.g. gitbox.bradina.localdomain) to ~/.profile.local" >&2
exit 1
fi
if [ -z "${GITEA_API:-}" ]; then
echo "GITEA_API is not set. Add 'export GITEA_API=https://<gitea host>/api/v1'" \
"(e.g. https://gitbox.branderson.io/api/v1) to ~/.profile.local" >&2
exit 1
fi
}
function require_token() {
if [ -z "${GITEA_TOKEN:-}" ] && [ -f "$GITEA_TOKEN_FILE" ]; then
GITEA_TOKEN=$(<"$GITEA_TOKEN_FILE")
fi
if [ -z "${GITEA_TOKEN:-}" ]; then
echo "No Gitea token found. Generate one in the Gitea web UI" \
"(Settings > Applications, scopes write:repository + write:issue)" \
"and save it to $GITEA_TOKEN_FILE (chmod 600), or set GITEA_TOKEN" \
"in the environment for this invocation only." >&2
exit 1
fi
}
function gitea_owner_repo() {
local remote_url host owner_repo
remote_url=$(git remote get-url origin 2>/dev/null) || {
echo "No 'origin' remote found in this repository" >&2
exit 1
}
host=$(echo "$remote_url" | sed -E 's#^[^@]+@([^:]+):.*#\1#')
if [ "$host" != "$GITEA_SSH_HOST" ]; then
echo "origin remote ($remote_url) is not on $GITEA_SSH_HOST; gitea-pr only supports this host" >&2
exit 1
fi
owner_repo=$(echo "$remote_url" | sed -E 's#^[^@]+@[^:]+:##; s#\.git$##')
echo "$owner_repo"
}
if [ "$#" -lt 1 ]; then
usage
exit 1
fi
subcommand="$1"
shift
case "$subcommand" in
create|comments|merge)
require_config
require_token
owner_repo=$(gitea_owner_repo)
owner="${owner_repo%%/*}"
repo="${owner_repo##*/}"
;;
*)
usage
exit 1
;;
esac
case "$subcommand" in
create)
title=""
body=""
base=""
while [ "$#" -gt 0 ]; do
case "$1" in
--title) title="$2"; shift 2 ;;
--body) body="$2"; shift 2 ;;
--base) base="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
esac
done
body="**Summary generated by LLM agent**
${body}"
current_branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$current_branch" = "HEAD" ]; then
echo "Detached HEAD: check out the feature branch before creating a PR" >&2
exit 1
fi
if [ -z "$base" ]; then
base=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's#refs/remotes/origin/##' || true)
base="${base:-main}"
fi
response=$(curl -sS -w '\n%{http_code}' "${GITEA_API}/repos/${owner}/${repo}/pulls?state=open" \
-H "Authorization: token ${GITEA_TOKEN}")
http_code=$(echo "$response" | tail -1)
json=$(echo "$response" | sed '$d')
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "Failed to check for an existing PR (HTTP $http_code):" >&2
echo "$json" >&2
exit 1
fi
existing_url=$(echo "$json" | jq -r --arg head "$current_branch" --arg base "$base" \
'.[] | select(.head.ref == $head and .base.ref == $base) | .html_url' | head -1)
if [ -n "$existing_url" ]; then
echo "$existing_url"
exit 0
fi
if [ -z "$title" ]; then
title="$current_branch"
fi
response=$(curl -sS -w '\n%{http_code}' -X POST "${GITEA_API}/repos/${owner}/${repo}/pulls" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg head "$current_branch" --arg base "$base" --arg title "$title" --arg body "$body" \
'{head: $head, base: $base, title: $title, body: $body}')")
http_code=$(echo "$response" | tail -1)
json=$(echo "$response" | sed '$d')
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "Failed to create PR (HTTP $http_code):" >&2
echo "$json" >&2
exit 1
fi
echo "$json" | jq -r '.html_url'
;;
comments)
pr_number="${1:-}"
if [ -z "$pr_number" ]; then
echo "Usage: gitea-pr comments PR_NUMBER" >&2
exit 1
fi
echo "=== Issue comments ==="
response=$(curl -sS -w '\n%{http_code}' "${GITEA_API}/repos/${owner}/${repo}/issues/${pr_number}/comments" \
-H "Authorization: token ${GITEA_TOKEN}")
http_code=$(echo "$response" | tail -1)
json=$(echo "$response" | sed '$d')
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "Failed to fetch issue comments (HTTP $http_code):" >&2
echo "$json" >&2
exit 1
fi
echo "$json" | jq -r '.[] | "[\(.user.login) @ \(.created_at)]\n\(.body)\n"'
echo "=== Reviews ==="
response=$(curl -sS -w '\n%{http_code}' "${GITEA_API}/repos/${owner}/${repo}/pulls/${pr_number}/reviews" \
-H "Authorization: token ${GITEA_TOKEN}")
http_code=$(echo "$response" | tail -1)
reviews_json=$(echo "$response" | sed '$d')
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "Failed to fetch reviews (HTTP $http_code):" >&2
echo "$reviews_json" >&2
exit 1
fi
echo "$reviews_json" | jq -r '.[] | select(.body != "") | "[\(.user.login) @ \(.submitted_at) — \(.state)]\n\(.body)\n"'
echo "=== Inline review comments ==="
review_ids=$(echo "$reviews_json" | jq -r '.[] | select(.comments_count > 0) | .id')
for review_id in $review_ids; do
response=$(curl -sS -w '\n%{http_code}' \
"${GITEA_API}/repos/${owner}/${repo}/pulls/${pr_number}/reviews/${review_id}/comments" \
-H "Authorization: token ${GITEA_TOKEN}")
http_code=$(echo "$response" | tail -1)
json=$(echo "$response" | sed '$d')
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "Failed to fetch comments for review ${review_id} (HTTP $http_code):" >&2
echo "$json" >&2
exit 1
fi
echo "$json" | jq -r '.[] | "[\(.user.login) @ \(.path):\(.line // "?")]\n\(.body)\n"'
done
;;
merge)
pr_number="${1:-}"
if [ -z "$pr_number" ]; then
echo "Usage: gitea-pr merge PR_NUMBER" >&2
exit 1
fi
response=$(curl -sS -w '\n%{http_code}' -X POST "${GITEA_API}/repos/${owner}/${repo}/pulls/${pr_number}/merge" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do": "merge"}')
http_code=$(echo "$response" | tail -1)
json=$(echo "$response" | sed '$d')
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "Failed to merge PR #${pr_number} (HTTP $http_code):" >&2
echo "$json" >&2
exit 1
fi
echo "Merged PR #${pr_number}"
;;
esac
-18
View File
@@ -1,18 +0,0 @@
#!/bin/bash
function usage() {
echo "Usage: postgres-docker-backup container_name db_user dump_file"
}
if [[ ! "$#" == 3 ]]; then
usage
exit 1
fi
container_name="$1"
db_user="$2"
dump_file="$3"
if [ -z "$container_name" ] || [ -z "$db_user" ] || [ -z "$dump_file" ]; then
usage
exit 1
fi
docker exec "$container_name" '/bin/sh' -c "pg_dumpall -U $db_user" > $dump_file
-18
View File
@@ -1,18 +0,0 @@
#!/bin/bash
function usage() {
echo "Usage: postgres-docker-restore container_name db_user"
}
if [[ ! "$#" == 2 ]]; then
usage
exit 1
fi
container_name="$1"
db_user="$2"
if [ -z "$container_name" ] || [ -z "$db_user" ]; then
usage
exit 1
fi
docker exec -it "$container_name" '/bin/sh' -c "psql -U $db_user"
-20
View File
@@ -1,20 +0,0 @@
#!/bin/bash
function usage() {
echo "Usage: postgres-docker-restore container_name db_user dump_file"
echo " Note that dump_file is relative to docker filesystem and must be mounted"
}
if [[ ! "$#" == 3 ]]; then
usage
exit 1
fi
container_name="$1"
db_user="$2"
dump_file="$3"
if [ -z "$container_name" ] || [ -z "$db_user" ] || [ -z "$dump_file" ]; then
usage
exit 1
fi
docker exec -it "$container_name" '/bin/sh' -c "psql -U $db_user < $dump_file"
-8
View File
@@ -1,8 +0,0 @@
#!/bin/sh
# Read clipboard content on stdin (e.g. from `tmux save-buffer -`) and write
# it as a raw OSC 52 escape sequence directly to the given tty device path.
# Intended to be called from tmux's pane-set-clipboard hook as:
# tmux save-buffer - | tmux-clipboard-relay "#{client_tty}"
tty_path="$1"
data=$(base64 | tr -d '\n')
printf '\033]52;c;%s\a' "$data" > "$tty_path"
-77
View File
@@ -1,77 +0,0 @@
#!/bin/bash
# TODO: Dotfiles may be in a different place
if [ ! -f "$HOME/.dotfiles-config" ]; then
echo "$HOME/.dotfiles-config not found, please run dotfiles/install.sh to create it"
exit 1
fi
source "$HOME/.dotfiles-config"
source "$DOTFILES_DIR/functions.sh"
# XAUTHORITY file may be in /tmp or it may be ~/.XAUTHORITY
if [ -f "$HOME/.Xauthority" ]; then
export XAUTHORITY="$HOME/.Xauthority"
else
auth_path="$(find /tmp -maxdepth 1 -name "xauth_*")"
if [ ! -z "$auth_path" ]; then
export XAUTHORITY="$auth_path"
else
echo "No Xauthority file found. Exiting"
exit 1
fi
fi
wallpapers_path="$HOME/wallpapers"
wallpapers_vertical_path="$HOME/wallpapers-vertical"
if [ ! -d "$wallpapers_path" ]; then
echo "ERROR: Wallpaper directory not found!"
echo "Please create wallpapers directory in $wallpapers_path"
exit 1
fi
if program_installed xrandr; then
display_count="$(xrandr | grep ' connected ' | wc -l)"
display_order="$(xrandr --listmonitors | tail -n +2 | sed 's/[\+\*]//g' | awk '{ print $2}')"
# Build feh command
feh_args=("--bg-fill")
for i in $(seq 1 "$display_count"); do
display_name="$(echo "$display_order" | sed "${i}q;d")"
# Get information about the current display, need verbose to always return rotation
# and remove 'primary' for consistent column placement
display_info="$(xrandr --query --verbose | grep "$display_name" | sed 's/primary //')"
# Get the selected resolution for the display
resolution="$(echo "$display_info" | awk '{ print $3 }' | sed 's/\+.*//')"
# Get the rotation for each display
rotation="$(echo "$display_info" | awk '{ print $5 }')"
path="$wallpapers_path"
resolution_segment=""
if [ "$rotation" != "normal" ] && [ "$rotation" != "inverted" ]; then
if [ ! -d "$wallpapers_vertical_path" ]; then
echo "$rotation"
echo "ERROR: Vertical display detected, but vertical wallpapers directory not found!"
echo "Please create vertical wallpapers directory in $wallpapers_vertical_path"
exit 1
fi
path="$wallpapers_vertical_path"
fi
# If we have a resolution-specific path for the display, use it
if [ -d "$path/1080p" ]; then
resolution_segment="/1080p"
fi
if [ -d "$path/4k" ]; then
if [ "$resolution" = "3840x2160" ] || [ "$resolution" = "2160x3840" ]; then
resolution_segment="/4k"
fi
fi
display_wallpaper="$(shuf -en1 "$path$resolution_segment"/*)"
feh_args+=("$display_wallpaper")
done
echo "feh ${feh_args[@]}"
feh "${feh_args[@]}"
else
# Default to ~/wallpapers/1080p or ~/wallpapers for first display if no xrandr
if [ -d "$path/1080p" ]; then
feh --bg-fill "$(shuf -en1 $wallpapers_path/1080p/*)"
else
feh --bg-fill "$(shuf -en1 $wallpapers_path/*)"
fi
fi
-59
View File
@@ -1,59 +0,0 @@
{
"workbench.colorTheme": "Gruvbox Dark Medium",
"vim.camelCaseMotion.enable": true,
"vim.easymotion": true,
"vim.enableNeovim": true,
"vim.handleKeys": {
"<C-a>": false,
"<C-d>": true,
"<C-f>": false,
"<C-s>": false,
"<C-z>": false
},
"vim.insertModeKeyBindingsNonRecursive": [
{
"before": [",", ","],
"after": ["<Esc>"]
}
],
"vim.normalModeKeyBindingsNonRecursive": [
{
"before": ["<leader>", "d"],
"after": ["d", "d"]
},
{
"before": ["<leader>", "d"],
"after": ["d", "d"]
},
{
"before": [",", ",", "<Esc>"],
"commands": [":nohl"]
},
{
"before": ["<leader>", "q"],
"commands": [":bp"]
},
{
"before": ["<leader>", "w"],
"commands": [":bn"]
},
{
"before": ["<leader>", "r", "r"],
"commands": [":action "]
},
{
"before": ["<leader>", "<leader>", "c"],
"commands": [":Blose"],
"silent": true
}
],
"vim.leader": ",",
// To improve performance
"extensions.experimental.affinity": {
"vscodevim.vim": 1
},
"vim.neovimUseConfigFile": true,
"editor.fontFamily": "'Source Code Pro', regular",
"vim.normalModeKeyBindings": [],
"python.venvPath": "~/.pyenv"
}
+3 -27
View File
@@ -1,3 +1,6 @@
import = [
"~/.config/alacritty/themes/themes/gruvbox_dark.toml"
]
[font]
size = 9.0
@@ -17,30 +20,3 @@ style = "Italic"
[font.normal]
family = "Source Code Pro for Powerline"
style = "Regular"
[general]
import = [
"~/.config/alacritty/themes/themes/gruvbox_dark.toml"
]
[selection]
# X11 always sets PRIMARY (middle-click paste) on text selection; that part
# needs no config. CLIPBOARD (Ctrl+V / Ctrl+Shift+V) is a separate buffer
# that nothing populates without this, so mirror every selection into it
# too. This is handled by Alacritty itself, so it covers anything visible
# on screen -- local shell, nested tmux, SSH'd remote shells -- regardless
# of whether the app underneath knows how to talk to the clipboard.
save_to_clipboard = true
[keyboard]
# Alacritty doesn't implement the CSI-u/Kitty keyboard protocol that lets a
# terminal report Shift+Enter as distinct from plain Enter, so it needs an
# explicit binding. Sending a raw line feed makes Shift+Enter equivalent to
# Ctrl+J, which Claude Code (and most other terminal apps) already treats
# as "insert newline" with zero further configuration -- this works
# unchanged through any depth of nested tmux, since it's just a literal
# byte rather than an escape sequence that needs to be interpreted along
# the way.
bindings = [
{ key = "Return", mods = "Shift", chars = "\n" },
]
-7
View File
@@ -1,7 +0,0 @@
[Desktop Entry]
Name=USDB Syncer
Comment=Download karaoke songs
Exec=usdb_syncer
Terminal=false
Type=Application
Categories=Software;
-7
View File
@@ -21,13 +21,6 @@ exec --no-startup-id dex --autostart --environment i3
# applications #
################
# set wallpaper
exec --no-startup-id sleep 1 && ~/dotfiles/bin/update-wallpaper
# exec --no-startup-id sleep 1 && systemctl start update-wallpaper.service
# start keyring
exec --no-startup-id dbus-update-activation-environment --all
#exec_always --no-startup-id conky -c ~/.config/conky/conkyrc-stats
#exec --no-startup-id sleep 7 && firefox https://todo.bradina.haus/
+18 -31
View File
@@ -10,8 +10,6 @@ bar {
font pango: Source Code Pro for Powerline 10
status_command i3blocks -c ~/.config/i3/i3blocks.conf
position bottom
output DP-0
output DP-2
#tray_padding 0
# When strip_workspace_numbers is set to yes,
@@ -39,32 +37,21 @@ bar {
# https://gist.github.com/lopspower/03fb1cc0ac9f32ef38f4
# #08052be6 --> e6=90%
bar {
font pango: Noto Sans Regular 10
status_command i3blocks -c ~/.config/i3/i3blocks-2.conf
i3bar_command i3bar --transparency
output HDMI-0
position bottom
colors {
# gruvbox colors
background $bg
statusline $yellow
# workspaces section
# border bg txt
focused_workspace $aqua $aqua $darkgray
inactive_workspace $darkgray $darkgray $yellow
active_workspace $darkgray $darkgray $yellow
urgent_workspace $red $red $bg
}
# colors {
# separator $purple
# background $darkbluetrans
# statusline $white
# border bg txt indicator
# focused_workspace $lighterblue $lighterblue $darkblue $purple
# active_workspace $lightdblue $lightdblue $darkblue $purple
# inactive_workspace $darkblue $darkblue $lightdblue $purple
# urgent_workspace $urgentred $urgentred $white $purple
# }
}
# bar {
# font pango: Noto Sans Regular 10
# status_command i3blocks -c ~/.config/i3/i3blocks-2.conf
# i3bar_command i3bar --transparency
# output HDMI-0
# position bottom
#
# colors {
# separator $purple
# background $darkbluetrans
# statusline $white
# border bg txt indicator
# focused_workspace $lighterblue $lighterblue $darkblue $purple
# active_workspace $lightdblue $lightdblue $darkblue $purple
# inactive_workspace $darkblue $darkblue $lightdblue $purple
# urgent_workspace $urgentred $urgentred $white $purple
# }
#}
-1
View File
@@ -1 +0,0 @@
alias vim=nvim
-142
View File
@@ -1,142 +0,0 @@
function program_installed {
local return_=0
type $1 >/dev/null 2>&1 || { local return_=1; }
return "$return_"
}
function list_processes() {
# List all processes matching the given process name string
ps aux | grep $(whoami) | grep -i "$1" | grep -v grep
}
function kill_all() {
# Kill all processes matching the given process name string
if [ ! "$1" ]; then
echo "kill_all requires a process name to kill all instances of"
return 1
fi
kill_flag=15
if [ "$15" ]; then
kill_flag="$15"
fi
count=$(list_processes "$1" | wc -l)
echo "Killing $count running processes matching $1"
kill -"$kill_flag" $(list_processes "$1" | awk '{print $2}')
count=$(list_processes "$1" | wc -l)
if [ $count -gt 0 ]; then
echo "$count processes could not be killed"
fi
}
function ranger-cd() {
# Make shell cd to ranger directory
tempfile=$(mktemp)
ranger --choosedir="$tempfile" "${@:-$(pwd)}" < $TTY
test -f "$tempfile" &&
if [ "$(cat -- "$tempfile")" != "$(echo -n `pwd`)" ]; then
cd $(cat $tempfile)
fi
zle reset-prompt
printf '\n'
rm -f -- "$tempfile"
}
function terminal-clock() {
tty-clock -tcb < $TTY
zle reset-prompt
printf '\n'
}
function check_dotfiles_version {
# Check if the version of the dotfiles directory is the latest version
# Return 0 if repo is present and the latest version
# Return 1 if the repo is not present
# Return 2 if the repo is behind the remote
# Return 3 if the repo is ahead of the remote
# Return 4 if the repo has diverged from the remote
# Return 5 if DOTFILES_DIR is not set
if [ -z $DOTFILES_DIR ]; then
return 5
fi
if [ ! -d $DOTFILES_DIR ]; then
return 2
fi
pushd $DOTFILES_DIR > /dev/null
# TODO: This may be gitbox
git fetch origin > /dev/null
UPSTREAM=${1:-'@{u}'}
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse "$UPSTREAM")
BASE=$(git merge-base @ "$UPSTREAM")
popd > /dev/null
if [[ $LOCAL = $REMOTE ]]; then
return 0
elif [[ $LOCAL = $BASE ]]; then
return 2
elif [[ $REMOTE = $BASE ]]; then
return 3
else
return 4
fi
}
function python_roots {
if [ -d $HOME/.local/lib/ ]; then
for directory in $(echo $HOME/.local/lib/python*); do
echo "$directory"
done
fi
if [ -d $HOME/.local/share/pipx/venvs/powerline-status/lib/ ]; then
for directory in $(echo $HOME/.local/share/pipx/venvs/powerline-status/lib/python*); do
echo "$directory"
done
fi
if [ -d $HOME/.local/pipx/venvs/powerline-status/lib/ ]; then
for directory in $(echo $HOME/.local/pipx/venvs/powerline-status/lib/python*); do
echo "$directory"
done
fi
if [ -d /usr/lib/ ]; then
for directory in $(echo /usr/lib/python*); do
echo "$directory"
done
fi
if [ -d /usr/local/lib/ ]; then
# TODO: Make this not print warnings when the directory doesn't exit
for directory in $(echo /usr/local/lib/python*); do
echo "$directory"
done
fi
}
function powerline_bash_sources {
# To support bash 3.2 on macOS, can't use declare -ag so workaround with capturing output
PYTHON_ROOTS=( $(python_roots) )
# Powerline plugin from distribution agnostic install directory
## Assemble list of possible sources
if [[ -a $POWERLINE_ROOT/bindings/bash/powerline.sh ]]; then
# Prefer user-set powerline directory
echo "$POWERLINE_ROOT/bindings/bash/powerline.sh"
fi
if [ -d /usr/share/powerline ]; then
# Apt installs here
echo "/usr/share/powerline/bindings/bash/powerline.sh"
fi
# If $POWERLINE_ROOT not set or set incorrectly, try default locations
for directory in "${PYTHON_ROOTS[@]}"; do
if [ -d "$directory" ]; then
echo "$directory"/site-packages/powerline/bindings/bash/powerline.sh
echo "$directory"/dist-packages/powerline/bindings/bash/powerline.sh
fi
done
}
-6
View File
@@ -1,6 +0,0 @@
if [ -n "$BASH_VERSION" ]; then
# include .bashrc if it exists
if [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
fi
fi
-84
View File
@@ -1,84 +0,0 @@
#PATHs
if [ -d "$HOME/.local/bin" ]; then
export PATH="$HOME/.local/bin:$PATH"
fi
if [ -f "$HOME/.bash_functions" ]; then
source "$HOME/.bash_functions"
fi
# User profiles
source $HOME/.profile
# Define $POWERLINE_ROOT
if [ -f $HOME/.profile.local ]; then
set -a
source $HOME/.profile.local
set +a
fi
if [ -n "$DOTFILES_DIR" ] && [ -d "$DOTFILES_DIR" ]; then
# Gruvbox colors
[[ -f "$DOTFILES_DIR/dependencies/gruvbox/gruvbox_256palette.sh" ]] && source "$DOTFILES_DIR/dependencies/gruvbox/gruvbox_256palette.sh"
fi
# Italic support
if [ -f $HOME/.xterm-256color-italic.terminfo ]; then
tic $HOME/.xterm-256color-italic.terminfo
fi
if [ -f $HOME/.screen-256color-italic.terminfo ]; then
tic $HOME/.screen-256color-italic.terminfo
fi
# Use neovim where available
if program_installed neovim; then
alias vim='nvim'
fi
# fzf fuzzy finder
if program_installed fzf; then
# TODO: This is broken when fzf is old, should include a local copy
eval "$(fzf --bash)"
fi
# thefuck command correction
if program_installed fuck; then
eval "$(thefuck --alias)"
fi
# aactivator
if program_installed aactivator; then
eval "$(aactivator init)"
fi
# Preferred editor for local and remote sessions
export EDITOR='vim'
# Powerline
if program_installed powerline; then
if [ -n "$POWERLINE_ROOT" ] && \
[ -f "$POWERLINE_ROOT" ] && \
[ -f "$POWERLINE_ROOT/bindings/bash/powerline.sh" ]; then
# Skip lookup if location is known
source "$POWERLINE_ROOT/bindings/bash/powerline.sh"
else
POWERLINE_BASH_SOURCES=( $(powerline_bash_sources) )
found=0
for file in "${POWERLINE_BASH_SOURCES[@]}"; do
if [[ -a $file ]]; then
# TODO: Store the POWERLINE_ROOT in .profile.local if found
source $file
found=1
break
fi
done
if [ $found -eq 0 ]; then
echo "Powerline binding not found."
echo "Please set \$POWERLINE_ROOT in .profile.local"
fi
fi
fi
# Local overrides
if [ -f $HOME/.bashrc_local ]; then
source $HOME/.bashrc_local
fi
@@ -1,30 +0,0 @@
#!/usr/bin/env bash
# PreToolUse hook: block obviously destructive Bash commands regardless of permission mode.
# Exit 2 blocks the tool call and returns stderr to Claude as feedback.
command=$(jq -r '.tool_input.command // empty')
deny_patterns=(
'rm[[:space:]]+(-[a-zA-Z]*[rR][a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*[rR])[[:space:]]+/($|[[:space:]])'
'rm[[:space:]]+(-[a-zA-Z]*[rR][a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*[rR])[[:space:]]+(--no-preserve-root|~($|/)|\*)'
':\(\)[[:space:]]*\{[[:space:]]*:.*\|.*:.*&.*\}[[:space:]]*;[[:space:]]*:'
'dd[[:space:]].*of=/dev/(sd|nvme|vd|hd)'
'mkfs(\.[a-zA-Z0-9]+)?[[:space:]]'
'>[[:space:]]*/dev/(sd|nvme|vd|hd)[a-z0-9]*($|[[:space:]])'
'chmod[[:space:]]+-R[[:space:]]+[0-7]{3,4}[[:space:]]+/($|[[:space:]])'
'chown[[:space:]]+-R[[:space:]].*[[:space:]]+/($|[[:space:]])'
'(^|[[:space:]])(shutdown|reboot|poweroff|halt)([[:space:]]|$)'
'wipefs[[:space:]]'
'shred[[:space:]].*/dev/'
'curl[[:space:]].*\|[[:space:]]*(sudo[[:space:]]+)?(sh|bash|zsh)($|[[:space:]])'
'wget[[:space:]].*-O-.*\|[[:space:]]*(sudo[[:space:]]+)?(sh|bash|zsh)($|[[:space:]])'
)
for pattern in "${deny_patterns[@]}"; do
if [[ "$command" =~ $pattern ]]; then
echo "Blocked by local denylist hook (~/.claude/hooks/deny-dangerous-bash.sh): command matches pattern '$pattern'" >&2
exit 2
fi
done
exit 0
-89
View File
@@ -1,89 +0,0 @@
#!/usr/bin/env bash
# PreToolUse hook: inside claude-sandbox, only allow `git push` to the Gitea
# host (GITEA_SSH_HOST / the GITEA_API host), and never directly to
# main/master there - the sandbox workflow is feature branch + `gitea-pr
# create`, never a direct push, and never to GitHub at all (read-only there).
# This is a fast-fail guard against an overeager agent, not a hard security
# boundary - a sandbox session has full shell access and could in principle
# edit .git/config to route around it. No-op outside the sandbox
# (CLAUDE_SANDBOX unset), since this restriction doesn't apply to normal host
# usage.
if [ -z "${CLAUDE_SANDBOX:-}" ]; then
exit 0
fi
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
cwd=$(echo "$input" | jq -r '.cwd // empty')
# Only care about `git push` invocations.
if ! [[ "$command" =~ (^|[[:space:]&\|;])git([[:space:]]+-C[[:space:]]+[^[:space:]]+)?[[:space:]]+push([[:space:]]|$) ]]; then
exit 0
fi
repo_dir="$cwd"
if [[ "$command" =~ git[[:space:]]+-C[[:space:]]+([^[:space:]]+) ]]; then
repo_dir="${BASH_REMATCH[1]}"
fi
# Positionally parse everything after "push": first non-flag token is the
# remote, second (if any) is the refspec. Word-splitting rather than a full
# shell parse, so quoting edge cases aren't handled - same pragmatic
# tradeoff as the sibling deny-dangerous-bash.sh denylist.
after_push="${command#*push}"
read -ra tokens <<< "$after_push"
positional=()
for tok in "${tokens[@]}"; do
case "$tok" in
-*) continue ;;
*) positional+=("$tok") ;;
esac
done
remote="origin"
refspec=""
if [ "${#positional[@]}" -ge 1 ]; then
remote="${positional[0]}"
fi
if [ "${#positional[@]}" -ge 2 ]; then
refspec="${positional[1]}"
fi
remote_url=$(git -C "$repo_dir" remote get-url "$remote" 2>/dev/null)
if [ -z "$remote_url" ]; then
# Can't resolve a remote from here - let git produce the real error
# rather than block or guess.
exit 0
fi
host=$(echo "$remote_url" | sed -E 's#^[a-zA-Z]+://([^/@]+@)?([^/:]+).*#\2#; s#^[^@/]+@([^:/]+)[:/].*#\1#')
gitea_api_host=""
if [ -n "${GITEA_API:-}" ]; then
gitea_api_host=$(echo "$GITEA_API" | sed -E 's#^[a-zA-Z]+://([^/]+).*#\1#')
fi
if [ "$host" != "${GITEA_SSH_HOST:-}" ] && [ "$host" != "$gitea_api_host" ]; then
echo "Blocked by git-push-guard.sh: this sandbox may only push to the Gitea host (${GITEA_SSH_HOST:-<GITEA_SSH_HOST not set>}), not '$host'. Reading from GitHub is fine; pushing there is not." >&2
exit 2
fi
# Resolve the target branch: a src:dst refspec's dst, a bare branch name, or
# (if no refspec was given at all, or it's HEAD) the current branch - `git
# push origin` and `git push origin HEAD` both push the current branch.
if [[ "$refspec" == *:* ]]; then
branch="${refspec#*:}"
elif [ -n "$refspec" ] && [ "$refspec" != "HEAD" ]; then
branch="$refspec"
else
branch=$(git -C "$repo_dir" branch --show-current 2>/dev/null)
fi
branch="${branch#refs/heads/}"
if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then
echo "Blocked by git-push-guard.sh: direct pushes to '$branch' aren't allowed in the sandbox. Push a feature branch and run 'gitea-pr create' instead." >&2
exit 2
fi
exit 0
-78
View File
@@ -1,78 +0,0 @@
#!/usr/bin/env bash
# SessionStart hook: when running inside the claude-sandbox Docker container
# (see ~/dotfiles/docker/claude-sandbox), inject a reminder of the sandbox's
# constraints and how to escape it. No-op outside the container.
if [ -z "${CLAUDE_SANDBOX:-}" ]; then
exit 0
fi
session_id=$(jq -r '.session_id // empty')
resume_id="${session_id:-<unknown-session-id>}"
context=$(cat <<EOF
You are running inside the claude-sandbox Docker container
(~/dotfiles/docker/claude-sandbox), not directly on the user's machine, with
--dangerously-skip-permissions active (no tool-use confirmations).
What this means:
- The workspace directory is bind-mounted read-write and is the real
project. ~/shared is also a real, persistent host directory (not part of
the project) - use it to hand the user a file (a screenshot, a temp
script) without touching the project checkout, or to pick up something
they left there for you. Everything else in the container besides
~/.claude is disposable: installed system packages, anything outside the
mounted paths, and the container itself go away when this run ends.
- Outbound network is restricted to an allowlist (Anthropic's API, the npm
registry, GitHub read-only, the configured Gitea host, plus anything set
via ALLOWED_DOMAINS). Requests to anything else will simply fail to
connect. If a task needs one more domain and nothing else, that's a quick
fix, not a reason to stop: tell the user to run
\`bin/claude-sandbox-allow <domain>\` (add \`--global\` to persist it past
this session) from a terminal outside the sandbox, then continue.
- Stay within the task you were given. Since there are no permission
prompts here, be more conservative than usual about anything destructive,
irreversible, or outside the project directory.
Git workflow - do this even though nothing stops you from doing otherwise:
- Do new work in a git worktree for the feature branch, not by switching
branches in place in the mounted checkout - that's the user's actual
working copy, and changing its branch out from under them is disruptive
even if you switch it back.
- The worktree MUST live under the mounted project directory itself, e.g.
\`git worktree add .worktrees/<branch> -b <branch>\` run from the project
root - never \`../<branch>\` or any other path outside it, including
anywhere under \$HOME. Only the exact directory bind-mounted into this
container is backed by real storage on the host; everything else,
including what looks like the project's own parent directory, is
container-local and silently disappears when the session ends - a
worktree created there is real work that just gets thrown away. Add
\`.worktrees/\` to \`.git/info/exclude\` (not \`.gitignore\` - this is
sandbox-local, not something to commit) so it doesn't clutter \`git
status\` for the user's real checkout.
- Push that branch to the Gitea remote, then run \`gitea-pr create\` from
the worktree. Never push to GitHub (read-only, and not reachable for
push regardless), and never push directly to main/master, on any remote.
A \`git-push-guard.sh\` hook enforces both as a backstop, but treat them
as the actual workflow to follow, not just a filter to route around.
If a task genuinely needs something else the sandbox can't provide -
broader network access than a domain or two, host-level or system changes,
GUI/hardware access - stop and explain clearly to the user what you need
and why, instead of trying to work around the firewall or sandbox yourself.
They can continue this exact session outside the container by running on
the host:
claude --resume $resume_id
(session history is shared via the bind-mounted ~/.claude, so it will pick
up right where this left off).
EOF
)
jq -n --arg context "$context" '{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": $context
}
}'
-30
View File
@@ -1,30 +0,0 @@
{
"name": "Gruvbox Dark",
"base": "dark",
"overrides": {
"claude": "#d3869b",
"text": "#ebdbb2",
"inactive": "#665c54",
"subtle": "#7c6f64",
"muted": "#928374",
"success": "#b8bb26",
"error": "#fb4934",
"warning": "#fabd2f",
"link": "#83a598",
"highlight": "#fabd2f",
"selection": "#504945",
"diffAdded": "#b8bb26",
"diffRemoved": "#fb4934",
"promptBorder": "#504945",
"bashBorder": "#504945",
"mcpBorder": "#504945",
"planMode": "#83a598",
"spinner": "#d3869b",
"header": "#8ec07c",
"separator": "#3c3836",
"badge": "#fe8019",
"permission": "#fe8019",
"suggestion": "#8ec07c",
"fastMode": "#fabd2f"
}
}
-84
View File
@@ -1,84 +0,0 @@
@define-color borders_breeze #4c4e51;
@define-color content_view_bg_breeze #141618;
@define-color error_color_backdrop_breeze #da4453;
@define-color error_color_breeze #da4453;
@define-color error_color_insensitive_backdrop_breeze #55242b;
@define-color error_color_insensitive_breeze #55242b;
@define-color insensitive_base_color_breeze #131517;
@define-color insensitive_base_fg_color_breeze #606263;
@define-color insensitive_bg_color_breeze #1f2124;
@define-color insensitive_borders_breeze #2d3033;
@define-color insensitive_fg_color_breeze #686a6c;
@define-color insensitive_selected_bg_color_breeze #1f2124;
@define-color insensitive_selected_fg_color_breeze #686a6c;
@define-color insensitive_unfocused_bg_color_breeze #1f2124;
@define-color insensitive_unfocused_fg_color_breeze #686a6c;
@define-color insensitive_unfocused_selected_bg_color_breeze #1f2124;
@define-color insensitive_unfocused_selected_fg_color_breeze #686a6c;
@define-color link_color_breeze #926ee4;
@define-color link_visited_color_breeze #9b59b6;
@define-color success_color_backdrop_breeze #27ae60;
@define-color success_color_breeze #27ae60;
@define-color success_color_insensitive_backdrop_breeze #19482f;
@define-color success_color_insensitive_breeze #19482f;
@define-color theme_base_color_breeze #141618;
@define-color theme_bg_color_breeze #202326;
@define-color theme_button_background_backdrop_breeze #292c30;
@define-color theme_button_background_backdrop_insensitive_breeze #272a2e;
@define-color theme_button_background_insensitive_breeze #272a2e;
@define-color theme_button_background_normal_breeze #292c30;
@define-color theme_button_decoration_focus_backdrop_breeze #926ee4;
@define-color theme_button_decoration_focus_backdrop_insensitive_breeze #4a406a;
@define-color theme_button_decoration_focus_breeze #926ee4;
@define-color theme_button_decoration_focus_insensitive_breeze #4a406a;
@define-color theme_button_decoration_hover_backdrop_breeze #926ee4;
@define-color theme_button_decoration_hover_backdrop_insensitive_breeze #4a406a;
@define-color theme_button_decoration_hover_breeze #926ee4;
@define-color theme_button_decoration_hover_insensitive_breeze #4a406a;
@define-color theme_button_foreground_active_backdrop_breeze #fcfcfc;
@define-color theme_button_foreground_active_backdrop_insensitive_breeze #686a6c;
@define-color theme_button_foreground_active_breeze #ffffff;
@define-color theme_button_foreground_active_insensitive_breeze #686a6c;
@define-color theme_button_foreground_backdrop_breeze #fcfcfc;
@define-color theme_button_foreground_backdrop_insensitive_breeze #6d6f72;
@define-color theme_button_foreground_insensitive_breeze #6d6f72;
@define-color theme_button_foreground_normal_breeze #fcfcfc;
@define-color theme_fg_color_breeze #fcfcfc;
@define-color theme_header_background_backdrop_breeze #202326;
@define-color theme_header_background_breeze #292c30;
@define-color theme_header_background_light_breeze #202326;
@define-color theme_header_foreground_backdrop_breeze #fcfcfc;
@define-color theme_header_foreground_breeze #fcfcfc;
@define-color theme_header_foreground_insensitive_backdrop_breeze #fcfcfc;
@define-color theme_header_foreground_insensitive_breeze #fcfcfc;
@define-color theme_hovering_selected_bg_color_breeze #926ee4;
@define-color theme_selected_bg_color_breeze #6c53a6;
@define-color theme_selected_fg_color_breeze #ffffff;
@define-color theme_text_color_breeze #fcfcfc;
@define-color theme_titlebar_background_backdrop_breeze #202326;
@define-color theme_titlebar_background_breeze #292c30;
@define-color theme_titlebar_background_light_breeze #202326;
@define-color theme_titlebar_foreground_backdrop_breeze #fcfcfc;
@define-color theme_titlebar_foreground_breeze #fcfcfc;
@define-color theme_titlebar_foreground_insensitive_backdrop_breeze #fcfcfc;
@define-color theme_titlebar_foreground_insensitive_breeze #fcfcfc;
@define-color theme_unfocused_base_color_breeze #141618;
@define-color theme_unfocused_bg_color_breeze #202326;
@define-color theme_unfocused_fg_color_breeze #fcfcfc;
@define-color theme_unfocused_selected_bg_color_alt_breeze #362c50;
@define-color theme_unfocused_selected_bg_color_breeze #362c50;
@define-color theme_unfocused_selected_fg_color_breeze #fcfcfc;
@define-color theme_unfocused_text_color_breeze #fcfcfc;
@define-color theme_unfocused_view_bg_color_breeze #131517;
@define-color theme_unfocused_view_text_color_breeze #606263;
@define-color theme_view_active_decoration_color_breeze #926ee4;
@define-color theme_view_hover_decoration_color_breeze #926ee4;
@define-color tooltip_background_breeze #292c30;
@define-color tooltip_border_breeze #535659;
@define-color tooltip_text_breeze #fcfcfc;
@define-color unfocused_borders_breeze #4c4e51;
@define-color unfocused_insensitive_borders_breeze #2d3033;
@define-color warning_color_backdrop_breeze #f67400;
@define-color warning_color_breeze #f67400;
@define-color warning_color_insensitive_backdrop_breeze #5e340f;
@define-color warning_color_insensitive_breeze #5e340f;
-1
View File
@@ -1,4 +1,3 @@
VteTerminal, vte-terminal {
padding: 13px;
}
@import 'colors.css';
+9 -18
View File
@@ -1,25 +1,16 @@
[Settings]
gtk-application-prefer-dark-theme=true
gtk-button-images=true
gtk-cursor-blink=true
gtk-cursor-blink-time=1000
gtk-cursor-theme-name=breeze_cursors
gtk-cursor-theme-size=24
gtk-decoration-layout=icon:minimize,maximize,close
gtk-enable-animations=true
gtk-font-name=Liberation Sans Regular 10
gtk-theme-name=Gruvbox-Dark-BL
gtk-icon-theme-name=Gruvbox_Dark
gtk-cursor-theme-name=Gruvbox-cursors
gtk-cursor-theme-size=16
gtk-toolbar-style=GTK_TOOLBAR_BOTH_HORIZ
gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR
gtk-button-images=1
gtk-menu-images=1
gtk-enable-event-sounds=0
gtk-enable-input-feedback-sounds=0
gtk-font-name=Noto Sans, 10
gtk-icon-theme-name=breeze-dark
gtk-menu-images=true
gtk-modules=colorreload-gtk-module
gtk-primary-button-warps-slider=true
gtk-sound-theme-name=ocean
gtk-theme-name=Gruvbox-Dark-BL
gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR
gtk-toolbar-style=3
gtk-xft-antialias=1
gtk-xft-dpi=122880
gtk-xft-hinting=1
gtk-xft-hintstyle=hintmedium
gtk-xft-rgba=none
-9
View File
@@ -22,15 +22,6 @@ set $ws7 "7"
set $ws8 "8"
set $ws9 "9"
set $ws10 "10"
set $ws11 "11"
set $ws12 "12"
set $ws13 "13"
set $ws14 "14"
set $ws15 "15"
set $ws16 "16"
set $ws17 "17"
set $ws18 "18"
set $ws19 "19"
# Define colors variables:
# gruvbox colors
+9 -10
View File
@@ -12,19 +12,20 @@ exec --no-startup-id numlockx on
# use arandr to setup displays and save the file as default:
exec --no-startup-id ~/.screenlayout/default.sh
# set wallpaper
exec --no-startup-id sleep 1 && feh --bg-fill --randomize ~/wallpapers
# set powersavings for display if not handled by DE:
# Usage: xset dpms {standby_time} {suspend_time} {off_time}
#exec --no-startup-id xset dpms 600 0 0
# exec --no-startup-id xset dpms 600 0 0
# disable power saving (for example if using xscreensaver)
exec --no-startup-id xset -dpms
#exec --no-startup-id xset -dpms
# use xautolock to use autosuspend rules for mobile devices
# https://wiki.archlinux.org/title/Session_lock#xautolock
#exec --no-startup-id xautolock -time 60 -locker "systemctl suspend"
# set wallpaper(s) on i3 start/restart
exec_always --no-startup-id "[ -f ~/.dotfiles-config ] && source ~/.dotfiles-config && $DOTFILES_DIR/bin/update-wallpaper"
# xscreensaver
# https://www.jwz.org/xscreensaver
@@ -44,17 +45,15 @@ exec --no-startup-id /usr/bin/dunst
# Scratchpad with notes
# `tmuxinator open notes` first for this to work
exec --no-startup-id alacritty --title notebook -e zsh -c 'tmuxinator start notes'
for_window [class="^Alacritty$" title="notebook"] move scratchpad
#, scratchpad hide
for_window [class="^Alacritty$" title="notebook"] move scratchpad, scratchpad hide
# Performance monitor
exec --no-startup-id alacritty --title monitoring -e zsh -c 'htop'
for_window [class="^Alacritty$" title="monitoring"] move scratchpad
#, scratchpad hide
for_window [class="^Alacritty$" title="monitoring"] move scratchpad, scratchpad hide
# Remote connections
# exec --no-startup-id alacritty --title servers -e zsh -c 'tmuxinator start servers'
# for_window [class="^Alacritty$" title="servers"] move scratchpad, scratchpad hide
exec --no-startup-id alacritty --title servers -e zsh -c 'tmuxinator start servers'
for_window [class="^Alacritty$" title="servers"] move scratchpad, scratchpad hide
###############
# system tray #
+5 -31
View File
@@ -7,15 +7,10 @@ floating_modifier $mod
# switch/iterate between workspaces
bindsym $mod+Tab workspace next
bindsym $mod+Shift+Tab workspace prev
bindsym $mod+c exec --no-startup-id ~/.config/i3/scripts/empty_workspace
bindsym $mod+Shift+c exec --no-startup-id ~/.config/i3/scripts/move_container_empty_workspace
bindsym $mod+slash [class="^Alacritty$" title="notebook"] scratchpad show
# bindsym $mod+slash scratchpad show
bindsym $mod+m [class="^Alacritty$" title="monitoring"] scratchpad show
# bindsym $mod+Shift+colon [class="^Alacritty$" title="servers"] scratchpad show
bindsym $mod+Shift+slash move scratchpad
bindsym $mod+Control+slash scratchpad show
bindsym $mod+Shift+colon [class="^Alacritty$" title="servers"] scratchpad show
# switch to workspace
bindsym $mod+1 workspace $ws1
@@ -167,8 +162,7 @@ mode "$mode_gaps_outer" {
######################################
# start a terminal
bindsym $mod+Return exec --no-startup-id alacritty -e sh -c "tmux new-session"
# bindsym $mod+Return exec --no-startup-id alacritty
bindsym $mod+Return exec --no-startup-id alacritty
# kill focused window
bindsym $mod+q kill
@@ -177,7 +171,7 @@ bindsym $mod+q kill
#bindsym --release button2 kill
# reload the configuration file
bindsym $mod+Control+r reload
bindsym $mod+Shift+c reload
# restart i3 inplace (preserves your layout/session, can be used to update i3)
bindsym $mod+Shift+r restart
@@ -212,19 +206,7 @@ bindsym $mod+Shift+Down move down
bindsym $mod+Shift+Up move up
bindsym $mod+Shift+Right move right
# move focused workspace
bindsym $mod+Control+h move workspace to output left
bindsym $mod+Control+j move workspace to output down
bindsym $mod+Control+k move workspace to output up
bindsym $mod+Control+l move workspace to output right
# alternatively, you can use the cursor keys:
bindsym $mod+Control+Left move workspace to output left
bindsym $mod+Control+Down move workspace to output down
bindsym $mod+Control+Up move workspace to output up
bindsym $mod+Control+Right move workspace to output right
# split in vertical orientation
# splij in vertical orientation
bindsym $mod+minus split v
bindsym $mod+KP_Subtract split v
@@ -272,7 +254,6 @@ bindsym $mod+p exec --no-startup-id /usr/local/bin/switch-audio-port
## App shortcuts
bindsym $mod+w exec /usr/bin/firefox
bindsym Print exec scrot ~/Pictures/Screenshots/%Y-%m-%d-%T-screenshot.png && notify-send "Screenshot saved to ~/Pictures/Screenshots/$(date +"%Y-%m-%d-%T")-screenshot.png"
#bindsym Print exec spectactle --output ~/Pictures/Screenshots/%Y-%m-%d-%T-screenshot.png
# Power Profiles menu switcher (rofi)
bindsym $mod+Shift+p exec --no-startup-id ~/.config/i3/scripts/power-profiles
@@ -291,12 +272,5 @@ bindsym $mod+t exec --no-startup-id rofi -show window \
## rofi bindings to manage clipboard (install rofi-greenclip from the AUR)
exec --no-startup-id greenclip daemon>/dev/null
bindsym $mod+b exec --no-startup-id rofi -modi "clipboard:greenclip print" -show clipboard \
bindsym $mod+c exec --no-startup-id rofi -modi "clipboard:greenclip print" -show clipboard \
-config ~/.config/rofi/rofidmenu.rasi
## autoclicker x 100
bindsym $mod+Shift+o exec --no-startup-id xdotool click --repeat 1000 --delay 10 1
bindsym $mod+Shift+i exec --no-startup-id xdotool click --repeat 10000 --delay 10 1
# calculator
bindsym XF86Calculator exec --no-startup-id galculator
+1
View File
@@ -9,6 +9,7 @@ default_border pixel 5
# thin borders
hide_edge_borders smart
smart_borders on
# Set inner/outer gaps
gaps inner 10
+15
View File
@@ -13,3 +13,18 @@ workspace_layout default
# Each new window will open fullscreen as a tab (left to right)
#workspace_layout tabbed
##########################################
# configuration for workspace behaviour: #
##########################################
# use workspaces on different displays:
# where you have to replace VGA-0/HDMI-0 with the names for your displays
# you can get from xrandr command
#workspace $ws1 output DP-2
#workspace $ws3 output DP-2
#workspace $ws5 output DP-2
#workspace $ws2 output DP-4
#workspace $ws4 output DP-4
#workspace $ws6 output DP-4
exec i3-msg workspace 2
+1
View File
@@ -0,0 +1 @@
/home/brad/dotfiles/config/plasma-i3/i3-bar.conf
+1
View File
@@ -0,0 +1 @@
/home/brad/dotfiles/config/plasma-i3/i3-keybindings.conf
-102
View File
@@ -1,102 +0,0 @@
# i3blocks config file changed for EndeavourOS-i3 setup
# source is available here:
# https://raw.githubusercontent.com/endeavouros-team/endeavouros-i3wm-setup/main/.config/i3/i3blocks.conf
# Maintainer: joekamprad [joekamprad //a_t// endeavouros.com]
# Former Visual Designer: Florent Valetti [@FLVAL EndeavourOS]
# created for i3wm setup on EndeavourOS
# https://endeavouros.com
# cheatsheet for icon fonts used on the block-bar:
# https://fontawesome.com/v4.7/cheatsheet/
# --> to update this run the following command:
# wget --backups=1 https://raw.githubusercontent.com/endeavouros-team/endeavouros-i3wm-setup/main/.config/i3/i3blocks.conf -P ~/.config/i3/
# Please see man i3blocks for a complete reference!
# The man page is also hosted at http://vivien.github.io/i3blocks
# List of valid properties:
#
# align
# color
# command
# full_text
# instance
# interval
# label
# min_width
# name
# separator
# separator_block_width
# short_text
# signal
# urgent
# Global properties
#
# The top properties below are applied to every block, but can be overridden.
separator=false
markup=pango
separator_block_width=15
#[Weather]
#command=~/.config/i3/scripts/openweather
# or:
#command=~/.config/i3/scripts/openweather-city
#interval=1800
#color=#7275b3
separator_block_width=10
[spacer]
separator_block_width=10
full_text=
color=#b16286
[pavucontrol]
separator_block_width=10
full_text=
command=pavucontrol
color=#689d6a
[volume-pulseaudio]
command=~/.config/i3/scripts/volume
instance=Master
color=#689d6a
interval=1
separator_block_width=10
[spacer]
separator_block_width=10
full_text=
color=#b16286
[time]
label=
command=date '+%a %b %d %I:%M:%S %p'
color=#98971a
interval=1
separator_block_width=10
[spacer]
separator_block_width=10
full_text=
color=#b16286
[keybindings]
full_text=
command=~/.config/i3/scripts/keyhint
[sleep_screen]
full_text=🖵
command=~/.config/i3/scripts/blur-lock
[shutdown_menu]
full_text=
command=~/.config/i3/scripts/powermenu
separator_block_width=15
[simple-2]
full_text=
color=#b16286
@@ -1,10 +0,0 @@
#!/usr/bin/env bash
MAX_DESKTOPS=20
WORKSPACES=$(seq -s '\n' 1 1 ${MAX_DESKTOPS})
EMPTY_WORKSPACE=$( (i3-msg -t get_workspaces | tr ',' '\n' | grep num | awk -F: '{print int($2)}' ; \
echo -e ${WORKSPACES} ) | sort -n | uniq -u | head -n 1)
i3-msg move container to workspace ${EMPTY_WORKSPACE}
-63
View File
@@ -1,63 +0,0 @@
" .ideavimrc is a configuration file for IdeaVim plugin. It uses
" the same commands as the original .vimrc configuration.
" You can find a list of commands here: https://jb.gg/h38q75
" Find more examples here: https://jb.gg/share-ideavimrc
"" -- Suggested options --
" Show a few lines of context around the cursor. Note that this makes the
" text scroll if you mouse-click near the start or end of the window.
set scrolloff=5
" Do incremental searching.
set incsearch
" Don't use Ex mode, use Q for formatting.
map Q gq
" --- Enable IdeaVim plugins https://jb.gg/ideavim-plugins
" Highlight copied text
Plug 'machakann/vim-highlightedyank'
" Commentary plugin
Plug 'tpope/vim-commentary'
"" -- Map IDE actions to IdeaVim -- https://jb.gg/abva4t
"" Map \r to the Reformat Code action
"map \r <Action>(ReformatCode)
"" Map <leader>d to start debug
"map <leader>d <Action>(Debug)
"" Map \b to toggle the breakpoint on the current line
"map \b <Action>(ToggleLineBreakpoint)
inoremap ,, <ESC>
cnoremap ,, <ESC><ESC>
nmap j gj
nmap k gk
" Only set on first run or it overwrites YCM behavior
" if has('vim_starting')
" imap <tab> <C-X><C-O>
" endif
let mapleader=","
let maplocalleader="\\"
" Close buffer
map <leader><leader>c :Bclose<CR>
" Prev/next buffer
map <leader>q :bp<CR>
map <leader>w :bn<CR>
" System clipboard cut copy paste
map <leader>y "+y
map <leader>Y "+Y
map <leader>p "+p
map <leader>P "+P
map <leader>x "+x
map <leader>dd "+dd
" Update ideavimrc
nnoremap <leader>v :e ~/.ideavimrc<CR>;
nnoremap <leader>rr :action IdeaVim.ReloadVimRc.reload<CR>;
+1 -4
View File
@@ -1,4 +1 @@
NO_NEOFETCH=1
NO_GIT_CHECK=1
NO_ZSH_COWSAY=1
LOAD_SSH_KEYS=0
POWERLINE_ROOT=/usr/share/powerline
-143
View File
@@ -1,143 +0,0 @@
# nvim config map
Lua config managed by [lazy.nvim](https://github.com/folke/lazy.nvim). Leader is `,`, local leader is `\`.
## Layout
- `init.lua` — bootstraps lazy.nvim, loads `config/` then `plugins/`
- `lua/config/options.lua` — `vim.opt` settings
- `lua/config/keymaps.lua` — keymaps not tied to a specific plugin
- `lua/config/autocmds.lua` — autocommands
- `lua/plugins/*.lua` — one lazy.nvim spec file per topic (each file returns a plugin spec table; lazy.nvim loads every file in the directory automatically)
- `ftplugin/*.lua` — filetype-specific settings (Python, vimwiki)
- `lua/util.lua` — small helper functions (`ranger()`, `delete_trailing_ws()`) used from keymaps
## Plugins
### Navigation & editing (`plugins/editor.lua`)
- **[neo-tree.nvim](https://github.com/nvim-neo-tree/neo-tree.nvim)** — file explorer sidebar
- `<leader>t` — toggle
- `<leader>cd` — cd to current file's directory and open
- **[telescope.nvim](https://github.com/nvim-telescope/telescope.nvim)** — fuzzy finder for files/buffers/grep/LSP symbols
- `<leader>.` — jump to symbol
- `<leader>;` — jump to buffer
- `<leader>be` — jump to buffer, sorted by most recently used
- `<leader>ag` — live grep
- `<leader>?` — fuzzy-search all keybindings
- **[flash.nvim](https://github.com/folke/flash.nvim)** — jump to any visible location via search-like labels
- `<leader>/` — jump
- `<leader>s` — search jump
- `<leader><leader>s` — treesitter jump
- `<leader>f` — forward jump
- **[nvim-surround](https://github.com/kylechui/nvim-surround)** — add/change/delete surrounding pairs (quotes, tags, brackets)
- `ys`, `cs`, `ds` — plugin defaults
- **[nvim-autopairs](https://github.com/windwp/nvim-autopairs)** — auto-close brackets/quotes, automatic
- **[nvim-treesitter-context](https://github.com/nvim-treesitter/nvim-treesitter-context)** — sticky header showing enclosing function/class while scrolling, automatic
- **[mini.align](https://github.com/echasnovski/mini.nvim)** — align text on a character
- `ga=`, `ga:`
- **[mini.bufremove](https://github.com/echasnovski/mini.nvim)** — close a buffer without closing its window/split
- `<leader><leader>c`
- **[vim-visual-multi](https://github.com/mg979/vim-visual-multi)** — multiple cursors
- `<C-n>` — add cursor on word
- **[vim-tmux-navigator](https://github.com/christoomey/vim-tmux-navigator)** — move between nvim splits and tmux panes; also works from inside `:terminal` buffers (e.g. the Claude Code terminal)
- `<C-h/j/k/l>` — move left/down/up/right
- **[emmet-vim](https://github.com/mattn/emmet-vim)** — Emmet expansion, HTML/CSS files only
### UI (`plugins/ui.lua`)
- **[gruvbox.nvim](https://github.com/ellisonleao/gruvbox.nvim)** — colorscheme
- **[lualine.nvim](https://github.com/nvim-lualine/lualine.nvim)** — statusline
- **[bufferline.nvim](https://github.com/akinsho/bufferline.nvim)** — buffer tabs along the top
- **[indent-blankline.nvim](https://github.com/lukas-reineke/indent-blankline.nvim)** — indent guides
- **[rainbow-delimiters.nvim](https://github.com/HiPhish/rainbow-delimiters.nvim)** — rainbow-colored matching brackets
- **[nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons)** — filetype icons (dependency for several plugins above)
- **[alpha-nvim](https://github.com/goolord/alpha-nvim)** — start screen shown on launch, see [Dashboard](#dashboard) below
- `<leader><leader>o` — reopen
### Completion & LSP
- **[blink.cmp](https://github.com/Saghen/blink.cmp)** — completion engine (LSP, path, buffer sources) — `plugins/completion.lua`
- **[mason.nvim](https://github.com/mason-org/mason.nvim)** / **[mason-lspconfig.nvim](https://github.com/mason-org/mason-lspconfig.nvim)** — installs/manages LSP servers — `plugins/lsp.lua`
- **[nvim-lspconfig](https://github.com/neovim/nvim-lspconfig)** — LSP client config for `ts_ls`, `svelte`, `html`, `cssls`, `basedpyright`, `ruff`, `bashls` — `plugins/lsp.lua`
- **[rustaceanvim](https://github.com/mrcjkb/rustaceanvim)** — Rust LSP setup (`rust_analyzer`), replaces the standard lspconfig flow for Rust — `plugins/rust.lua`
- `<localleader>rr` — cargo run
LSP buffer keymaps (set on `LspAttach`, see `plugins/lsp.lua`):
- `K` — hover
- `gd` — definition
- `gy` — type definition
- `gi` — implementation
- `gr` — references
- `rn` — rename
- `ff` (visual) — format selection
### Debugging (`plugins/dap.lua`)
- **[nvim-dap](https://github.com/mfussenegger/nvim-dap)** — Debug Adapter Protocol client
- `<leader>db` — toggle breakpoint
- `<leader>dc` — continue / start debugging
- `<leader>di` / `<leader>do` / `<leader>dO` — step into / over / out
- `<leader>dr` — toggle REPL
- `<leader>dq` — terminate session
- **[nvim-dap-python](https://github.com/mfussenegger/nvim-dap-python)** — configures nvim-dap for Python using `debugpy`. Requires `:MasonInstall debugpy`; without it, debugging is skipped with a warning.
### Formatting & linting (`plugins/format.lua`)
- **[conform.nvim](https://github.com/stevearc/conform.nvim)** — format-on-save (prettier, ruff_format, rustfmt, stylua per filetype)
- **[nvim-lint](https://github.com/mfussenegger/nvim-lint)** — async linting on save/insert-leave (eslint_d, shellcheck)
### Treesitter (`plugins/treesitter.lua`)
**[nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter)** — syntax highlighting, folding, and indentation for the installed parser list. Uses core Neovim's built-in `vim.treesitter.start()`/foldexpr rather than the old treesitter modules.
### Git (`plugins/git.lua`)
- **[gitsigns.nvim](https://github.com/lewis6991/gitsigns.nvim)** — git change signs in the gutter, hunk stage/preview/blame
- **[vim-fugitive](https://github.com/tpope/vim-fugitive)** — Git commands
- `:Git`, `:Gdiffsplit`, `:Gvdiffsplit`, `:Gread`, `:Gwrite`, `:Gclog`
### AI (`plugins/ai.lua`)
- **[snacks.nvim](https://github.com/folke/snacks.nvim)** — utility library (terminal provider etc.), dependency for claudecode.nvim
- **[claudecode.nvim](https://github.com/coder/claudecode.nvim)** — Claude Code terminal integration. Diffs render as a single unified (VS Code-style, red/green interleaved) buffer in one new split next to your active pane, and keyboard focus stays in the Claude terminal instead of jumping to the diff.
- `<leader>ac` — toggle
- `<leader>af` — focus
- `<leader>ab` — add current buffer
- `<leader>as` (visual) — send selection
- `<leader>aa` / `<leader>ad` — accept/deny diff
### Wiki / notes (`plugins/vimwiki.lua`)
- **[vimwiki](https://github.com/vimwiki/vimwiki)** — personal wiki across 4 notebooks (bradwiki, work, private, shared under `~/synced-notebooks/`)
- `<leader>m...` — wiki keymaps (`vim.g.vimwiki_map_prefix`)
- **[vim-zettel](https://github.com/michal-h21/vim-zettel)** — Zettelkasten-style note linking on top of vimwiki
- **[vim-table-mode](https://github.com/dhruvasagar/vim-table-mode)** — markdown/wiki table formatting
- `:TableModeToggle`, or `<leader><leader>t...` prefix
- **[markdown-preview.nvim](https://github.com/iamcco/markdown-preview.nvim)** — live browser preview for markdown/vimwiki
- `<localleader>v` — toggle preview
### Misc (`plugins/misc.lua`)
- **[aerial.nvim](https://github.com/stevearc/aerial.nvim)** — code outline/symbol sidebar
- `<leader>ct` — toggle
- **[undotree](https://github.com/jiaoshijie/undotree)** — visual undo history tree
- `<leader>u` — toggle
- **[persistence.nvim](https://github.com/folke/persistence.nvim)** — session save/restore per project
- restore from the dashboard's `s` button
- **[which-key.nvim](https://github.com/folke/which-key.nvim)** — shows available keybindings as you type a prefix (e.g. pause after `<leader>`), automatic
## Dashboard
The start screen (shown on launch, reopen with `<leader><leader>o`) is [alpha-nvim](https://github.com/goolord/alpha-nvim), configured in `plugins/ui.lua`. It shows a random `fortune | cowthink` header plus buttons:
- `e` — new file
- `f` — find file (Telescope)
- `g` — live grep (Telescope)
- `s` — restore last session (persistence.nvim)
- `q` — quit
## Buffer navigation
`<leader>be` fuzzy-searches open buffers via Telescope, sorted by most recently used, excluding the current buffer. `<leader>;` does the same but in default Telescope order. Both are fuzzy-filterable as you type.
-21
View File
@@ -1,21 +0,0 @@
" Source config files
source $HOME/.config/nvim/config/plugin.vimrc
source $HOME/.config/nvim/config/bindings-vimwiki.vimrc
source $HOME/.config/nvim/config/init.vimrc
source $HOME/.config/nvim/config/general.vimrc
source $HOME/.config/nvim/config/bindings.vimrc
source $HOME/.config/nvim/config/functions.vimrc
" Python dependencies
if isdirectory(expand($HOME.'/.pyenv/versions/neovim2'))
let g:python_host_prog=expand($HOME.'/.pyenv/versions/neovim2/bin/python')
endif
if isdirectory(expand($HOME.'/.pyenv/versions/neovim3'))
let g:python3_host_prog=expand($HOME.'/.pyenv/versions/neovim3/bin/python')
endif
" Local overrides
let $LOCALFILE=expand("$HOME/.nvim_local.vimrc")
if filereadable($LOCALFILE)
source $LOCALFILE
endif
+78
View File
@@ -0,0 +1,78 @@
vim.cmd([[
function! MapVimwiki()
" grep: if exists('g:loaded_vimwiki')
" double m
nmap <leader>mm <Plug>VimwikiIndex
" m wiki
nmap <leader>mw <Plug>VimwikiUISelect
" m help
nmap <leader>mhelp :verbose map ,m<CR>
" m go
nmap <leader>mg <Plug>VimwikiGoto
" m personal
nmap <leader>mf <Plug>VimwikiUISelect1<CR>
" m work
nmap <leader>m<Space> <Plug>VimwikiUISelect2<CR>
" m private
nmap <leader>mp <Plug>VimwikiUISelect3<CR>
" m shared
nmap <leader>ms <Plug>VimwikiUISelect4<CR>
" <C-Enter> and <S-Enter> (the original bindings for these) don't work
" because they send the same keycode and probably other reasons
nmap <leader>m- <Plug>VimwikiSplitLink
nmap <leader>m<bar> <Plug>VimwikiVSplitLink
nmap <leader>mnt <Plug>VimwikiNextTask
" TODO This command doesn't exist
" nmap <leader>mN <Plug>VimwikiPrevTask
nmap <leader>mnl <Plug>VimwikiNextLink
nmap <leader>mNl <Plug>VimwikiPrevLink
" nmap <leader>m/ <Plug>VimwikiSearch /
" nmap <leader>maq <Plug>ZettelOpen
nmap <Tab> <Plug>VimwikiIncreaseLvlSingleItem
nmap <S-Tab> <Plug>VimwikiDecreaseLvlSingleItem
imap <Tab> <Plug>VimwikiIncreaseLvlSingleItem
imap <S-Tab> <Plug>VimwikiDecreaseLvlSingleItem
vmap <Tab> <Plug>VimwikiIncreaseLvlSingleItem
vmap <S-Tab> <Plug>VimwikiDecreaseLvlSingleItem
" This is how to map insert mode plug commands <Esc><Plug>{command}a
" This always jumps to the end of the line, but with 'a' it doesn't skip
" the [ ] when adding at the start of the line
imap <C-Space> <Esc><Plug>VimwikiToggleListItemA
nmap <C-d><C-Space> <Plug>VimwikiRemoveSingleCB
imap <C-d><C-Space> <Esc><Plug>VimwikiRemoveSingleCBa
nmap <leader>mb <Plug>VimwikiBackLinks
" nmap <leader>m\ <Plug>VimwikiTable
nmap <leader>mqtoc <Plug>VimwikiTOC
nmap <leader>mqgtl <Plug>VimwikiRebuildTags<CR><Plug>VimwikiGenerateTagLinks<CR>
" Insert link from system clipboard
vmap <leader>mk c[]<Esc>Pea()<Esc>"+P
imap <leader>mk <Esc>viW<C-k>ea
nmap <leader>mk ciw[]<Esc>Pea()<Esc>"+P
" TODO: None of these work
" nmap <leader>m| :VimwikiVSplitLink 1 1<CR>
" nmap <leader>m- :VimwikiSplitLink 1 1<CR>
" vmap <leader>m- :VimwikiSplitLink 1 1<CR>
" Vim-Zettel
" TODO The <Plug> syntax is correct but broken
" autocmd Filetype vimwiki nmap <leader>mag <Plug>ZettelOpen
autocmd Filetype vimwiki nmap <leader>mag :ZettelOpen<CR>
" autocmd Filetype vimwiki nmap <leader>mp0 :ZettelOpen<CR>p0
" autocmd Filetype vimwiki nmap <leader>mp1 :ZettelOpen<CR>p1
" TODO This doesn't work
autocmd Filetype vimwiki nmap <leader>mat :ZettelOpen<CR>title:
endfunction
augroup Vimwiki_Mappings
autocmd!
" TODO: This isn't working, vimwiki bindings are not being initialized
autocmd FileType vimwiki :call MapVimwiki()
augroup END
" TODO This shouldn't be necessary, why does this need to be called before vimwiki
" and after startup?
" :call MapVimwiki()
]])
+191
View File
@@ -0,0 +1,191 @@
vim.cmd([[
" --- Mappings ---
" Explanation of mapping commands: https://stackoverflow.com/a/3776182
" - General -
" TODO: Remap this to easymotion but don't step on other bindings
" noremap <space> :
inoremap ,, <ESC>
cnoremap ,, <ESC><ESC>
nmap j gj
nmap k gk
" Only set on first run or it overwrites YCM behavior
" if has('vim_starting')
" imap <tab> <C-X><C-O>
" endif
let mapleader=","
let maplocalleader="\\"
" Clear highlighting on ,, in normal mode
map <leader><leader><ESC> :nohl<CR>
" Enable hex mode
map <leader>\he :%!xxd<CR>
" Disable hex mode
map <leader>\hd :%!xxd -r<CR>
" Show whitespace
nmap <leader><leader>lw :set list!<CR>
" List registers
map <leader><leader>lr :reg<CR>
" List marks
map <leader><leader>lm :marks<CR>
" Close buffer
map <leader><leader>c :Bclose<CR>
" Prev/next buffer
map <leader>q :bp<CR>
map <leader>w :bn<CR>
" System clipboard cut copy paste
map <leader>y "+y
map <leader>Y "+Y
map <leader>p "+p
map <leader>P "+P
map <leader>x "+x
map <leader>dd "+dd
" Line break above
" TODO urxvt (and other ttys) treat C-CR, S-CR, and CR as the same input
" imap <C-CR> <CR><ESC><ESC>Bkki
" Move between splits
nnoremap <C-j> <C-W><C-j>
nnoremap <C-k> <C-W><C-k>
nnoremap <C-h> <C-W><C-h>
nnoremap <C-l> <C-W><C-l>
" Make split
nnoremap <leader>- :sp<space>
nnoremap <leader><bar> :vsp<space>
set splitbelow
set splitright
" Open vimrc
map <leader>v :e $MYVIMRC<CR>
" Set working directory to current file directory
map <leader>cd :cd %:p:h<CR>:pwd<CR>:NERDTreeCWD<CR>:NERDTreeToggle<CR>
" Update vimrc
map <leader>rr :call ReloadVimRC()<CR>;
" Open GVim menu
" map <leader>m :emenu <C-Z><C-Z>
" Low-light mode
map <leader><leader>ll :set background=dark<CR>
" Daylight mode
map <leader><leader>LL :set background=light<CR>
" Open ranger
nmap <leader>ra :call Ranger()<CR>
" Delete whitespace
nmap <leader>rw :call DeleteTrailingWS()<CR>
" Fix syntax highlighting
noremap <F12> <Esc>:syntax sync fromstart<CR>
function! MapPlugins()
" - Plugins -
if exists('g:loaded_startify')
" Open Startify
map <leader><leader>o :Startify<CR>
endif
if exists('g:loaded_nerd_tree')
" Toggle directory view
map <leader>t :NERDTreeToggle<CR>
endif
" Generative AI
if exists('g:loaded_tagbar')
" Toggle tagbar
map <leader>ct :TagbarToggle<CR>
endif
if exists('g:loaded_vimwiki')
" TODO This shouldn't be necessary!
" NOTE: Changes must be mirrored to config/bindings-vimwiki.vimrc!
endif
if exists('g:loaded_table_mode')
endif
if exists('g:loaded_zettel')
let g:zettel_default_mappings = 0
endif
" Update ctags
" nnoremap <leader>ct :! ctags -R<CR><CR>
if exists('g:loaded_ctrlp')
" Jump to a tag
nnoremap <leader>. :CtrlPTag<CR>
" Jump to a buffer
nnoremap <leader>; :CtrlPBuffer<CR>
endif
" Make a config for YCM from Makefile
map <leader>ygc :YcmGenerateConfig<CR>
" Ag
map <leader>ag :Ag<space>
" Easymotion
if exists('g:EasyMotion_loaded')
" let g:EasyMotion_do_mapping = 0
let g:EasyMotion_smartcase = 1
map <leader>/ <Plug>(easymotion-sn)
omap <leader>/ <Plug>(easymotion-tn)
" map <leader>n <Plug>(easymotion-next)
" map <leader>N <Plug>(easymotion-prev)
" Bidirectional single-letter search
map <leader>s <Plug>(easymotion-s)
" Bidirectional 2-letter search
map <leader><leader>s <Plug>(easymotion-s2)
" Forward single letter search
map <leader>f <Plug>(easymotion-f)
" Directional jumps
map <leader>l <Plug>(easymotion-lineforward)
map <leader>j <Plug>(easymotion-j)
map <leader>k <Plug>(easymotion-k)
map <leader>h <Plug>(easymotion-linebackward)
endif
" Multiple Cursors
let g:multi_cursor_use_default_mapping=0
let g:multi_cursor_next_key='n'
let g:multi_cursor_prev_key='m'
let g:multi_cursor_skip_key='.'
let g:multi_cursor_quit_key=','
let g:multi_cursor_start_key='<C-n>'
if exists('g:tabular_loaded')
" Tabular
map <leader>a= :Tabularize /=<CR>
map <leader>a: :Tabularize /:\zs<CR>
endif
if exists('g:loaded_nerd_comments')
" NERD Commenter
" Enter the NERD Commenter menu which can be tabbed through
map <leader>nc :emenu Plugin.comment.<C-Z><C-Z>
endif
if exists('g:loaded_gundo')
" Gundo
nnoremap <leader>u :GundoToggle<CR>
endif
" --- Language Specific ---
" - General
" COC
" Use K to show documentation in preview window
nnoremap K :call ShowDocumentation()
nmap ff <plug>(coc-format-selected)
nmap rn <plug>(coc-rename)
nmap gd <plug>(coc-definition)
nmap gy <plug>(coc-type-definition)
nmap gi <plug>(coc-implementation)
nmap gr <plug>(coc-references)
" - Rust -
map <localleader>rr :RustRun<CR>
" - Markdown -
map <localleader>v :LivedownToggle<CR>
endfunction
augroup Plugin_Mappings
autocmd!
autocmd VimEnter * :call MapPlugins()
augroup END
]])
+3 -12
View File
@@ -82,6 +82,8 @@ function! MapPlugins()
map <leader>t :NERDTreeToggle<CR>
endif
" Generative AI
if exists('g:loaded_tagbar')
" Toggle tagbar
map <leader>ct :TagbarToggle<CR>
@@ -161,16 +163,6 @@ function! MapPlugins()
nnoremap <leader>u :GundoToggle<CR>
endif
if exists('g:loaded_claudecode')
" Claude Code
nnoremap <leader>ac :ClaudeCode<CR>
nnoremap <leader>af :ClaudeCodeFocus<CR>
nnoremap <leader>ab :ClaudeCodeAdd %<CR>
vnoremap <leader>as :ClaudeCodeSend<CR>
nnoremap <leader>aa :ClaudeCodeDiffAccept<CR>
nnoremap <leader>ad :ClaudeCodeDiffDeny<CR>
endif
" --- Language Specific ---
" - General
" COC
@@ -188,8 +180,7 @@ function! MapPlugins()
map <localleader>rr :RustRun<CR>
" - Markdown -
" map <localleader>v :LivedownToggle<CR>
nmap <localleader>v <Plug>MarkdownPreviewToggle
map <localleader>v :LivedownToggle<CR>
endfunction
augroup Plugin_Mappings
+62
View File
@@ -0,0 +1,62 @@
vim.cmd([[
"------ Functions ------
if executable('ranger')
" Open ranger from within vim
function! Ranger()
" Get a temp file name without creating it
let tmpfile = substitute(system('mktemp -u'), '\n', '', '')
" Launch ranger, passing it the temp file name
silent exec '!RANGER_RETURN_FILE='.tmpfile.' ranger'
" If the temp file has been written by ranger
if filereadable(tmpfile)
" Get the selected file name from the temp file
let filetoedit = system('cat '.tmpfile)
exec 'edit '.filetoedit
call delete(tmpfile)
endif
redraw!
endfunction
endif
" set viminfo=%M
" Delete trailing white space on save, useful for Python and CoffeeScript ;)
func! DeleteTrailingWS()
silent! normal mz
silent! %s/\s\+$//ge
silent! normal `z
endfunc
" autocmd BufWrite *.py :call DeleteTrailingWS()
" autocmd BufWrite *.coffee :call DeleteTrailingWS()
" Only define if not defined, otherwise it'll be redefined in the middle of
" execution
if !exists("*ReloadVimRC")
func! ReloadVimRC()
" IMPORTANT: After way too much messing with this trying to make everything work properly
" after a vimrc reload, my advice is DON'T RELOAD THE VIMRC WHILE EDITING A FILE!
" Side effects will include: Some syntax highlighting lost until next save, indentline
" plugin will break, newly opened buffers will use buffer reloaded on's syntax file for
" comment plugins (and possibly other things)
" Ommision of 'syntax on' here will prevent anything from breaking inside the current buffer,
" however new buffers will be opened without any syntax highlighting
source $MYVIMRC
" Need to toggle this twice, once toggles every other time, none turns
" it off on reload. I have no idea why this is.
RainbowToggle
RainbowToggle
syntax on
" Workaround for indentline turning off on syntax enable
IndentLinesReset
endfunc
endif
" COC Show Documentation under cursor
function! ShowDocumentation()
if (index(['vim','help'], &filetype) >= 0)
execute 'h '.expand('')
else
call CocAction('doHover')
endif
endfunction
]])
+164
View File
@@ -0,0 +1,164 @@
vim.cmd([[
" Needed for vimwiki
set nocompatible
filetype plugin on
syntax on
" Control-X Control-O to open autocomplete box
set omnifunc=syntaxcomplete#Complete
" Leave hidden buffers open
set hidden
set autoread " Reload files changed outside vim
" Turn off .swp files
set noswapfile
set nobackup
set t_Co=256 " Run in 256-color mode
set number " Show line numbers
" Set the clipbaord
" set clipboard+=unnamedplus
" let g:clipboard = {
" \ 'name': 'myClipboard',
" \ 'copy': {
" \ '+': 'tmux load-buffer -',
" \ '*': 'tmux load-buffer -',
" \ },
" \ 'paste': {
" \ '+': 'tmux save-buffer -',
" \ '*': 'tmux save-buffer -',
" \ },
" \ 'cache_enabled': 1,
" \ }
" let g:loaded_clipboard_provider=1
" Matchit plugin enhances %
runtime macros/matchit.vim
" Set unix line endings
" set fileformat=unix
" Set encoding for vim-devicons
set encoding=UTF-8
" Don't redraw screen when not needed
set lazyredraw
" Allow backspacing over everything
set backspace=indent,eol,start
" Keep longer history
set history=700
" Incremental search
set incsearch
" Smart case insensitive search
set ignorecase
set smartcase
" Maintain context around cursor
set scrolloff=15
" Wildmode configuration
set wildmode=longest,list,full
set wildmenu
" Make hardcoded tab work
:set wildcharm=<C-Z>
" Mouse support
set mouse=a
" Configure tab behavior
set tabstop=4
set softtabstop=4
set expandtab
set shiftwidth=4
" set autoindent
set cindent
set cinkeys-=0#
set indentkeys-=0#
" set smartindent
" Linewrap
set wrap linebreak nolist
" Show current position
set ruler
" Show matching brackets
set showmatch
" Make conceals less of an issue to edit by expanding on cursor
let g:indentLine_concealcursor=""
let g:indentLine_conceallevel=2
" Code folding
set foldmethod=syntax
set foldcolumn=5
set foldlevel=2
set foldlevelstart=2
let g:vimwiki_folding='list'
let g:markdown_folding=1
" Make views automatic (they store folds)
set viewoptions-=options
" TODO: These caused all of my buffers to cycle on buffer operations and
" slowed things down
" autocmd BufWinLeave * if expand("%") != "" | silent! mkview! | endif
" autocmd BufWinEnter * if expand("%") != "" | silent! loadview | endif
" Don't open empty buffer when starting existing session
set shortmess+=I
" Load default menus
source $VIMRUNTIME/menu.vim
" Always display airline statusline
set laststatus=2
" Hide default statusline text
set noshowmode "
" Theme
" colorscheme zenburn
" colorscheme solarized
" colorscheme base16-default
" colorscheme hybrid_reverse
" colorscheme hybrid_material
try
silent! colorscheme gruvbox
catch /^Vim\%((\a\++)\)\=:E185/
colorscheme desert
endtry
set termguicolors
let g:enable_bold_font = 1
" Have vrm extend background color to full terminal screen
set t_ut=
" --- GUI settings ---
" set lines=50
" set columns=150
" --- Language specific settings ---
" autocmd! BufNewFile,BufReadPre,FileReadPre,BufEnter * set expandtab softtabstop=4 shiftwidth=4
augroup Enter_Buffer
autocmd!
autocmd BufNewFile,BufReadPre,FileReadPre,BufEnter *.pde setlocal softtabstop=2 shiftwidth=2
autocmd BufNewFile,BufReadPre,FileReadPre,BufEnter Makefile setlocal noexpandtab softtabstop=0 shiftwidth=8
autocmd BufNewFile,BufReadPre,FileReadPre,BufEnter *.asm,*.S setlocal softtabstop=8 shiftwidth=8
augroup END
au BufNewFile,BufRead *.py
\ setlocal tabstop=4 |
\ setlocal softtabstop=4 |
\ setlocal shiftwidth=4 |
\ setlocal textwidth=79 |
\ setlocal expandtab |
\ setlocal autoindent |
\ setlocal fileformat=unix |
]])
+187
View File
@@ -0,0 +1,187 @@
vim.cmd([[
let g:vim_plug_install_dir = "~/.local/share/nvim/site/autoload"
let g:plugin_install_dir = "~/.local/share/nvim/plugged"
" Install vim-plug if not present
if empty(glob(g:vim_plug_install_dir . "/plug.vim"))
execute "!curl -fLo " . g:vim_plug_install_dir . "/plug.vim "
\ "--create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim"
endif
call plug#begin('~/.vim/plugged')
" --- General ---
" - Visual -
" Rainbow parentheses
Plug 'luochen1990/rainbow'
" Gruvbox
Plug 'morhetz/gruvbox'
" Airline
" TODO This now depends on lua
Plug 'bling/vim-airline'
" - Views -
" Start screen
Plug 'mhinz/vim-startify'
" Directory view
Plug 'scrooloose/nerdtree'
" Visualize undo tree
Plug 'sjl/gundo.vim'
" Much nicer buffer management
Plug 'jlanzarotta/bufexplorer'
" Shows indentation level
Plug 'yggdroot/indentLine'
" Show marks
" TODO Make this not conflict with ,mm for vimwiki, probably just an issue
" with not having vimwiki installed yet
" Plug 'kshenoy/vim-signature'
"
" - Sessions -
" Better sessions
Plug 'tpope/vim-obsession'
" - Generative AI -
Plug 'David-Kunz/gen.nvim'
" - Ctags -
" Ctags view
Plug 'majutsushi/tagbar'
" Automatically keep ctags up to date
" Plug 'xolox/vim-easytags'
" TODO Fix compatibility with vim-zettel
" Plug 'c0r73x/neotags.nvim'
" - Syntax and completion -
" Asynchronous linting
Plug 'neomake/neomake'
" Completion
Plug 'Shougo/deoplete.nvim', { 'do': ':UpdateRemotePlugins' }
" Awesome autocompletion for almost everything
" Plug 'Valloric/YouCompleteMe', {
" \ 'build' : {
" \ 'mac' : './install.sh --clang-completer --system-libclang --omnisharp-completer --racer-completer',
" \ 'unix' : './install.sh --clang-completer --system-libclang --omnisharp-completer --racer-completer',
" \ 'windows' : './install.sh --clang-completer --system-libclang --omnisharp-completer --racer-completer',
" \ 'cygwin' : './install.sh --clang-completer --system-libclang --omnisharp-completer --racer-completer'
" \ }
" \ }
" - Commenting -
" Comment toggling with lots of options
Plug 'scrooloose/nerdcommenter'
" Simple, quick commenting
Plug 'tpope/vim-commentary'
" - Git -
" Shows git changes in the side gutter
" Plug 'airblade/vim-gitgutter'
Plug 'mhinz/vim-signify'
" Git wrapper inside vim
Plug 'tpope/vim-fugitive'
" - Automation -
" Surround things and change surroundings
Plug 'tpope/vim-surround'
" Parentheses and bracket auto-close
Plug 'Raimondi/delimitMate'
" Much nicer navigation
" TODO Move this to space key
Plug 'easymotion/vim-easymotion'
" Multiple cursors
Plug 'terryma/vim-multiple-cursors'
" Automatic alignment
Plug 'godlygeek/tabular'
" Tables
Plug 'dhruvasagar/vim-table-mode'
" - Search -
" Kickass fuzzy finder for whole filesystem
Plug 'ctrlpvim/ctrlp.vim'
" Can use Ag from vim
" Plug 'rking/ag.vim'
" - Utility and dependencies
" Library required for some plugins
" Plug 'L9'
" Close buffers
Plug 'rbgrouleff/bclose.vim'
" Seamless navigation between tmux panes and vim splits
Plug 'christoomey/vim-tmux-navigator'
" Required for easytags
Plug 'xolox/vim-misc'
" Make . work for plugins
Plug 'tpope/vim-repeat'
" Auto reload changed config
Plug 'xolox/vim-reload'
" Make configuration files for YCM
" Plug 'rdnetto/YCM-Generator'
" - Note Taking -
" Note taking and wiki managment
Plug 'vimwiki/vimwiki'
" TODO Why can't these go in Search?
Plug 'junegunn/fzf'
Plug 'junegunn/fzf.vim'
Plug 'michal-h21/vim-zettel'
" TODO Make this pull on buffer open / load
" Plug 'michal-h21/vimwiki-sync'
" --- Language specific ---
" - All -
Plug 'sheerun/vim-polyglot'
" Integration with language servers but manual configuration
Plug 'neoclide/coc.nvim', {'branch': 'release'}
" Allows vim to configure filetype by file section (for svelte)
Plug 'Shougo/context_filetype.vim'
" Prettier
Plug 'prettier/vim-prettier', {
\ 'do': 'npm install',
\ 'for': ['javascript', 'typescript', 'css', 'less', 'scss', 'json', 'graphql', 'markdown', 'vue', 'svelte', 'yaml', 'html'] }
" - Web -
Plug 'mattn/emmet-vim'
Plug 'mattn/webapi-vim'
Plug 'cakebaker/scss-syntax.vim'
" - Python -
" Plug 'klen/python-mode'
" Plug 'davidhalter/jedi-vim'
" Completion
" Plug 'zchee/deoplete-jedi'
" - C# -
" Omnicompletion and syntax
" Plug 'OmniSharp/omnisharp-vim'
" - Rust -
" Syntax and autocompletion
" Plug 'rust-lang/rust.vim'
" Plug 'phildawes/racer'
" - Javascript -
Plug 'pangloss/vim-javascript'
" - Typescript -
Plug 'HerringtonDarkholme/yats.vim'
" TODO: Needs to be installed with `:CocInstall coc-tsserver`, figure out how
" to config this
" Plug 'coc-extensions/coc-tsserver'
" - Svelte
" Plug 'evanleck/vim-svelte', {'branch': 'main'}
Plug 'leafOfTree/vim-svelte-plugin'
Plug 'coc-extensions/coc-svelte'
" - Processing -
Plug 'sophacles/vim-processing'
" - Markdown -
" Plug 'shime/vim-livedown'
" --- Icons ---
" For some reason this needs to be loaded last
Plug 'ryanoasis/vim-devicons'
call plug#end()
]])
+5 -10
View File
@@ -1,5 +1,5 @@
let g:vim_plug_install_dir = "~/.local/share/nvim/site/autoload"
let g:plugin_install_dir = "~/.vim/plugged"
let g:plugin_install_dir = "~/.local/share/nvim/plugged"
" Install vim-plug if not present
if empty(glob(g:vim_plug_install_dir . "/plug.vim"))
@@ -39,6 +39,9 @@ Plug 'yggdroot/indentLine'
" Better sessions
Plug 'tpope/vim-obsession'
" - Generative AI -
Plug 'David-Kunz/gen.nvim'
" - Ctags -
" Ctags view
Plug 'majutsushi/tagbar'
@@ -116,7 +119,7 @@ Plug 'xolox/vim-reload'
" Note taking and wiki managment
Plug 'vimwiki/vimwiki'
" TODO Why can't these go in Search?
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
Plug 'junegunn/fzf'
Plug 'junegunn/fzf.vim'
Plug 'michal-h21/vim-zettel'
" TODO Make this pull on buffer open / load
@@ -173,14 +176,6 @@ Plug 'sophacles/vim-processing'
" - Markdown -
" Plug 'shime/vim-livedown'
" Plug 'iamcco/markdown-preview.nvim', { 'do': 'cd app && npx --yes yarn install' }
Plug 'iamcco/markdown-preview.nvim', { 'do': { -> mkdp#util#install() }, 'for': ['markdown', 'vim-plug']}
" --- AI ---
" Floating/split terminal windows used by claudecode.nvim
Plug 'folke/snacks.nvim'
" Claude Code integration
Plug 'coder/claudecode.nvim'
" --- Icons ---
+333
View File
@@ -0,0 +1,333 @@
require('gen').setup({
model = "phi",
host = "10.0.2.11",
port = "11434"
})
vim.cmd([[
" Table of Contents
" --- General ---
" - Visual -
" - Views -
" -
" TODO
" --- General ---
" - Sessions -
" Create new session optionally with given name. These show up in Startify
command -nargs=? SessionNew :Obsess ~/.local/share/nvim/session/<args>
command -nargs=0 SessionEnd :Obsess!
command -nargs=1 SessionLoad :source ~/.local/share/nvim/session/<args>
" TODO Could use g:startify_session_persistence and just rely on that
" - Visual -
set background=dark
let base16colorspace=256
" let g:solarized_termtrans = 3
" Airline
let g:airline_powerline_fonts=1
" PERF: Could cause bad performance
" let g:airline_skip_empty_sections=1
let g:airline#extensions#tabline#enabled=1
" Modify the statusbar if a recording is in session
" TODO: This prevents some extensions (tagbar, virtualenv) from using the
" section
" TODO: Only if Obsession installed!
let g:airline_section_x='%{ObsessionStatus("Session Recording |", "")} %{&filetype}'
" Gruvbox
let g:gruvbox_italic=1
let g:gruvbox_contrast_dark="medium"
let g:gruvbox_contrast_light="soft"
let g:gruvbox_invert_selection=1
let g:gruvbox_invert_signs=0
let g:gruvbox_improved_strings=0
let g:gruvbox_improved_warnings=1
let g:gruvbox_italicize_strings=1
" Rainbow parentheses
" if exists('g:rainbow_conf')
" augroup Rainbow_Parentheses
" autocmd!
" autocmd VimEnter * if exists('g:rainbow_conf') |
" \ RainbowToggle |
" \ endif
" augroup END
" endif
" - Views -
" Startify
let g:startify_bookmarks = [
\"~/dotfiles/config/nvim/config/init.vimrc",
\"~/dotfiles/config/nvim/config/general.vimrc",
\"~/dotfiles/config/nvim/config/plugin.vimrc",
\"~/dotfiles/config/nvim/config/bindings.vimrc",
\"~/dotfiles/config/nvim/config/functions.vimrc",
\"~/dotfiles/zshrc",
\"~/dotfiles/config/i3/config"
\]
let g:startify_custom_indices = ['f', 'd', 's', 'a', 'g']
let g:startify_custom_header = map(split(system('fortune -a -s | fmt -80 -s | cowthink -$(shuf -n 1 -e b d g p s t w y) -f $(shuf -n 1 -e $(cowsay -l | tail -n +2)) -n'), '\n'), '" ". v:val') + [","]
" let g:startify_lists = [
" \ { 'type': 'files', 'header': [' MRU'] },
" \ { 'type': 'dir', 'header': [' MRU '. getcwd()] },
" \ { 'type': 'sessions', 'header': [' Sessions'] },
" \ { 'type': 'bookmarks', 'header': [' Bookmarks'] },
" \ { 'type': 'commands', 'header': [' Commands'] },
" \ ]
" Automatically open tagbar when entering supported buffer
" autocmd BufEnter * nested :call tagbar#autoopen(0)
" - Generative AI -
" - Utility -
" DelimitMate sane bracing
let delimitMate_expand_cr = 1
" Indent Guide configuration
" set ts=2 sw=2 et
let g:indent_guides_enable_on_vim_startup = 0
let g:indent_guides_start_level = 2
let g:indent_guides_auto_colors = 0
augroup Indent_Guides
autocmd!
autocmd VimEnter,Colorscheme * if exists('g:indentLine_loaded') |
\ :hi IndentGuidesOdd ctermbg=darkgreen |
\ endif
autocmd VimEnter,Colorscheme * if exists('g:indentLine_loaded') |
\ :hi IndentGuidesEven ctermbg=cyan |
\ endif
augroup END
" Indent Line configuration
let g:indentLine_faster = 1
" let g:indentLine_leadingSpaceEnabled = 1
" let g:indentLine_leadingSpaceChar = '.'
" GitGutter
let g:gitgutter_map_keys = 0
" NERD Commenter
" let g:NERDMenuMode = 1
" NERD Tree
" This causes an extra buffer to open when nvim starts
" augroup DIRCHAANGE
" au!
" autocmd DirChanged global :NERDTreeCWD
" augroup END
" Sort by modification date
let NERDTreeSortOrder = ['[ [-timestamp] ]']
" Ctrl-P configuration
" Use nearest version control directory as cwd
let g:ctrlp_working_path_mode = 'r'
" Syntastic configuration
let g:syntastic_aggregate_errors = 1
let g:syntastic_always_populate_loc_list = 1
let g:syntastic_auto_loc_list = 1
let g:syntastic_check_on_open = 1
let g:syntastic_check_on_wq = 0
let g:syntastic_mode_map = {
\ "mode": "active",
\ "passive_filetypes": ['asm', 'scss'] }
" YCM configuration
let g:ycm_global_ycm_extra_conf = '~/dotfiles/.ycm_extra_conf.py'
let g:ycm_extra_conf_globlist = ['~/dotfiles/.ycm_extra_conf.py']
let g:ycm_server_keep_logfiles = 1
let g:ycm_server_log_level = 'debug'
" let g:loaded_youcompleteme = 1
" let g:ycm_min_num_of_chars_for_completion = 100
let g:neobundle#install_process_timeout = 1500
" nnoremap <leader><leader>zy :let g:ycm_auto_trigger=0<CR>
" nnoremap <leader><leader>zY :let g:ycm_auto_trigger=1<CR>
" Powerline configuration
let g:powerline_pycmd = "py"
" let g:powerline_pyeval = "pyeval"
" Neotags configuration
" let g:neotags_enabled = 1
" Vimwiki configuration
let g:vimwiki_list = [
\{'path': '~/synced-notebooks/bradwiki/', 'syntax': 'markdown', 'ext': '.md', 'diary_rel_path': 'daily_notes/', 'diary_index': 'index', 'diary_header': 'Daily Notes', 'auto_tags': 1, 'auto_diary_index': 1, 'auto_generate_links': 1, 'auto_generate_tags': 1},
\{'path': '~/synced-notebooks/work_notebook/', 'syntax': 'markdown', 'ext': '.md', 'diary_rel_path': 'daily_notes/', 'diary_index': 'index', 'diary_header': 'Daily Notes', 'auto_tags': 1, 'auto_diary_index': 1, 'auto_generate_links': 1, 'auto_generate_tags': 1},
\{'path': '~/synced-notebooks/privatewiki/', 'syntax': 'markdown', 'ext': '.md'},
\{'path': '~/synced-notebooks/sharedwiki/', 'syntax': 'markdown', 'ext': '.md'}]
let g:vimwiki_map_prefix = '<Leader>m'
augroup filetype_vimwiki
autocmd!
" Wrap lines nicely
autocmd Filetype vimwiki set breakindent
autocmd Filetype vimwiki set breakindentopt=shift:2
augroup END
" Vim-Zettel configuration
let g:zettel_options = [{}, {"front_matter": [["tags", ""], ["type", "note"] ], "template": "~/work_notebook/templates/daily_note.tpl"}]
" Table Mode configuration
let g:table_mode_map_prefix = '<Leader><Leader>t'
" --- Language Specific ---
" - All -
" let g:polyglot_disabled = ['python']
" COC
" let g:coc_node_path = '$HOME/.nvm/versions/node/v12.16.3/bin/node'
set updatetime=300
set shortmess+=c " don't give |ins-completion-menu| messages.
" - Svelte -
let g:svelte_preprocessor_tags = [
\ { 'name': "ts", 'tag': "script", 'as': "typescript" }
\ ]
let g:svelte_preprocessors = ["scss", "sass", "typescript", "ts"]
let g:vim_svelte_plugin_load_full_syntax = 1
let g:vim_svelte_plugin_use_sass = 1
let g:vim_svelte_plugin_use_typescript = 1
let g:vim_svelte_plugin_use_foldexpr = 0
if !exists('g:context_filetype#same_filetypes')
let g:context_filetype#filetypes = {}
endif
let g:context_filetype#filetypes.svelte =
\ [
\ {'filetype' : 'typescript', 'start': '<script\%( [^>]*\)\? \%(ts\|lang="\%(ts\|typescript\)"\)\%( [^>]*\)\?>', 'end': '</script>'},
\ {'filetype' : 'javascript', 'start' : '<script \?.*>', 'end' : '</script>'},
\ {'filetype' : 'scss', 'start' : '<style \?.*lang=''scss''>', 'end' : '</style>'},
\ {'filetype' : 'sass', 'start' : '<style \?.*lang="sass">', 'end' : '</style>'},
\ {'filetype' : 'css', 'start' : '<style \?.*>', 'end' : '</style>'},
\ ]
let g:ft = ''
" For NERDCommenter
let g:NERDSpaceDelims = 1
" let g:NERDCompactSexyComs = 1
fu! NERDCommenter_before()
if (&ft == 'html') || (&ft == 'svelte')
let g:ft = &ft
let cfts = context_filetype#get_filetypes()
if len(cfts) > 0
if cfts[0] == 'svelte'
let cft = 'html'
else
let cft = cfts[0]
endif
echo "Using filetype=".cft." for comment"
exe 'setf '.cft
if exists('b:NERDCommenterDelims')
unlet b:NERDCommenterDelims
endif
call nerdcommenter#SetUp()
endif
endif
endfu
fu! NERDCommenter_after()
if g:ft == 'svelte'
exec 'setf '.g:ft
if exists('b:NERDCommenterDelims')
unlet b:NERDCommenterDelims
endif
call nerdcommenter#SetUp()
let g:ft = ''
endif
endfu
" Prettier
let g:prettier#quickfix_enabled = 0
let g:prettier#autoformat_require_pragma = 0
au BufWritePre *.css,*.svelte,*.pcss,*.html,*.ts,*.js,*.json PrettierAsync
" - Web -
let g:user_emmet_install_global=0
augroup Emmet
autocmd!
autocmd Filetype html,css if exists('g:loaded_emmet_vim') |
\ EmmetInstall |
\ endif
augroup END
" let g:user_emmet_leader_key='<localleader>'
" - Javascript -
" Javascript configuration
let g:javascript_enable_dom_htmlcss = 1
" - Python -
" Run in python using F9
augroup Python
autocmd!
autocmd FileType python nnoremap <buffer> <F9> :exec '!python' shellescape(@%, 1)<cr>
augroup END
" Documentation
let g:pymode_doc = 1
let g:pymode_doc_key = 'K'
" Linting
let g:pymode_lint = 1
let g:pymode_lint_checkers = ['pyflakes', 'pep8']
let g:pymode_lint_message = 1
" Auto check on save
let g:pymode_lint_write = 1
" Support virtualenv
let g:pymode_virtualenv = 1
" Enable breakpoints plugin
let g:pymode_breakpoint = 1
let g:pymode_breakpoint_bind = '<localleader>b'
" Syntax highlighting
let g:pymode_syntax = 1
let g:pymode_syntax_all = 1
let g:pymode_syntax_indent_errors = g:pymode_syntax_all
let g:pymode_syntax_space_errors = g:pymode_syntax_all
" Enable python folding
let g:pymode_folding = 1
" Enable pymode motion
let g:pymode_motion = 1
" Set up default python options
let g:pymode_options = 1
" Trim whitespace on save
let g:pymode_trim_whitespaces = 1
" PEP8-compatible python indent
" let g:pymode_indent = 1
" Rope support
let g:pymode_rope = 0
" [YELP] Ignore line too long
let g:pymode_lint_ignore = "W503,E128"
" - Rust -
" Rust racer
let g:racer_cmd = "~/dotfiles/racer/target/release/racer"
" Must install Rust source to this location first
let $RUST_SRC_PATH="/usr/local/src/rust/src/"
" Rust folding
let g:rust_fold = 2
" - Processing -
let g:processing_fold = 1
" - Markdown -
let g:livedown_open = 1
]])
+2 -19
View File
@@ -73,6 +73,8 @@ let g:startify_custom_header = map(split(system('fortune -a -s | fmt -80 -s | co
" Automatically open tagbar when entering supported buffer
" autocmd BufEnter * nested :call tagbar#autoopen(0)
" - Generative AI -
" - Utility -
" DelimitMate sane bracing
let delimitMate_expand_cr = 1
@@ -173,7 +175,6 @@ let g:table_mode_map_prefix = '<Leader><Leader>t'
" COC
" let g:coc_node_path = '$HOME/.nvm/versions/node/v12.16.3/bin/node'
let g:coc_disable_startup_warning = 1
set updatetime=300
set shortmess+=c " don't give |ins-completion-menu| messages.
@@ -322,22 +323,4 @@ let g:rust_fold = 2
let g:processing_fold = 1
" - Markdown -
" Livedown
let g:livedown_open = 1
" Markdown Preview
" --- AI ---
" Claude Code
" plugin/claudecode.lua auto-calls require('claudecode').setup(g:claudecode_auto_setup)
" once the plugin loads (during plug#end() below), so this must be a plain
" vimscript dict rather than a lua block -- see the guarded keymaps in
" bindings.vimrc for the commands this enables.
" Full option list: https://github.com/coder/claudecode.nvim#configuration-options
let g:claudecode_auto_setup = {
\ 'terminal': {
\ 'provider': 'snacks',
\ 'split_side': 'right',
\ 'split_width_percentage': 0.30,
\ },
\ }
-4
View File
@@ -1,4 +0,0 @@
vim.bo.textwidth = 79
if vim.bo.modifiable then
vim.bo.fileformat = "unix"
end
-36
View File
@@ -1,36 +0,0 @@
local map = function(mode, lhs, rhs)
vim.keymap.set(mode, lhs, rhs, { buffer = true })
end
map("n", "<leader>mm", "<Plug>VimwikiIndex")
map("n", "<leader>mw", "<Plug>VimwikiUISelect")
map("n", "<leader>mhelp", ":verbose map ,m<CR>")
map("n", "<leader>mg", "<Plug>VimwikiGoto")
map("n", "<leader>mf", "<Plug>VimwikiUISelect1<CR>")
map("n", "<leader>m<Space>", "<Plug>VimwikiUISelect2<CR>")
map("n", "<leader>mp", "<Plug>VimwikiUISelect3<CR>")
map("n", "<leader>ms", "<Plug>VimwikiUISelect4<CR>")
map("n", "<leader>m-", "<Plug>VimwikiSplitLink")
map("n", "<leader>m<Bar>", "<Plug>VimwikiVSplitLink")
map("n", "<leader>mnt", "<Plug>VimwikiNextTask")
map("n", "<leader>mnl", "<Plug>VimwikiNextLink")
map("n", "<leader>mNl", "<Plug>VimwikiPrevLink")
map({ "n", "i", "v" }, "<Tab>", "<Plug>VimwikiIncreaseLvlSingleItem")
map({ "n", "i", "v" }, "<S-Tab>", "<Plug>VimwikiDecreaseLvlSingleItem")
map("i", "<C-Space>", "<Esc><Plug>VimwikiToggleListItemA")
map("n", "<C-d><C-Space>", "<Plug>VimwikiRemoveSingleCB")
map("i", "<C-d><C-Space>", "<Esc><Plug>VimwikiRemoveSingleCBa")
map("n", "<leader>mb", "<Plug>VimwikiBackLinks")
map("n", "<leader>mqtoc", "<Plug>VimwikiTOC")
map("n", "<leader>mqgtl", "<Plug>VimwikiRebuildTags<CR><Plug>VimwikiGenerateTagLinks<CR>")
map("v", "<leader>mk", 'c[]<Esc>Pea()<Esc>"+P')
map("i", "<leader>mk", '<Esc>viW<C-k>ea')
map("n", "<leader>mk", 'ciw[]<Esc>Pea()<Esc>"+P')
map("n", "<leader>mag", ":ZettelOpen<CR>")
map("n", "<leader>mat", ":ZettelOpen<CR>title:")
+17 -89
View File
@@ -1,91 +1,19 @@
-- Version guard: native LSP config (vim.lsp.config/vim.lsp.enable, added in
-- 0.11) and the pinned nvim-treesitter (hard-requires 0.12 in its own health
-- check) both need a current Neovim. Older versions -- e.g. Debian's apt
-- package, which lags upstream by a long way even fully updated -- don't
-- fail here; they fail later with a confusing cascade of lspconfig and
-- mason-lspconfig errors. Check up front instead, before anything else in
-- this file runs (some of it, like vim.uv below, doesn't exist pre-0.10).
if vim.fn.has("nvim-0.12") == 0 then
local ok, v = pcall(vim.version)
local current = ok and string.format("%d.%d.%d", v.major, v.minor, v.patch) or "an older version"
vim.cmd([[
" -- Source config files
source $HOME/.config/nvim/config/bindings-vimwiki.vimrc
source $HOME/.config/nvim/config/init.vimrc
source $HOME/.config/nvim/config/plugin.vimrc
source $HOME/.config/nvim/config/general.vimrc
source $HOME/.config/nvim/config/bindings.vimrc
source $HOME/.config/nvim/config/functions.vimrc
-- vim.loop (not vim.uv -- that alias doesn't exist pre-0.10) to detect the
-- machine's arch, since Neovim ships separate release assets per arch and
-- the install command below needs to name the right one.
local machine = (vim.loop.os_uname() or {}).machine or ""
local asset_arch = ({ x86_64 = "x86_64", amd64 = "x86_64", aarch64 = "arm64", arm64 = "arm64" })[machine]
" -- Python dependencies
let g:python_host_prog = expand("~/.pyenv/versions/neovim2/bin/python")
let g:python3_host_prog = expand("~/.pyenv/versions/neovim3/bin/python")
if not asset_arch then
vim.api.nvim_echo({
{
"This config requires Neovim 0.12.0 or newer (found "
.. current
.. "). No prebuilt Neovim release is published for this machine's architecture ("
.. (machine ~= "" and machine or "unknown")
.. "); check https://github.com/neovim/neovim/releases/latest for a build, or install from source.\n",
"ErrorMsg",
},
}, true, {})
vim.fn.getchar()
os.exit(1)
end
local sysname = ((vim.loop.os_uname() or {}).sysname or ""):lower()
local is_mac = sysname == "darwin"
local asset = (is_mac and "nvim-macos-" or "nvim-linux-") .. asset_arch
local lines = {
{ "This config requires Neovim 0.12.0 or newer (found " .. current .. ", " .. machine .. ").\n\n", "ErrorMsg" },
{ "Install a current build into ~/.local (no root needed):\n", "None" },
{ " curl -LO https://github.com/neovim/neovim/releases/latest/download/" .. asset .. ".tar.gz\n", "None" },
{ " mkdir -p ~/.local && tar xzf " .. asset .. ".tar.gz --strip-components=1 -C ~/.local\n", "None" },
{ " rm " .. asset .. ".tar.gz\n", "None" },
{ " # then make sure ~/.local/bin is on your PATH\n\n", "Comment" },
}
if not is_mac then
vim.list_extend(lines, {
{ "Or system-wide (needs root):\n", "None" },
{ " curl -LO https://github.com/neovim/neovim/releases/latest/download/" .. asset .. ".tar.gz\n", "None" },
{ " sudo mkdir -p /opt/nvim && sudo tar xzf " .. asset .. ".tar.gz --strip-components=1 -C /opt/nvim\n", "None" },
{ " sudo ln -sf /opt/nvim/bin/nvim /usr/local/bin/nvim\n", "None" },
{ " rm " .. asset .. ".tar.gz\n", "None" },
})
end
vim.api.nvim_echo(lines, true, {})
vim.fn.getchar()
os.exit(1)
end
-- Leader keys MUST be set before lazy.setup() so lazy-loaded `keys = {...}`
-- specs resolve <leader> correctly when they register their keymaps.
vim.g.mapleader = ","
vim.g.maplocalleader = "\\"
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.uv.fs_stat(lazypath) then
local out = vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable",
lazypath,
})
if vim.v.shell_error ~= 0 then
vim.api.nvim_echo({
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
{ out, "WarningMsg" },
}, true, {})
vim.fn.getchar()
os.exit(1)
end
end
vim.opt.rtp:prepend(lazypath)
require("config.options")
require("config.keymaps")
require("config.autocmds")
require("lazy").setup("plugins", {
change_detection = { notify = false },
})
" -- Local overrides
let $LOCALFILE=expand("$HOME/.nvim_local.vimrc")
if filereadable($LOCALFILE)
source $LOCALFILE
endif
]])
-47
View File
@@ -1,47 +0,0 @@
{
"aerial.nvim": { "branch": "master", "commit": "28fe6e822ae344544c379d60fcb13c9519a1f08a" },
"alpha-nvim": { "branch": "main", "commit": "6c6a89d5b068b5251c8bdf0dd57bb921bcfeeb09" },
"blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" },
"blink.lib": { "branch": "main", "commit": "5876dd95deeb70aadbe9f1c0b7117a135061cdac" },
"bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" },
"claudecode.nvim": { "branch": "main", "commit": "2390c6e45c4789072c293ac69de051d169668b29" },
"conform.nvim": { "branch": "master", "commit": "619363c30309d29ffa631e67c8183f2a72caa373" },
"emmet-vim": { "branch": "master", "commit": "92ef2f74f4093edc99db5e9e4cf7e40116a85bd6" },
"flash.nvim": { "branch": "main", "commit": "fcea7ff883235d9024dc41e638f164a450c14ca2" },
"gitsigns.nvim": { "branch": "main", "commit": "eb60cc7b94c46005237fd34170d76f3a089a90aa" },
"gruvbox.nvim": { "branch": "main", "commit": "154eb5ff5b96d0641307113fa385eaf0d36d9796" },
"indent-blankline.nvim": { "branch": "master", "commit": "d28a3f70721c79e3c5f6693057ae929f3d9c0a03" },
"lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" },
"lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" },
"markdown-preview.nvim": { "branch": "master", "commit": "a923f5fc5ba36a3b17e289dc35dc17f66d0548ee" },
"mason-lspconfig.nvim": { "branch": "main", "commit": "47059d71b42d74b0a1e9f61c1d99d301039c3b5b" },
"mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" },
"mini.align": { "branch": "main", "commit": "8cebe89d82f20e6d21b27346bcde4b4b6244a9f1" },
"mini.bufremove": { "branch": "main", "commit": "4cfbb201b2760da79ca6e1a00e9f67cbc99cbb8f" },
"neo-tree.nvim": { "branch": "v3.x", "commit": "ebd66767191714e008ce73b769518a763ff31bdc" },
"nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" },
"nvim-autopairs": { "branch": "master", "commit": "7b9923abad60b903ece7c52940e1321d39eccc79" },
"nvim-dap": { "branch": "master", "commit": "9e848e09a697ee95302a3ef2dd43fd6eb709e570" },
"nvim-dap-python": { "branch": "master", "commit": "1808458eba2b18f178f990e01376941a42c7f93b" },
"nvim-lint": { "branch": "master", "commit": "a219b2c9e5b4765e5c845aba119dad55806fcaf1" },
"nvim-lspconfig": { "branch": "master", "commit": "d224a1920728ba129880efc700d4a0180ac4ecbb" },
"nvim-surround": { "branch": "main", "commit": "8b47db616ef658b8fc27e61db2896aa2f40134de" },
"nvim-treesitter": { "branch": "main", "commit": "4916d6592ede8c07973490d9322f187e07dfefac" },
"nvim-treesitter-context": { "branch": "master", "commit": "b311b30818951d01f7b4bf650521b868b3fece16" },
"nvim-web-devicons": { "branch": "master", "commit": "dad71387de386a946b123079d0e53f23028f3abd" },
"persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" },
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
"rainbow-delimiters.nvim": { "branch": "master", "commit": "a798325b7f36acc62741d1029930a7b96d4dd4bf" },
"rustaceanvim": { "branch": "main", "commit": "88575b98bb9937fb9983ddec5e532b67e75ce677" },
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
"telescope-fzf-native.nvim": { "branch": "main", "commit": "b25b749b9db64d375d782094e2b9dce53ad53a40" },
"telescope.nvim": { "branch": "master", "commit": "427b576c16792edad01a92b89721d923c19ad60f" },
"undotree": { "branch": "main", "commit": "02b69aed427b848c4dca483fc5e9524b6019c296" },
"vim-fugitive": { "branch": "master", "commit": "3b753cf8c6a4dcde6edee8827d464ba9b8c4a6f0" },
"vim-table-mode": { "branch": "master", "commit": "bb025308a45c67c7c8f0763ba37bc2ee3f534df0" },
"vim-tmux-navigator": { "branch": "master", "commit": "e41c431a0c7b7388ae7ba341f01a0d217eb3a432" },
"vim-visual-multi": { "branch": "master", "commit": "a6975e7c1ee157615bbc80fc25e4392f71c344d4" },
"vim-zettel": { "branch": "master", "commit": "554d16bf65b04ae86b92db08a44b4fbcbc31023f" },
"vimwiki": { "branch": "dev", "commit": "a54a3002e229c4b43d69ced170ff77663a5b2c40" },
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }
}
-53
View File
@@ -1,53 +0,0 @@
-- autoread (options.lua) only reloads a buffer once Vim checks the file's
-- timestamp, which by default only happens on a handful of internal
-- triggers. Force that check on focus/buffer-switch/idle so external edits
-- (e.g. from claudecode.nvim) show up immediately instead of surfacing the
-- stale-buffer prompt later.
vim.api.nvim_create_autocmd({ "FocusGained", "BufEnter", "CursorHold", "CursorHoldI" }, {
group = vim.api.nvim_create_augroup("CheckTimeOnFocus", { clear = true }),
callback = function()
if vim.fn.getcmdwintype() == "" then
vim.cmd("checktime")
end
end,
})
local augroup = vim.api.nvim_create_augroup("EnterBuffer", { clear = true })
vim.api.nvim_create_autocmd({ "BufNewFile", "BufReadPre", "FileReadPre", "BufEnter" }, {
group = augroup,
pattern = "Makefile",
callback = function()
vim.bo.expandtab = false
vim.bo.softtabstop = 0
vim.bo.shiftwidth = 8
end,
})
vim.api.nvim_create_autocmd({ "BufNewFile", "BufReadPre", "FileReadPre", "BufEnter" }, {
group = augroup,
pattern = { "*.asm", "*.S" },
callback = function()
vim.bo.softtabstop = 8
vim.bo.shiftwidth = 8
end,
})
-- claude-sandbox (docker/claude-sandbox) sets CLAUDE_SANDBOX, never present
-- on a normal host session, so this only fires inside the sandbox: open
-- claudecode.nvim's terminal on startup rather than requiring <leader>ac,
-- since starting a Claude session is the entire point of being in there.
-- :ClaudeCode is defined by the plugin itself, so it doesn't exist until
-- claudecode.nvim has actually loaded - it's lazy-loaded on the <leader>ac
-- keymap only (no `cmd` in its lazy.nvim spec), so force that load first.
if vim.env.CLAUDE_SANDBOX then
vim.api.nvim_create_autocmd("VimEnter", {
once = true,
callback = function()
vim.schedule(function()
require("lazy").load({ plugins = { "claudecode.nvim" } })
vim.cmd("ClaudeCode")
end)
end,
})
end
-59
View File
@@ -1,59 +0,0 @@
local map = vim.keymap.set
-- Insert/command-mode escape via ,,
map("i", ",,", "<Esc>", { desc = "Escape insert mode" })
map("c", ",,", "<Esc><Esc>", { desc = "Escape command mode" })
-- Terminal mode is a separate mode from Insert, so the above doesn't cover
-- it (e.g. the Claude Code terminal). <C-\><C-n> is the real escape back to
-- Normal mode there.
map("t", ",,", "<C-\\><C-n>", { desc = "Escape terminal mode" })
-- Move by display line, not physical line
map("n", "j", "gj", { desc = "Move down by display line" })
map("n", "k", "gk", { desc = "Move up by display line" })
-- Clear search highlight
map("n", "<leader><leader><Esc>", ":nohl<CR>", { desc = "Clear search highlight" })
-- Hex mode
map("n", "<leader>\\he", ":%!xxd<CR>", { desc = "Hex dump buffer" })
map("n", "<leader>\\hd", ":%!xxd -r<CR>", { desc = "Undo hex dump" })
-- Toggle whitespace display
map("n", "<leader><leader>lw", ":set list!<CR>", { desc = "Toggle whitespace display" })
-- List registers / marks
map("n", "<leader><leader>lr", ":reg<CR>", { desc = "List registers" })
map("n", "<leader><leader>lm", ":marks<CR>", { desc = "List marks" })
-- Prev/next buffer
map("n", "<leader>q", ":bp<CR>", { desc = "Previous buffer" })
map("n", "<leader>w", ":bn<CR>", { desc = "Next buffer" })
-- System clipboard
map({ "n", "v" }, "<leader>y", '"+y', { desc = "Yank to system clipboard" })
map({ "n", "v" }, "<leader>Y", '"+Y', { desc = "Yank line to system clipboard" })
map({ "n", "v" }, "<leader>p", '"+p', { desc = "Paste from system clipboard" })
map({ "n", "v" }, "<leader>P", '"+P', { desc = "Paste before from system clipboard" })
map({ "n", "v" }, "<leader>x", '"+x', { desc = "Cut to system clipboard" })
map("n", "<leader>dd", '"+dd', { desc = "Delete line to system clipboard" })
-- Make split
map("n", "<leader>-", ":sp<Space>", { desc = "Horizontal split" })
map("n", "<leader><Bar>", ":vsp<Space>", { desc = "Vertical split" })
-- Open init.lua
map("n", "<leader>v", ":e $MYVIMRC<CR>", { desc = "Edit init.lua" })
-- Low-light / daylight mode
map("n", "<leader><leader>ll", ":set background=dark<CR>", { desc = "Dark background" })
map("n", "<leader><leader>LL", ":set background=light<CR>", { desc = "Light background" })
-- Ranger / trailing whitespace utilities
map("n", "<leader>ra", function() require("util").ranger() end, { desc = "Open ranger" })
map("n", "<leader>rw", function() require("util").delete_trailing_ws() end, { desc = "Delete trailing whitespace" })
-- Terminal-mode Ctrl-Z otherwise goes straight to the embedded process (e.g.
-- suspending the claude CLI inside claudecode.nvim's terminal with no way to
-- fg it back) instead of suspending Neovim itself, like Ctrl-Z does by
-- default in Normal mode.
map("t", "<C-z>", "<C-\\><C-n><C-z>", { desc = "Suspend Neovim" })
-87
View File
@@ -1,87 +0,0 @@
local opt = vim.opt
-- Completion / buffers
opt.omnifunc = "syntaxcomplete#Complete"
opt.hidden = true
opt.autoread = true
-- No swap/backup files
opt.swapfile = false
opt.backup = false
opt.number = true
-- matchit is built into Neovim's runtime already; no `runtime macros/matchit.vim` needed
opt.encoding = "utf-8"
opt.lazyredraw = true
opt.backspace = { "indent", "eol", "start" }
opt.history = 700
opt.incsearch = true
opt.ignorecase = true
opt.smartcase = true
opt.scrolloff = 15
opt.wildmode = { "longest", "list", "full" }
opt.wildmenu = true
opt.wildcharm = vim.api.nvim_replace_termcodes("<C-Z>", true, true, true):byte()
opt.mouse = "a"
-- Tabs
opt.tabstop = 4
opt.softtabstop = 4
opt.expandtab = true
opt.shiftwidth = 4
opt.wrap = true
opt.linebreak = true
opt.list = false
opt.ruler = true
opt.showmatch = true
opt.viewoptions:remove("options")
opt.shortmess:append("I")
opt.laststatus = 2
opt.showmode = false
opt.termguicolors = true
vim.g.enable_bold_font = 1
-- Have vrm extend background color to full terminal screen
vim.cmd("set t_ut=")
opt.splitbelow = true
opt.splitright = true
opt.updatetime = 300
opt.shortmess:append("c")
-- Clipboard: copy-only, via dotfiles/bin/clipboard-copy. This is a small
-- script (rather than nvim's built-in raw-OSC-52-only provider) because
-- reaching the real clipboard needs more than a raw OSC 52 write: it picks
-- pbcopy/xclip/wl-copy when nvim is running locally, and when none of those
-- are available (nvim on a remote host with no local display) sends a
-- single plain OSC 52 write. Any tmux hops between here and the real
-- terminal relay that onward themselves via the pane-set-clipboard hook in
-- tmux.conf, so this works whether nvim is local or several ssh/tmux hops
-- deep, on any machine sharing this dotfiles repo, without needing to know
-- how many hops there are. Paste is intentionally left as a no-op: OSC 52
-- read support is inconsistent across terminals and can hang waiting for a
-- reply, so use the terminal's own native paste (e.g. Ctrl-Shift-V)
-- instead.
local clipboard_copy = vim.fn.expand("~/dotfiles/bin/clipboard-copy")
vim.g.clipboard = {
name = "clipboard-copy (copy-only)",
copy = {
["+"] = { clipboard_copy },
["*"] = { clipboard_copy },
},
paste = {
["+"] = function() return {} end,
["*"] = function() return {} end,
},
}
-66
View File
@@ -1,66 +0,0 @@
return {
{ "folke/snacks.nvim", lazy = false },
{
"coder/claudecode.nvim",
dependencies = { "folke/snacks.nvim" },
opts = {
terminal = {
provider = "snacks",
split_side = "right",
split_width_percentage = 0.30,
},
diff_opts = {
keep_terminal_focus = true,
-- Unified (single-buffer, VS Code-style) diff instead of side-by-side
-- splits: it only ever opens one new window, so it can't add a second
-- split and squish the rest of the layout the way the side-by-side
-- diff sometimes did.
layout = "unified",
},
},
config = function(_, opts)
require("claudecode").setup(opts)
-- claudecode picks a window for the diff by scanning all windows for the
-- first non-sidebar/non-terminal one, not the one you're actually
-- focused on, so the diff could land away from your active pane. Prefer
-- the current window when it's a normal editing window.
local diff = require("claudecode.diff")
local default_find_main_editor_window = diff._find_main_editor_window
diff._find_main_editor_window = function()
local win = vim.api.nvim_get_current_win()
local buf = vim.api.nvim_win_get_buf(win)
local buftype = vim.api.nvim_buf_get_option(buf, "buftype")
local win_config = vim.api.nvim_win_get_config(win)
local is_floating = win_config.relative and win_config.relative ~= ""
local is_diff_win = vim.api.nvim_win_get_option(win, "diff")
if not is_floating and not is_diff_win and buftype ~= "terminal" and buftype ~= "prompt" then
return win
end
return default_find_main_editor_window()
end
-- claudecode's unified diff buffers pick up treesitter's foldexpr via
-- the normal FileType autocmd, so they otherwise inherit the global
-- foldlevelstart and open partially collapsed. Fully expand folds in
-- these specifically instead of changing the default for all buffers.
vim.api.nvim_create_autocmd("BufWinEnter", {
group = vim.api.nvim_create_augroup("ClaudeCodeDiffUnfold", { clear = true }),
callback = function(args)
if vim.b[args.buf].claudecode_inline_diff then
vim.wo.foldlevel = 99
end
end,
})
end,
keys = {
{ "<leader>ac", "<cmd>ClaudeCode<CR>", desc = "Toggle Claude Code" },
{ "<leader>af", "<cmd>ClaudeCodeFocus<CR>", desc = "Focus Claude Code" },
{ "<leader>ab", "<cmd>ClaudeCodeAdd %<CR>", desc = "Add buffer to Claude Code" },
{ "<leader>as", "<cmd>ClaudeCodeSend<CR>", mode = "v", desc = "Send selection to Claude Code" },
{ "<leader>aa", "<cmd>ClaudeCodeDiffAccept<CR>", desc = "Accept Claude Code diff" },
{ "<leader>ad", "<cmd>ClaudeCodeDiffDeny<CR>", desc = "Deny Claude Code diff" },
},
},
}
-10
View File
@@ -1,10 +0,0 @@
return {
"saghen/blink.cmp",
version = "*",
opts = {
keymap = { preset = "default" },
appearance = { nerd_font_variant = "mono" },
completion = { documentation = { auto_show = true } },
sources = { default = { "lsp", "path", "buffer" } },
},
}
-33
View File
@@ -1,33 +0,0 @@
return {
{
"mfussenegger/nvim-dap",
keys = {
{ "<leader>db", function() require("dap").toggle_breakpoint() end, desc = "Toggle breakpoint" },
{ "<leader>dc", function() require("dap").continue() end, desc = "Debug: continue" },
{ "<leader>di", function() require("dap").step_into() end, desc = "Debug: step into" },
{ "<leader>do", function() require("dap").step_over() end, desc = "Debug: step over" },
{ "<leader>dO", function() require("dap").step_out() end, desc = "Debug: step out" },
{ "<leader>dr", function() require("dap").repl.toggle() end, desc = "Debug: toggle REPL" },
{ "<leader>dq", function() require("dap").terminate() end, desc = "Debug: terminate" },
},
},
{
-- Configures nvim-dap for Python using debugpy, installed via Mason
-- (:MasonInstall debugpy) so it lives in an isolated venv rather than
-- needing debugpy in every project's own environment.
"mfussenegger/nvim-dap-python",
dependencies = { "mfussenegger/nvim-dap" },
ft = "python",
config = function()
local python = vim.fn.stdpath("data") .. "/mason/packages/debugpy/venv/bin/python"
if vim.fn.executable(python) == 0 then
vim.notify(
"debugpy not installed: run :MasonInstall debugpy to enable Python debugging",
vim.log.levels.WARN
)
return
end
require("dap-python").setup(python)
end,
},
}
-137
View File
@@ -1,137 +0,0 @@
return {
{
"nvim-neo-tree/neo-tree.nvim",
branch = "v3.x",
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-tree/nvim-web-devicons",
"MunifTanjim/nui.nvim",
},
keys = {
{ "<leader>t", ":Neotree toggle<CR>", desc = "Toggle file explorer" },
{
"<leader>cd",
function()
vim.cmd("cd " .. vim.fn.fnameescape(vim.fn.expand("%:p:h")))
vim.cmd("pwd")
vim.cmd("Neotree dir=" .. vim.fn.fnameescape(vim.fn.getcwd()))
end,
desc = "cd to current file's directory and open explorer",
},
},
opts = {},
},
{
"nvim-telescope/telescope.nvim",
dependencies = {
"nvim-lua/plenary.nvim",
{ "nvim-telescope/telescope-fzf-native.nvim", build = "make" },
},
cmd = "Telescope",
keys = {
{ "<leader>.", "<cmd>Telescope lsp_document_symbols<CR>", desc = "Jump to symbol" },
{ "<leader>;", "<cmd>Telescope buffers<CR>", desc = "Jump to buffer" },
{
"<leader>be",
"<cmd>Telescope buffers sort_mru=true ignore_current_buffer=true<CR>",
desc = "Jump to buffer (MRU)",
},
{ "<leader>ag", "<cmd>Telescope live_grep<CR>", desc = "Grep" },
{ "<leader>?", "<cmd>Telescope keymaps<CR>", desc = "Search keybindings" },
},
config = function()
local telescope = require("telescope")
telescope.setup({})
telescope.load_extension("fzf")
end,
},
{
"folke/flash.nvim",
event = "VeryLazy",
opts = {},
keys = {
{ "<leader>/", function() require("flash").jump() end, mode = { "n", "x", "o" }, desc = "Flash jump" },
{ "<leader>s", function() require("flash").jump({ search = { mode = "search" } }) end, mode = { "n", "x", "o" }, desc = "Flash search jump" },
{ "<leader><leader>s", function() require("flash").treesitter() end, mode = { "n", "x", "o" }, desc = "Flash treesitter" },
{ "<leader>f", function() require("flash").jump({ search = { forward = true, wrap = false } }) end, mode = { "n", "x", "o" }, desc = "Flash forward" },
},
},
{ "kylechui/nvim-surround", event = { "BufReadPost", "BufNewFile" }, opts = {} },
{ "windwp/nvim-autopairs", event = "InsertEnter", opts = {} },
{
"nvim-treesitter/nvim-treesitter-context",
event = { "BufReadPost", "BufNewFile" },
dependencies = { "nvim-treesitter/nvim-treesitter" },
opts = {},
},
{
"echasnovski/mini.align",
version = "*",
keys = {
{ "ga=", mode = { "n", "x" }, desc = "Align on =" },
{ "ga:", mode = { "n", "x" }, desc = "Align on :" },
},
config = function()
require("mini.align").setup({
mappings = { start = "ga", start_with_preview = "" },
})
end,
},
{ "echasnovski/mini.bufremove", version = "*", keys = {
{ "<leader><leader>c", function() require("mini.bufremove").delete(0, false) end, desc = "Close buffer" },
} },
{
"mg979/vim-visual-multi",
branch = "master",
keys = { "<C-n>" },
init = function()
vim.g.VM_maps = {
["Find Under"] = "<C-n>",
["Find Subword Under"] = "<C-n>",
}
end,
},
{
"christoomey/vim-tmux-navigator",
lazy = false,
-- Plugin's own default mappings rely on a Vim-only <C-w> terminal escape
-- that Neovim doesn't support, so :terminal buffers (e.g. claudecode.nvim)
-- leak the raw keystrokes to the embedded process. Disable them and
-- define our own, using Neovim's actual terminal-mode escape.
init = function()
vim.g.tmux_navigator_no_mappings = 1
end,
keys = {
{ "<C-h>", "<cmd>TmuxNavigateLeft<CR>" },
{ "<C-j>", "<cmd>TmuxNavigateDown<CR>" },
{ "<C-k>", "<cmd>TmuxNavigateUp<CR>" },
{ "<C-l>", "<cmd>TmuxNavigateRight<CR>" },
{ "<C-h>", "<C-\\><C-n><cmd>TmuxNavigateLeft<CR>", mode = "t" },
{ "<C-j>", "<C-\\><C-n><cmd>TmuxNavigateDown<CR>", mode = "t" },
{ "<C-k>", "<C-\\><C-n><cmd>TmuxNavigateUp<CR>", mode = "t" },
{ "<C-l>", "<C-\\><C-n><cmd>TmuxNavigateRight<CR>", mode = "t" },
},
},
{
"mattn/emmet-vim",
ft = { "html", "css" },
init = function()
vim.g.user_emmet_install_global = 0
end,
config = function()
vim.api.nvim_create_autocmd("FileType", {
pattern = { "html", "css" },
callback = function() vim.cmd("EmmetInstall") end,
})
end,
},
}
-55
View File
@@ -1,55 +0,0 @@
return {
{
"stevearc/conform.nvim",
event = { "BufWritePre" },
cmd = { "ConformInfo" },
opts = {
formatters_by_ft = {
css = { "prettier" },
svelte = { "prettier" },
pcss = { "prettier" },
html = { "prettier" },
typescript = { "prettier" },
typescriptreact = { "prettier" },
javascript = { "prettier" },
javascriptreact = { "prettier" },
json = { "prettier" },
yaml = { "prettier" },
markdown = { "prettier" },
python = { "ruff_format" },
rust = { "rustfmt" },
lua = { "stylua" },
},
format_on_save = {
lsp_format = "fallback",
timeout_ms = 2000,
},
},
},
{
"mfussenegger/nvim-lint",
event = { "BufWritePost", "BufReadPost", "InsertLeave" },
config = function()
require("lint").linters_by_ft = {
javascript = { "eslint_d" },
typescript = { "eslint_d" },
javascriptreact = { "eslint_d" },
typescriptreact = { "eslint_d" },
svelte = { "eslint_d" },
}
if vim.fn.executable("shellcheck") == 1 then
require("lint").linters_by_ft.sh = { "shellcheck" }
else
vim.notify("shellcheck not found: skipping shell linting", vim.log.levels.WARN)
end
vim.api.nvim_create_autocmd({ "BufWritePost", "BufReadPost", "InsertLeave" }, {
callback = function()
require("lint").try_lint()
end,
})
end,
},
}
-4
View File
@@ -1,4 +0,0 @@
return {
{ "lewis6991/gitsigns.nvim", event = { "BufReadPre", "BufNewFile" }, opts = {} },
{ "tpope/vim-fugitive", cmd = { "Git", "Gdiffsplit", "Gvdiffsplit", "Gread", "Gwrite", "Gclog" } },
}
-59
View File
@@ -1,59 +0,0 @@
local servers = { "basedpyright", "ruff" }
local npm_servers = { "ts_ls", "svelte", "html", "cssls", "bashls" }
local function has_public_npm()
if vim.fn.executable("npm") ~= 1 then
return false
end
local result = vim.fn.system("npm config get registry")
return result:find("registry.npmjs.org") ~= nil
end
if has_public_npm() then
vim.list_extend(servers, npm_servers)
else
vim.notify(
"public npm registry unavailable: skipping LSP servers " .. table.concat(npm_servers, ", "),
vim.log.levels.WARN
)
end
return {
{
"mason-org/mason.nvim",
opts = {},
},
{
"mason-org/mason-lspconfig.nvim",
dependencies = { "mason-org/mason.nvim" },
opts = {
ensure_installed = servers,
},
},
{
"neovim/nvim-lspconfig",
dependencies = { "mason-org/mason-lspconfig.nvim", "saghen/blink.cmp", "saghen/blink.lib" },
config = function()
local capabilities = require("blink.cmp").get_lsp_capabilities()
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("LspKeymaps", { clear = true }),
callback = function(args)
local opts = { buffer = args.buf }
vim.keymap.set("n", "K", vim.lsp.buf.hover, opts)
vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
vim.keymap.set("n", "gy", vim.lsp.buf.type_definition, opts)
vim.keymap.set("n", "gi", vim.lsp.buf.implementation, opts)
vim.keymap.set("n", "gr", vim.lsp.buf.references, opts)
vim.keymap.set("n", "rn", vim.lsp.buf.rename, opts)
vim.keymap.set("v", "ff", function() vim.lsp.buf.format({ async = true }) end, opts)
end,
})
for _, server in ipairs(servers) do
vim.lsp.config(server, { capabilities = capabilities })
vim.lsp.enable(server)
end
end,
},
}
-31
View File
@@ -1,31 +0,0 @@
return {
{
"stevearc/aerial.nvim",
dependencies = { "nvim-treesitter/nvim-treesitter", "nvim-tree/nvim-web-devicons" },
keys = {
{ "<leader>ct", "<cmd>AerialToggle<CR>", desc = "Toggle symbol outline" },
},
opts = {},
},
{
"jiaoshijie/undotree",
dependencies = { "nvim-lua/plenary.nvim" },
keys = {
{ "<leader>u", function() require("undotree").toggle() end, desc = "Toggle undo tree" },
},
opts = {},
},
{
"folke/persistence.nvim",
event = "BufReadPre",
opts = {},
},
{
"folke/which-key.nvim",
event = "VeryLazy",
opts = {},
},
}
-25
View File
@@ -1,25 +0,0 @@
return {
{
"mrcjkb/rustaceanvim",
version = "^6",
lazy = false,
init = function()
vim.g.rustaceanvim = {
tools = {},
server = {
on_attach = function(_, bufnr)
vim.keymap.set("n", "<localleader>rr", function()
vim.cmd.RustLsp("run")
end, { buffer = bufnr })
end,
},
}
end,
},
{
"mason-org/mason-lspconfig.nvim",
opts = {
automatic_enable = { exclude = { "rust_analyzer" } },
},
},
}
-100
View File
@@ -1,100 +0,0 @@
return {
"nvim-treesitter/nvim-treesitter",
lazy = false,
build = ":TSUpdate",
config = function()
local parsers = {
"javascript",
"typescript",
"tsx",
"svelte",
"html",
"css",
"scss",
"json",
"yaml",
"python",
"rust",
"bash",
"lua",
"vim",
"vimdoc",
"query",
"markdown",
"markdown_inline",
"regex",
}
-- Install parsers (skip if the tree-sitter CLI isn't available; parsers
-- already cached from a previous run on this machine still work fine)
if vim.fn.executable("tree-sitter") == 1 then
require("nvim-treesitter").install(parsers)
else
local uname = vim.uv.os_uname()
local sysname = (uname.sysname or ""):lower()
local machine = uname.machine or ""
local is_mac = sysname == "darwin"
local install_cmd
if is_mac then
local arch_part = ({ x86_64 = "x64", amd64 = "x64", aarch64 = "arm64", arm64 = "arm64" })[machine] or "x64"
local asset = "tree-sitter-macos-" .. arch_part
install_cmd = " curl -L https://github.com/tree-sitter/tree-sitter/releases/latest/download/"
.. asset
.. ".gz | gunzip > ~/.local/bin/tree-sitter && chmod +x ~/.local/bin/tree-sitter"
else
install_cmd = " cargo install --root ~/.local tree-sitter-cli"
end
vim.notify(
"tree-sitter CLI not found: skipping install of missing parsers.\n"
.. "Install into ~/.local/bin:\n"
.. install_cmd,
vim.log.levels.WARN
)
end
-- Enable treesitter highlighting, folding, and indentation for
-- filetypes with an installed parser. Note some filetype names differ
-- from their parser names (e.g. "bash" parser -> "sh" filetype).
vim.api.nvim_create_autocmd("FileType", {
pattern = {
"javascript",
"typescript",
"typescriptreact",
"javascriptreact",
"svelte",
"html",
"css",
"scss",
"json",
"yaml",
"python",
"rust",
"sh",
"lua",
"vim",
"help",
"query",
"markdown",
},
callback = function()
-- Highlighting, provided by Neovim. Falls back to Vim's legacy
-- syntax/indent if no parser is installed for this filetype.
if not pcall(vim.treesitter.start) then
return
end
-- Folding, provided by Neovim
vim.wo.foldexpr = "v:lua.vim.treesitter.foldexpr()"
vim.wo.foldmethod = "expr"
-- Indentation, provided by nvim-treesitter
vim.bo.indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()"
end,
})
-- General fold display settings (not treesitter-specific)
vim.opt.foldlevel = 2
vim.opt.foldlevelstart = 2
vim.opt.foldcolumn = "5"
end,
}
-101
View File
@@ -1,101 +0,0 @@
return {
{
"ellisonleao/gruvbox.nvim",
priority = 1000,
opts = {
italic = { strings = true, comments = true, operators = false, folds = true },
invert_selection = true,
invert_signs = false,
},
config = function(_, opts)
require("gruvbox").setup(opts)
vim.o.background = "dark"
local ok = pcall(vim.cmd.colorscheme, "gruvbox")
if not ok then
vim.cmd.colorscheme("desert")
end
end,
},
{
"nvim-lualine/lualine.nvim",
dependencies = { "nvim-tree/nvim-web-devicons" },
event = "VeryLazy",
opts = {
options = { theme = "gruvbox" },
},
},
{
"akinsho/bufferline.nvim",
dependencies = { "nvim-tree/nvim-web-devicons" },
event = "VeryLazy",
opts = {},
},
{
"lukas-reineke/indent-blankline.nvim",
main = "ibl",
event = { "BufReadPost", "BufNewFile" },
opts = {},
},
{
"HiPhish/rainbow-delimiters.nvim",
event = { "BufReadPost", "BufNewFile" },
},
{ "nvim-tree/nvim-web-devicons", opts = {} },
{
"goolord/alpha-nvim",
dependencies = { "nvim-tree/nvim-web-devicons" },
event = "VimEnter",
config = function()
local alpha = require("alpha")
local dashboard = require("alpha.themes.dashboard")
local function fortune_header()
-- cowsay -l prints a header line, then wraps every cow name across
-- several space-separated lines (not one name per line), so a name
-- has to be picked from all of them split on whitespace, not by
-- treating a whole (possibly multi-name) line as one name.
local raw = vim.fn.systemlist("cowsay -l")
local cow = "default"
local names = {}
for i = 2, #raw do
for name in raw[i]:gmatch("%S+") do
table.insert(names, name)
end
end
if #names > 0 then
cow = names[math.random(#names)]
end
local moods = { "b", "d", "g", "p", "s", "t", "w", "y" }
local mood = moods[math.random(#moods)]
local cmd = string.format(
"fortune -a -s | fmt -80 -s | cowthink -%s -f %s -n",
mood,
cow
)
local ok, lines = pcall(vim.fn.systemlist, cmd)
if not ok or #lines == 0 then
return { "Welcome to Neovim" }
end
return lines
end
dashboard.section.header.val = fortune_header()
dashboard.section.buttons.val = {
dashboard.button("e", " New file", ":enew<CR>"),
dashboard.button("f", " Find file", ":Telescope find_files<CR>"),
dashboard.button("g", " Live grep", ":Telescope live_grep<CR>"),
dashboard.button("s", " Restore session", ":lua require('persistence').load()<CR>"),
dashboard.button("q", " Quit", ":qa<CR>"),
}
alpha.setup(dashboard.opts)
vim.keymap.set("n", "<leader><leader>o", ":Alpha<CR>")
end,
},
}
-64
View File
@@ -1,64 +0,0 @@
vim.g.vimwiki_list = {
{
path = "~/synced-notebooks/bradwiki/",
syntax = "markdown",
ext = ".md",
diary_rel_path = "daily_notes/",
diary_index = "index",
diary_header = "Daily Notes",
auto_tags = 1,
auto_diary_index = 1,
auto_generate_links = 1,
auto_generate_tags = 1,
},
{
path = "~/synced-notebooks/work_notebook/",
syntax = "markdown",
ext = ".md",
diary_rel_path = "daily_notes/",
diary_index = "index",
diary_header = "Daily Notes",
auto_tags = 1,
auto_diary_index = 1,
auto_generate_links = 1,
auto_generate_tags = 1,
},
{ path = "~/synced-notebooks/privatewiki/", syntax = "markdown", ext = ".md" },
{ path = "~/synced-notebooks/sharedwiki/", syntax = "markdown", ext = ".md" },
}
vim.g.vimwiki_map_prefix = "<Leader>m"
vim.g.vimwiki_folding = "list"
vim.g.markdown_folding = 1
vim.g.zettel_options = {
{},
{
front_matter = { { "tags", "" }, { "type", "note" } },
template = "~/work_notebook/templates/daily_note.tpl",
},
}
vim.g.table_mode_map_prefix = "<Leader><Leader>t"
vim.api.nvim_create_autocmd("FileType", {
group = vim.api.nvim_create_augroup("VimwikiFiletype", { clear = true }),
pattern = "vimwiki",
callback = function()
vim.opt_local.breakindent = true
vim.opt_local.breakindentopt = "shift:2"
end,
})
return {
{ "vimwiki/vimwiki", lazy = false },
{ "michal-h21/vim-zettel", dependencies = { "vimwiki/vimwiki" }, lazy = false },
{ "dhruvasagar/vim-table-mode", cmd = "TableModeToggle" },
{
"iamcco/markdown-preview.nvim",
build = function() vim.fn["mkdp#util#install"]() end,
ft = { "markdown", "vimwiki" },
keys = {
{ "<localleader>v", "<Plug>MarkdownPreviewToggle", ft = { "markdown", "vimwiki" }, desc = "Toggle markdown preview" },
},
},
}
-28
View File
@@ -1,28 +0,0 @@
local M = {}
-- Open ranger from within nvim; if it writes a chosen path to its return
-- file, open that file. Mirrors the old vimscript Ranger() function.
function M.ranger()
if vim.fn.executable("ranger") == 0 then
vim.notify("ranger is not installed", vim.log.levels.WARN)
return
end
local tmpfile = vim.fn.system("mktemp -u"):gsub("\n", "")
vim.cmd(("silent !RANGER_RETURN_FILE=%s ranger"):format(tmpfile))
if vim.fn.filereadable(tmpfile) == 1 then
local filetoedit = vim.fn.system("cat " .. tmpfile)
vim.cmd("edit " .. filetoedit)
vim.fn.delete(tmpfile)
end
vim.cmd("redraw!")
end
-- Delete trailing whitespace on the current buffer, preserving cursor
-- position. Mirrors the old vimscript DeleteTrailingWS() function.
function M.delete_trailing_ws()
local view = vim.fn.winsaveview()
vim.cmd([[silent! %s/\s\+$//ge]])
vim.fn.winrestview(view)
end
return M
+3 -4
View File
@@ -22,6 +22,7 @@ shadow-exclude = [
"class_g = 'lattedock'",
"_GTK_FRAME_EXTENTS@:c"
]
xinerama-shadow-crop = true;
### Blur
blur:{
@@ -51,6 +52,8 @@ backend = "glx";
#backend = "xrender";
vsync = true;
glx-no-stencil = true;
glx-no-rebind-pixmap = true;
xrender-sync-fence = true;
dbus=true;
@@ -71,7 +74,3 @@ mark-wmwin-focused = true;
mark-ovredir-focused = true;
use-ewmh-active-win = true;
detect-client-leader = true;
## Fixes graphical issues (jitter/flickering) in Jetbrains IDEs
## TODO: This doesn't fully fix the issue
unredir-if-possible = false;
@@ -1,5 +1,5 @@
{
"groups": {
"stack_depth": { "fg": "gray1", "bg": "gray10", "attrs": ["bold"] }
"stack_depth": { "fg": "gb:light1", "bg": "gb:light4", "attrs": ["bold"] }
}
}
@@ -1,5 +0,0 @@
{
"groups": {
"stack_depth": { "fg": "gb:light1", "bg": "gb:light4", "attrs": ["bold"] }
}
}
+1 -1
View File
@@ -13,7 +13,7 @@
}
},
"pdb": {
"colorscheme": "gruvbox",
"colorscheme": "default",
"theme": "default"
},
"shell": {
+5 -129
View File
@@ -1,130 +1,6 @@
# Override in .profile.local for custom repo
DOTFILES_LOCAL_REPOSITORY="https://gitbox.branderson.io/branderson/dotfiles-local"
export TERMINAL=alacritty
export EDITOR=nvim
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket"
alias vim=nvim
XDG_DATA_DIRS="/var/lib/flatpak/exports/share:$XDG_DATA_DIRS"
# Programs expected to be installed by tools
# These are the name on $PATH, not the package name
dependencies=(
tree
nvim
ag
fzf
node
cowsay
fortune
)
# Load dotfiles directory into DOTFILES_DIR
# Dotfiles directory should be in environment or ~/.dotfiles-config
if [ -f "$HOME/.dotfiles-config" ]; then
source "$HOME/.dotfiles-config"
fi
# PATHs
## Earlier paths in PATH checked first
if [ -d /usr/games ]; then
export PATH="/usr/games:$PATH"
fi
if [ -d /opt/homebrew/opt/coreutils/libexec/gnubin ]; then
export PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH"
fi
if [ -d /opt/homebrew/opt/python@3.13/libexec/bin ]; then
export PATH="/opt/homebrew/opt/python@3.13/libexec/bin:$PATH"
fi
if [ -d /opt/homebrew/bin ]; then
export PATH="/opt/homebrew/bin:$PATH"
fi
## Keep user bin directories first in PATH so they're preferred
if [ -d "$DOTFILES_DIR/bin" ]; then
export PATH="$DOTFILES_DIR/bin:$PATH"
fi
## OS-specific binaries
if [[ $OSTYPE == 'darwin'* ]]; then
if [[ -d "$DOTFILES_DIR/macbin" ]]; then
export PATH="$DOTFILES_DIR/macbin:$PATH"
fi
elif [[ $OSTYPE == 'linux'* ]]; then
if [[ -d "$DOTFILES_DIR/linbin" ]]; then
export PATH="$DOTFILES_DIR/linbin:$PATH"
fi
fi
if [ -d "$HOME/.local/bin" ]; then
export PATH="$HOME/.local/bin:$PATH"
fi
if [ -d "$HOME/bin" ]; then
export PATH="$HOME/bin:$PATH"
fi
source "$HOME/.bash_functions"
# Set up brew if on macOS
if [[ $OSTYPE == 'darwin'* ]]; then
if [ -f /opt/homebrew/bin/brew ]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
fi
fi
# Prefer alacritty terminal if installed
if program_installed alacritty; then
export TERMINAL=alacritty
fi
# Prefer neovim, if installed
if program_installed nvim; then
export EDITOR=nvim
alias vim=nvim
fi
if program_installed powerline; then
# Make powerline support Python virtualenvs
export POWERLINE_COMMAND="$DOTFILES_DIR/scripts/powerline-wrapper.sh"
fi
# alias make='nocorrect make'
# alias git='nocorrect git'
# Crontab
#alias crontab-e='vim ~/.crontab && crontab ~/.crontab'
#alias sl="sl -laF"
# Put flatpak apps in XDG
if program_installed flatpak; then
export XDG_DATA_DIRS="/var/lib/flatpak/exports/share:$XDG_DATA_DIRS"
fi
# pyenv paths
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
# Make sure ssh-agent is available
if [ -n "$LOAD_SSH_KEYS" ] && [ "$LOAD_SSH_KEYS" -eq 1 ]; then
if program_installed ssh-add && [ ! -z "$SSH_AUTH_SOCK" ] && [ -d "$SSH_AUTH_SOCK" ] > /dev/null 2>&1 ; then
# Add SSH keys to agent if not already added
## Find existing keys, supporting one layer of folder organization
public_keys=($(find "$HOME/.ssh/" -maxdepth 2 -name "*.pub"))
if [ ${#public_keys[@]} -gt 0 ]; then
for public_key in "${public_keys[@]}"; do
if [[ $(uname -s) == Linux* ]]; then
# Add SSH keys only if not already present
ssh-add -l | grep -q `ssh-keygen -lf "$public_key" | awk '{print $2}'` || ssh-add -k
break
elif [[ $(uname -s) == Darwin* ]]; then
# Add SSH keys only if not already present
ssh-add -l | grep -q `ssh-keygen -lf "$public_key" | awk '{print $2}'` || ssh-add --apple-use-keychain
break
fi
done
fi
fi
fi
if program_installed i3; then
# Set QT environment variables
export QT_QPA_PLATFORMTHEME="qt5ct"
fi
# Extra domains claude-sandbox's firewall should allow beyond its defaults
export ALLOWED_DOMAINS=""
# Gitea host, used by bin/gitea-pr and claude-sandbox's git-push-guard hook
export GITEA_SSH_HOST="gitbox.bradina.localdomain"
export GITEA_API="https://gitbox.branderson.io/api/v1"
@@ -1,37 +0,0 @@
[Unit]
After=network-online.target nss-lookup.target
ConditionPathIsDirectory=/etc/pacman.d/gnupg/
ConditionPathIsReadWrite=/etc/pacman.d/gnupg/
ConditionFileIsExecutable=/usr/bin/archlinux-keyring-wkd-sync
Description=Refresh existing keys of archlinux-keyring
Wants=network-online.target
[Service]
ExecStart=/usr/bin/archlinux-keyring-wkd-sync
CapabilityBoundingSet=
DeviceAllow=
LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=noaccess
ProtectSystem=strict
ReadWritePaths=/etc/pacman.d/gnupg
RemoveIPC=true
RestrictAddressFamilies=~AF_PACKET AF_NETLINK
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@resources
@@ -1,11 +0,0 @@
[Unit]
Description=Refresh existing PGP keys of archlinux-keyring regularly
[Timer]
OnCalendar=weekly
Persistent=true
RandomizedDelaySec=12h
[Install]
WantedBy=timers.target
-14
View File
@@ -1,14 +0,0 @@
[Unit]
Description=Update wallpapers when display change detected
[Service]
Type=simple
User=brad
Environment=DISPLAY=:0
ExecStartPre=/bin/sleep 3
# TODO: The file might not be here if ~/.dotfiles-config set to somewhere else
ExecStart=/home/brad/dotfiles/bin/update-wallpaper
[Install]
WantedBy=multi-user.target
+2 -163
View File
@@ -1,163 +1,2 @@
# ----------------------------------------------------------------------------
# Prefix
# ----------------------------------------------------------------------------
set -g prefix C-a
unbind C-b
bind C-a send-prefix
bind Tab last-window
# ----------------------------------------------------------------------------
# General behavior
# ----------------------------------------------------------------------------
set-option -g set-titles on
set -g default-terminal "screen-256color"
set -g history-limit 10000
set -sg escape-time 1
set-option -g base-index 1
setw -g pane-base-index 1
setw -g aggressive-resize on
# ----------------------------------------------------------------------------
# Windows and panes
# ----------------------------------------------------------------------------
bind T swap-window -t 1
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
bind c new-window -c "#{pane_current_path}"
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
# Smart pane switching with awareness of vim splits
is_vim='echo "#{pane_current_command}" | grep -iqE "(^|\/)g?(view|n?vim?)(diff)?$"'
# Claude Code also wants its own use of C-j (insert newline), same as vim
# wants C-h/j/k/l for its own window navigation -- forward it raw instead of
# letting tmux swallow it for pane-switching.
is_vim_or_claude='echo "#{pane_current_command}" | grep -iqE "(^|\/)(g?(view|n?vim?)(diff)?|claude)$"'
bind -n C-h if-shell "$is_vim" "send-keys C-h" "select-pane -L"
bind -n C-j if-shell "$is_vim_or_claude" "send-keys C-j" "select-pane -D"
bind -n C-k if-shell "$is_vim" "send-keys C-k" "select-pane -U"
bind -n C-l if-shell "$is_vim" "send-keys C-l" "select-pane -R"
bind -n 'C-\' if-shell "$is_vim" "send-keys 'C-\\'" "select-pane -l"
bind -r C-h select-window -t :-
bind -r C-l select-window -t :+
bind a setw synchronize-panes
# ----------------------------------------------------------------------------
# Copy mode and mouse
# ----------------------------------------------------------------------------
setw -g mode-keys vi
set-option -g -q mouse on
bind y run 'tmux save-buffer - | $HOME/dotfiles/bin/clipboard-copy'
bind C-y run 'tmux save-buffer - | $HOME/dotfiles/bin/clipboard-copy'
# Let escape sequences a pane explicitly DCS-wraps for passthrough (e.g.
# sixel image data, see the iTerm2 terminal-features line below) reach the
# real terminal, even when nested behind ssh/tmux layers. Not used for the
# clipboard -- that has its own relay via the pane-set-clipboard hook above.
# allow-passthrough was added in tmux 3.3; older tmux doesn't understand the
# option, so only set it when the running tmux is new enough.
if-shell "tmux -V | awk '{print $2}' | awk -F. '{if ($1*1000+$2 >= 3003) exit 0; else exit 1}'" 'set -g allow-passthrough on'
# Let tmux tell Shift+Enter apart from plain Enter and forward that
# distinction inward, so Claude Code's Shift+Enter-inserts-newline shortcut
# works. extended-keys/terminal-features need tmux >= 3.2. The terminal
# name is matched with '*' rather than the commonly-suggested 'xterm*',
# because a nested tmux's "terminal" is the outer tmux, which reports
# itself using default-terminal above (screen-256color) -- 'xterm*' would
# never match that, breaking the nested case this is meant to support.
#
# Investigated as a suspect for multi-line paste getting corrupted with
# literal CSI 27;<mod>;<key>~ garbage in place of newlines (see Claude Code
# issues #43169 and #30239) -- turning this off did not fix that paste
# corruption, so it's an upstream Claude Code paste-tokenizer bug, not
# something caused by this setting. Restored.
if-shell "tmux -V | awk '{print $2}' | awk -F. '{if ($1*1000+$2 >= 3002) exit 0; else exit 1}'" 'set -s extended-keys on; set -as terminal-features "*:extkeys"'
# Accept pane-originated OSC 52 clipboard-set attempts into a real tmux
# buffer. Only on-vs-off matters here -- off would refuse to even look at
# these escapes; "on" (rather than "external") is needed so the pane-set-
# clipboard hook below has an actual buffer to read.
set -g set-clipboard on
# tmux's own mechanism for forwarding an accepted clipboard-set (or its own
# copy-mode copies) to the real terminal is generating a fresh OSC 52 write
# via the "Ms" terminfo capability -- confirmed unreliable here (both
# pane-forwarded and tmux-generated OSC 52 silently failed to reach the
# clipboard even with Ms correctly registered against Alacritty). Relay it
# ourselves instead: whenever any pane sets the clipboard, grab what was
# just captured above and write it straight to this client's own terminal
# device. If that's a real terminal, this is the final hop and we're done.
# If it's actually another tmux one layer further out (e.g. this tmux is
# itself running inside an outer tmux over ssh), that data becomes pane
# output there, and the *same* hook -- present there too, since it's the
# same dotfiles -- fires again. That recurses cleanly through however many
# tmux layers are actually in the chain, without any of them needing to
# know the total count in advance.
set-hook -g pane-set-clipboard 'run-shell "tmux save-buffer - | $HOME/dotfiles/bin/tmux-clipboard-relay #{client_tty}"'
# The default/no-argument form of copy-pipe (used by double/triple-click and
# a couple of default bindings) pipes to this option instead of a literal
# command -- route it through the same working script as the explicit
# copy-pipe bindings in tmux/modern.conf.
set -g copy-command '$HOME/dotfiles/bin/clipboard-copy'
# iTerm2 supports SIXEL images (since 3.3.0) but doesn't set a distinct TERM
# to identify itself, so this matches on its usual "xterm-256color" report --
# any other client reporting that same TERM will also be treated as
# sixel-capable. Alacritty is deliberately left off: upstream Alacritty has
# no sixel support, so telling tmux otherwise would replace today's clean
# text placeholder with garbled escape data.
set -as terminal-features ",xterm-256color:sixel"
# tmux renamed the copy-mode key table (vi-copy -> copy-mode-vi) and changed
# bind-key's table flag (-t -> -T) in 2.4. Source the file matching the
# running tmux so old tmux never parses syntax it doesn't understand.
if-shell "tmux -V | awk '{print $2}' | awk -F. '{if ($1*1000+$2 < 2004) exit 0; else exit 1}'" 'source-file ~/.tmux/legacy.conf' 'source-file ~/.tmux/modern.conf'
# ----------------------------------------------------------------------------
# Misc bindings
# ----------------------------------------------------------------------------
bind-key C run-shell "cal $(date +%Y)"
bind r source-file ~/.tmux.conf \; display "Reloaded config"
# A pane whose connection died mid-redraw (e.g. an SSH session killed by the
# laptop sleeping) can get stuck with tmux thinking the cursor is hidden,
# since that's tracked from the pane's own output and nothing ever sent the
# matching show-cursor sequence. Re-run it in the active pane to fix it.
bind R send-keys "printf '\\033[?25h'" Enter
# ----------------------------------------------------------------------------
# Powerline (guarded so machines without it don't error on load)
# ----------------------------------------------------------------------------
run-shell "command -v powerline-daemon >/dev/null 2>&1 && powerline-daemon -q; true"
run-shell "command -v powerline-config >/dev/null 2>&1 && powerline-config tmux setup; true"
# ----------------------------------------------------------------------------
# Status bar
# ----------------------------------------------------------------------------
# Flash the status bar when this client's prefix key is armed, so nested
# tmux sessions (e.g. laptop -> ssh -> devbox tmux) make it obvious which
# layer is about to receive the next keystroke. Falls back to whatever
# status-style powerline set above (captured here) the rest of the time.
# Colors match gruvbox's own visual/selection convention (bg0 on orange).
set -g @status_style_armed "bg=#fe8019,fg=#282828"
run-shell 'tmux set-option -g @status_style_normal "$(tmux show-options -gv status-style)"'
set -g status-style "#{?client_prefix,#{@status_style_armed},#{@status_style_normal}}"
# ----------------------------------------------------------------------------
# Plugins (TPM)
# ----------------------------------------------------------------------------
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'
# Local overrides
if-shell "[ -f ~/.tmux_local.conf ]" 'source ~/.tmux_local.conf'
# Initialize TPM (must be the last line)
run '~/.tmux/plugins/tpm/tpm'
# run-shell "~/.tmux/verify_tmux_version.sh"
source-file "~/.tmux/tmux_3.1c.conf"
-16
View File
@@ -1,16 +0,0 @@
# tmux < 2.4 copy-mode key table (vi-copy, old -t flag) and mouse-wheel/
# PageUp passthrough. Sourced conditionally from tmux.conf; see
# tmux/modern.conf for tmux >= 2.4.
bind-key -t vi-copy PPage page-up
bind-key -t vi-copy NPage page-down
bind-key -t vi-copy WheelUpPane halfpage-up
bind-key -t vi-copy WheelDownPane halfpage-down
bind-key -t vi-copy v begin-selection
bind-key -t vi-copy y copy-pipe "$HOME/dotfiles/bin/clipboard-copy"
unbind-key -t vi-copy Enter
bind-key -t vi-copy Enter copy-pipe "$HOME/dotfiles/bin/clipboard-copy"
bind-key -n PPage if-shell -F "#{alternate_on}" "send-keys PPage" "copy-mode -e; send-keys PPage"
bind-key -n WheelUpPane if-shell -F -t = "#{alternate_on}" "send-keys -M" "select-pane -t =; copy-mode -e; send-keys -M"
bind-key -n WheelDownPane if-shell -F -t = "#{alternate_on}" "send-keys -M" "select-pane -t =; send-keys -M"
-24
View File
@@ -1,24 +0,0 @@
# tmux >= 2.4 copy-mode key table and mouse-wheel/PageUp passthrough.
# Sourced conditionally from tmux.conf; see tmux/legacy.conf for older tmux.
bind-key -T copy-mode-vi PPage send-keys -X page-up
bind-key -T copy-mode-vi NPage send-keys -X page-down
bind-key -T copy-mode-vi WheelUpPane send-keys -X halfpage-up
bind-key -T copy-mode-vi WheelDownPane send-keys -X halfpage-down
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi y send-keys -X copy-pipe "$HOME/dotfiles/bin/clipboard-copy"
unbind-key -T copy-mode-vi Enter
bind-key -T copy-mode-vi Enter send-keys -X copy-pipe "$HOME/dotfiles/bin/clipboard-copy"
# The default MouseDragEnd1Pane binding (copy-selection-and-cancel) relies on
# tmux's own OSC 52 write via the Ms terminfo capability, which doesn't
# reliably reach every terminal (confirmed broken against Alacritty here).
# Route mouse-drag copies through the same clipboard-copy script as keyboard
# yanks instead, so both use the mechanism that's actually proven to work.
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "$HOME/dotfiles/bin/clipboard-copy"
# Let mouse wheel / PageUp pass through to apps using the alternate screen
# (vim, less, etc.) instead of always entering copy-mode
bind-key -T root PPage if-shell -F "#{alternate_on}" "send-keys PPage" "copy-mode -e; send-keys PPage"
bind-key -T root WheelUpPane if-shell -F -t = "#{alternate_on}" "send-keys -M" "select-pane -t =; copy-mode -e; send-keys -M"
bind-key -T root WheelDownPane if-shell -F -t = "#{alternate_on}" "send-keys -M" "select-pane -t =; send-keys -M"
+37
View File
@@ -0,0 +1,37 @@
# PREFIX | splits horizontally and PREFIX - splits vertically
bind | split-window -h
bind - split-window -v
# mouse support
set -g mode-mouse on # scrolling
set -g mouse-select-pane on # clicking into panes
set -g mouse-resize-pane on # resizing panes
set -g mouse-select-window on # select windows
# toggle mouse support
bind m \
set -g mode-mouse on \;\
set -g mouse-resize-pane on \;\
set -g mouse-select-pane on \;\
set -g mouse-select-window on \;\
display 'Mouse: ON'
bind M \
set -g mode-mouse off \;\
set -g mouse-resize-pane off \;\
set -g mouse-select-pane off \;\
set -g mouse-select-window off \;\
display 'Mouse: OFF'
# ----------------------
# set some pretty colors
# ----------------------
# set pane colors - hilight the active pane
# set-option -g pane-border-fg colour235 #base02
# set-option -g pane-active-border-fg colour240 #base01
# # colorize messages in the command line
# set-option -g message-bg black #base02
# set-option -g message-fg brightred #orange
source "$HOME/.tmux/tmux_shared.conf"
+2
View File
@@ -0,0 +1,2 @@
source "$HOME/.tmux/tmux_shared.conf"
+48
View File
@@ -0,0 +1,48 @@
# PREFIX | splits horizontally and PREFIX - splits vertically
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
# open new window with pwd
bind c new-window -c "#{pane_current_path}"
# PREFIX T makes current window index 1
bind T swap-window -t 1
# use vim movement for panes
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
# mouse support
set-option -g -q mouse on
# Copy Mode
bind-key -t root PPage if-shell -F "#{alternate_on}" "send-keys PPage" "copy-mode -e; send-keys PPage"
bind-key -t vi-copy PPage page-up
bind-key -t vi-copy NPage page-down
# Mouse Wheel
bind-key -t root WheelUpPane if-shell -F -t = "#{alternate_on}" "send-keys -M" "select-pane -t =; copy-mode -e; send-keys -M"
bind-key -t root WheelDownPane if-shell -F -t = "#{alternate_on}" "send-keys -M" "select-pane -t =; send-keys -M"
bind-key -t vi-copy WheelUpPane halfpage-up
bind-key -t vi-copy WheelDownPane halfpage-down
# Make sure the client is always using the correct environment
# eval $(tmux switch-client \; show-environment -s)
# Copy/Paste
#
# provide access to clipboard
# set-option -g default-command "reattach-to-user-namespace -l zsh"
# set-window-option -g automatic-rename on
# use vim keybindings in copy mode
setw -g mode-keys vi
# v selection
bind-key -t vi-copy v begin-selection
bind-key -t vi-copy y copy-pipe "reattach-to-user-namespace pbcopy"
# make enter use copy pipe
unbind -t vi-copy Enter
bind-key -t vi-copy Enter copy-pipe "reattach-to-user-namespace pbcopy"
+42
View File
@@ -0,0 +1,42 @@
# PREFIX | splits horizontally and PREFIX - splits vertically
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
# open new window with pwd
bind c new-window -c "#{pane_current_path}"
# use vim keybindings in copy mode
setw -g mode-keys vi
# Copy Mode
bind-key -T copy-mode-vi PPage send-keys -X page-up
bind-key -T copy-mode-vi NPage send-keys -X page-down
# Mouse Wheel
bind-key -T copy-mode-vi WheelUpPane send-keys -X halfpage-up
bind-key -T copy-mode-vi WheelDownPane send-keys -X halfpage-down
# v selection
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi y send-keys -X copy-pipe "reattach-to-user-namespace pbcopy"
# make enter use copy pipe
unbind -T copy-mode-vi Enter
bind-key -T copy-mode-vi Enter send-keys -X copy-pipe "reattach-to-user-namespace pbcopy"
set-option -g -q mouse on
set-option -g -q mouse-resize-pane on
set-option -g -q mouse-select-pane on
set-option -g -q mouse-select-window on
# Enable middle click in urxvt
set-option -ga terminal-override ',rxvt-uni*:XT:Ms=\E]52;%p1%s;%p2%s\007'
# Copy Mode
bind-key -T root PPage if-shell -F "#{alternate_on}" "send-keys PPage" "copy-mode -e; send-keys PPage"
# Mouse Wheel
bind-key -T root WheelUpPane if-shell -F -t = "#{alternate_on}" "send-keys -M" "select-pane -t =; copy-mode -e; send-keys -M"
bind-key -T root WheelDownPane if-shell -F -t = "#{alternate_on}" "send-keys -M" "select-pane -t =; send-keys -M"
source ~/.tmux/tmux_shared.conf
+42
View File
@@ -0,0 +1,42 @@
# PREFIX | splits horizontally and PREFIX - splits vertically
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
# open new window with pwd
bind c new-window -c "#{pane_current_path}"
# use vim keybindings in copy mode
setw -g mode-keys vi
# Copy Mode
bind-key -T copy-mode-vi PPage send-keys -X page-up
bind-key -T copy-mode-vi NPage send-keys -X page-down
# Mouse Wheel
bind-key -T copy-mode-vi WheelUpPane send-keys -X halfpage-up
bind-key -T copy-mode-vi WheelDownPane send-keys -X halfpage-down
# v selection
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi y send-keys -X copy-pipe "reattach-to-user-namespace pbcopy"
# make enter use copy pipe
unbind -T copy-mode-vi Enter
bind-key -T copy-mode-vi Enter send-keys -X copy-pipe "reattach-to-user-namespace pbcopy"
set-option -g -q mouse on
set-option -g -q mouse-resize-pane on
set-option -g -q mouse-select-pane on
set-option -g -q mouse-select-window on
# Enable middle click in urxvt
set-option -ga terminal-override ',rxvt-uni*:XT:Ms=\E]52;%p1%s;%p2%s\007'
# Copy Mode
bind-key -T root PPage if-shell -F "#{alternate_on}" "send-keys PPage" "copy-mode -e; send-keys PPage"
# Mouse Wheel
bind-key -T root WheelUpPane if-shell -F -t = "#{alternate_on}" "send-keys -M" "select-pane -t =; copy-mode -e; send-keys -M"
bind-key -T root WheelDownPane if-shell -F -t = "#{alternate_on}" "send-keys -M" "select-pane -t =; send-keys -M"
source ~/.tmux/tmux_shared.conf
+106
View File
@@ -0,0 +1,106 @@
# use UTF8
# set -g utf8
# set-window-option -g utf8 on
# 256 color mode
set -g default-terminal "screen-256color"
# set-option -sa terminal-overrides ',screen*:RGB'
# set-option -ga terminal-overrides ',screen*:Tc'
# set-option -sa terminal-overrides ',xterm*:RGB'
# set-option -ga terminal-overrides ',xterm*:Tc'
# set-option -sa terminal-overrides ',tmux*:RGB'
# set-option -ga terminal-overrides ',tmux*:Tc'
# set-option -sa terminal-overrides ',rxvt*:RGB'
# set-option -ga terminal-overrides ',rxvt*:Tc'
# Properly handle ssh agent forwarding when reconnecting to session
# set-environment -g 'SSH_AUTH_SOCK' ~/.ssh/ssh_auth_sock
set-option -g set-titles on
# Tmux attach will create a new session if one doesn't exist
# new-session -n $HOST
# set scrollback history
set -g history-limit 10000
# set Ctrl-a as prefix and unbind Ctrl-b
set -g prefix C-a
unbind C-b
# last active window
# bind-key C-a last-window
bind Tab last-window
# pass prefix through to application
bind C-a send-prefix
# don't constrain size unless smaller client looking at window
setw -g aggressive-resize on
# set command delay
set -sg escape-time 1
# set base index to 1 rather than 0
set-option -g base-index 1
setw -g pane-base-index 1
# use PREFIX-r to reload config
bind r source-file ~/.tmux/tmux_shared.conf \; display "Reloaded config"
# PREFIX T makes current window index 1
bind T swap-window -t 1
# use vim movement for panes
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
# Smart pane switching with awareness of vim splits
is_vim='echo "#{pane_current_command}" | grep -iqE "(^|\/)g?(view|n?vim?)(diff)?$"'
bind -n C-h if-shell "$is_vim" "send-keys C-h" "select-pane -L"
bind -n C-j if-shell "$is_vim" "send-keys C-j" "select-pane -D"
bind -n C-k if-shell "$is_vim" "send-keys C-k" "select-pane -U"
bind -n C-l if-shell "$is_vim" "send-keys C-l" "select-pane -R"
bind -n 'C-\' if-shell "$is_vim" "send-keys 'C-\\'" "select-pane -l"
# bind C-h and C-l to cycle through panes
bind -r C-h select-window -t :-
bind -r C-l select-window -t :+
# Synchonize keyboard input across panes
bind a setw synchronize-panes
# open calendar
bind-key C run-shell "cal $(date +%Y)"
# Make sure the client is always using the correct environment
# eval $(tmux switch-client \; show-environment -s)
bind y run 'tmux save-buffer - | reattach-to-user-namespace pbcopy '
bind C-y run 'tmux save-buffer - | reattach-to-user-namespace pbcopy '1
# ----------------------
# set some pretty colors
# ----------------------
# set pane colors - hilight the active pane
# set-option -g pane-border-fg colour235 #base02
# set-option -g pane-active-border-fg colour240 #base01
# Show active pane
# set-window-option -g window-style 'bg=#181818'
# set-window-option -g window-active-style 'bg=black'
# set-window-option -g pane-active-border-style ''
# set-window-option -g pane-active-border-style 'fg=#181818'
# colorize messages in the command line
# set-option -g message-bg black #base02
# set-option -g message-fg brightred #orange
# Powerline
run-shell "powerline-daemon -q"
run-shell "powerline-config tmux setup"
# source-file $POWERLINE_ROOT/bindings/tmux/powerline.conf
# Local overrides
if-shell "[ -f ~/.tmux_local.conf ]" 'source ~/.tmux_local.conf'
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
verify_tmux_version() {
tmux_home=~/.tmux
tmux_version="$(tmux -V | cut -c 6-)"
# This is necessary because 3.1c gives a syntax error in bc
# TODO Fix this it will break with the next tmux version
if [[ $tmux_version -eq "3.1c"]] ; then
tmux source-file $tmux_home/tmux_3.1c.conf
exit
if [[ $(echo "$tmux_version >= 2.5" | bc) -eq 1 ]] ; then
tmux source-file $tmux_home/tmux_3.1c.conf
exit
elif [[ $(echo "$tmux_version >= 2.1" | bc) -eq 1 ]] ; then
tmux source-file $tmux_home/tmux_2.1_to_2.5.conf
exit
elif [[ $(echo "$tmux_version >= 1.9" | bc) -eq 1 ]] ; then
tmux source-file $tmux_home/tmux_1.9_to_2.1.conf
exit
else
tmux source-file $tmux_home/tmux_1.9_down.conf
exit
fi
}
verify_tmux_version
-1
View File
@@ -1 +0,0 @@
ACTION=="change", SUBSYSTEM=="drm", RUN+="/usr/bin/systemctl start --no-block update-wallpapers.service"
+3 -90
View File
@@ -1,30 +1,3 @@
# Returns 0 if program is installed and 1 otherwise
function program_installed {
local return_=0
type $1 >/dev/null 2>&1 || { local return_=1; }
return "$return_"
}
function cd {
builtin cd "$@"
if [[ -z "$VIRTUAL_ENV" ]] ; then
# If env folder is found then activate the vitualenv
if [[ -d ./venv ]] ; then
source ./venv/bin/activate
fi
else
# If no longer within virtualenv, deactivate
parentdir="$(dirname "$VIRTUAL_ENV")"
if [[ "$PWD"/ != "$parentdir"/* ]] ; then
deactivate
fi
fi
}
function list_processes() {
# List all processes matching the given process name string
ps aux | grep $(whoami) | grep -i "$1" | grep -v grep
@@ -78,19 +51,13 @@ function check_dotfiles_version {
# Return 2 if the repo is behind the remote
# Return 3 if the repo is ahead of the remote
# Return 4 if the repo has diverged from the remote
# Return 5 if DOTFILES_DIR is not set
if [ -z $DOTFILES_DIR ]; then
return 5
fi
if [ ! -d $DOTFILES_DIR ]; then
if [ ! -d $HOME/dotfiles ]; then
return 2
fi
pushd $DOTFILES_DIR > /dev/null
pushd $HOME/dotfiles > /dev/null
# TODO: This may be gitbox
git fetch origin > /dev/null
git fetch origin
UPSTREAM=${1:-'@{u}'}
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse "$UPSTREAM")
@@ -108,57 +75,3 @@ function check_dotfiles_version {
return 4
fi
}
function set_python_roots {
PYTHON_ROOTS=()
if [ -d $HOME/.local/lib/ ]; then
for directory in $(echo $HOME/.local/lib/python*(N)); do
PYTHON_ROOTS+=$directory
done
fi
if [ -d $HOME/.local/share/pipx/venvs/powerline-status/lib/ ]; then
for directory in $(echo $HOME/.local/share/pipx/venvs/powerline-status/lib/python*(N)); do
PYTHON_ROOTS+=$directory
done
fi
if [ -d $HOME/.local/pipx/venvs/powerline-status/lib/ ]; then
for directory in $(echo $HOME/.local/pipx/venvs/powerline-status/lib/python*(N)); do
PYTHON_ROOTS+=$directory
done
fi
if [ -d /usr/lib/ ]; then
for directory in $(echo /usr/lib/python*(N)); do
PYTHON_ROOTS+=$directory
done
fi
if [ -d /usr/local/lib/ ]; then
# TODO: Make this not print warnings when the directory doesn't exit
for directory in $(echo /usr/local/lib/python*(N)); do
PYTHON_ROOTS+=$directory
done
fi
}
function set_powerline_zsh_sources {
set_python_roots
# Powerline plugin from distribution agnostic install directory
## Assemble list of possible sources
declare -ag POWERLINE_ZSH_SOURCES=()
if [[ -a $POWERLINE_ROOT/bindings/zsh/powerline.zsh ]]; then
# Prefer user-set powerline directory
POWERLINE_ZSH_SOURCES+=$POWERLINE_ROOT/bindings/zsh/powerline.zsh
fi
if [ -d /usr/share/powerline ]; then
# Apt installs here
POWERLINE_ZSH_SOURCES+="/usr/share/powerline/bindings/zsh/powerline.zsh"
fi
# If $POWERLINE_ROOT not set or set incorrectly, try default locations
for directory in $PYTHON_ROOTS; do
#for directory in $(echo $template); do
if [ -d "$directory" ]; then
POWERLINE_ZSH_SOURCES+="$directory"/site-packages/powerline/bindings/zsh/powerline.zsh
POWERLINE_ZSH_SOURCES+="$directory"/dist-packages/powerline/bindings/zsh/powerline.zsh
fi
# done
done
}
+185 -191
View File
@@ -1,92 +1,93 @@
# ZSH-specific environment variables
# Override in .profile.local
## oh-my-zsh
# Automatically update oh-my-zsh without asking
DISABLE_UPDATE_PROMPT=true
# Disable oh-my-zsh auto-update checks
DISABLE_AUTO_UPDATE=false
# How often to check for oh-my-zsh updates
UPDATE_ZSH_DAYS=30
# Disable cowsay on login by default.
NO_ZSHRC_COWSAY=1
# Theme to load [~/.oh-my-zsh/themes/]
ZSH_THEME="agnoster"
# Automatic window title setting
DISABLE_AUTO_TITLE="true"
# Command correction support
ENABLE_CORRECTION="false"
# Case-sensitive completion
CASE_SENSITIVE="false"
# ls colors
DISABLE_LS_COLORS="false"
# Red ... on completion waiting
COMPLETION_WAITING_DOTS="true"
# Uncomment the following line if you want to disable marking untracked files
# under VCS as dirty. This makes repository status check for large repositories
# much, much faster.
# DISABLE_UNTRACKED_FILES_DIRTY="true"
#
# Uncomment the following line if you want to change the command execution time
# stamp shown in the history command output.
# The optional three formats: "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd"
# HIST_STAMPS="mm/dd/yyyy"
# User profiles
source $HOME/.profile
# Define $POWERLINE_ROOT
if [ -f $HOME/.profile.local ]; then
set -a
source $HOME/.profile.local
set +a
fi
# Gruvbox colors
# source "$HOME/.vim/bundle/gruvbox/gruvbox_256palette.sh"
[[ -f "$HOME/dotfiles/dependencies/gruvbox/gruvbox_256palette.sh" ]] && source "$HOME/dotfiles/dependencies/gruvbox/gruvbox_256palette.sh"
# Custom functions
source "$HOME/.zsh_functions"
if [ -n "$DOTFILES_DIR" ] && [ -d "$DOTFILES_DIR" ]; then
# Load dependencies
## oh-my-zsh
if [ ! -f "$DOTFILES_DIR/dependencies/ohmyzsh/oh-my-zsh.sh" ]; then
cd "$DOTFILES_DIR"
git submodule update --init
cd -
fi
export ZSH=$DOTFILES_DIR/dependencies/ohmyzsh
## Gruvbox colors
[[ -f "$DOTFILES_DIR/dependencies/gruvbox/gruvbox_256palette.sh" ]] && source "$DOTFILES_DIR/dependencies/gruvbox/gruvbox_256palette.sh"
fi
missing_dependencies=()
for dependency in "${dependencies[@]}"; do
if ! program_installed $dependency; then
missing_dependencies+="$dependency"
fi
done
if [ "${#missing_dependencies[@]}" -gt 0 ]; then
echo "Missing dependencies: ${missing_dependencies[@]}"
fi
# Path to your oh-my-zsh installation.
export ZSH=$HOME/dotfiles/dependencies/ohmyzsh
#export RUST_SRC_PATH=/usr/local/src/rust/src
# Dircolors
# eval $(dircolors ~/.dircolors)
# Z
# . $HOME/dotfiles/z/z.sh
# Set name of the theme to load.
# # Look in ~/.oh-my-zsh/themes/
# # Optionally, if you set this to "random", it'll load a random theme each
# # time that oh-my-zsh is loaded.
ZSH_THEME="agnoster"
# Base16 Shell
# BASE16_SHELL="$HOME/.config/base16-shell/base16-tomorrow.dark.sh"
# [[ -s $BASE16_SHELL ]] && source $BASE16_SHELL
# BASE16_SHELL="$HOME/.config/base16-shell/base16-ocean.dark.sh"
# [[ -s $BASE16_SHELL ]] && source $BASE16_SHELL
#
# # Uncomment the following line to use case-sensitive completion.
# # CASE_SENSITIVE="true"
#
# # Uncomment the following line to disable bi-weekly auto-update checks.
# # DISABLE_AUTO_UPDATE="true"
#
# # Uncomment the following line to change how often to auto-update (in days).
# # export UPDATE_ZSH_DAYS=13
#
# # Uncomment the following line to disable colors in ls.
# # DISABLE_LS_COLORS="true"
#
# # Uncomment the following line to disable auto-setting terminal title.
# # DISABLE_AUTO_TITLE="true"
#
# # Uncomment the following line to enable command auto-correction.
# # ENABLE_CORRECTION="true"
#
# # Uncomment the following line to display red dots whilst waiting for completion.
# # COMPLETION_WAITING_DOTS="true"
#
# # Uncomment the following line if you want to disable marking untracked files
# # under VCS as dirty. This makes repository status check for large repositories
# # much, much faster.
# # DISABLE_UNTRACKED_FILES_DIRTY="true"
#
# # Uncomment the following line if you want to change the command execution time
# # stamp shown in the history command output.
# # The optional three formats: "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd"
# # HIST_STAMPS="mm/dd/yyyy"
#
# # Would you like to use another custom folder than $ZSH/custom?
# # ZSH_CUSTOM=/path/to/new-custom-folder
# # zsh-syntax-highlighting must come last
plugins=(archlinux colorize command-not-found cp git github bower grunt npm macos)
# tmux
zmodload zsh/zpty
# # User configuration
# Enable environment variables as CD directories
setopt cdablevars
# Enable correction for mistyped commands
unsetopt correctall
setopt appendhistory autocd extendedglob
unsetopt beep
# alias make='nocorrect make'
# alias git='nocorrect git'
alias -s txt=vim
alias -s .c=vim
#PATHs
#export PATH=$HOME/bin:/usr/local/bin:$PATH
#export MANPATH="/usr/local/man:$MANPATH"
#PATH="$(ruby -e 'puts Gem.user_dir')/bin:$PATH"
# Add user cowpath to COWPATH
#COWPATH="$COWPATH:$HOME/dotfiles/cowfiles"
# Make a random (cow?) with a random face say something
# fortune -a | fmt -80 -s | cowthink -$(shuf -n 1 -e b d g p s t w y) -f $(shuf -n 1 -e $(cowsay -l | tail -n +2)) -n
if [ -d $ZSH ] && [ ! -z $ZSH ]; then
source $ZSH/oh-my-zsh.sh
# User profiles
source $HOME/.profile
# Define $POWERLINE_ROOT
if [ -f $HOME/.profile.local ]; then
source $HOME/.profile.local
fi
source $ZSH/oh-my-zsh.sh
# Italic support
if [ -f $HOME/.xterm-256color-italic.terminfo ]; then
tic $HOME/.xterm-256color-italic.terminfo
@@ -95,19 +96,114 @@ if [ -f $HOME/.screen-256color-italic.terminfo ]; then
tic $HOME/.screen-256color-italic.terminfo
fi
# TODO: Remove this
# Use neovim where available
# if type nvim > /dev/null 2>&1; then
# alias vim='nvim'
# fi
if type nvim > /dev/null 2>&1; then
alias vim='nvim'
fi
# # Preferred editor for local and remote sessions
# # if [[ -n $SSH_CONNECTION ]]; then
# export EDITOR='vim'
export EDITOR='vim'
# export TERM=xterm-256color
# TODO ====
# # else
# # export EDITOR='mvim'
# # fi
#
# # Compilation flags
# # export ARCHFLAGS="-arch x86_64"
#
# # ssh
# # export SSH_KEY_PATH="~/.ssh/dsa_id"
#
# # Set personal aliases, overriding those provided by oh-my-zsh libs,
# # plugins, and themes. Aliases can be placed here, though oh-my-zsh
# # users are encouraged to define aliases within the ZSH_CUSTOM folder.
# # For a full list of active aliases, run `alias`.
# #
# # Example aliases
# # alias zshconfig="mate ~/.zshrc"
# # alias ohmyzsh="mate ~/.oh-my-zsh"
alias -s txt=vim
alias -s .c=vim
# Crontab
#alias crontab-e='vim ~/.crontab && crontab ~/.crontab'
# Lines configured by zsh-newuser-install
setopt appendhistory autocd extendedglob
unsetopt beep
bindkey -v
# End of lines configured by zsh-newuser-install
# The following lines were added by compinstall
zstyle :compinstall filename '~/.zshrc'
autoload -Uz compinit
compinit
# End of lines added by compinstall
DEFAULT_USER=brad
# Syntax highlighting configuration
# ZSH_HIGHLIGHT_STYLES[path]='bold'
# ZSH_HIGHLIGHT_STYLES[path_prefix]=none
# ZSH_HIGHLIGHT_STYLES[path_approx]='fg=yellow'
# ZSH_HIGHLIGHT_STYLES[globbing]='fg=yellow'
# Source files
#typeset -ga sources
#sources+=$HOME/dotfiles/shortcuts.txt
# Powerline plugin from distribution agnostic install directory
# pip install --user powerline-status &>/dev/null
if [[ -a $POWERLINE_ROOT/bindings/zsh/powerline.zsh ]]; then
# Prefer user-set powerline directory
sources+=$POWERLINE_ROOT/bindings/zsh/powerline.zsh
else
echo "Attempting to source Powerline bindings from default locations.\nPlease set \$POWERLINE_ROOT in .profile.local"
# If $POWERLINE_ROOT not set or set incorrectly, try default locations
sources+=$HOME/.local/lib/python2.6/site-packages/powerline/bindings/zsh/powerline.zsh
sources+=$HOME/.local/lib/python2.6/dist-packages/powerline/bindings/zsh/powerline.zsh
sources+=$HOME/.local/lib/python2.7/site-packages/powerline/bindings/zsh/powerline.zsh
sources+=$HOME/.local/lib/python2.7/dist-packages/powerline/bindings/zsh/powerline.zsh
sources+=$HOME/.local/lib/python3.6/site-packages/powerline/bindings/zsh/powerline.zsh
sources+=$HOME/.local/lib/python3.6/dist-packages/powerline/bindings/zsh/powerline.zsh
sources+=/usr/lib/python2.6/site-packages/powerline/bindings/zsh/powerline.zsh
sources+=/usr/lib/python2.6/dist-packages/powerline/bindings/zsh/powerline.zsh
sources+=/usr/local/lib/python2.6/site-packages/powerline/bindings/zsh/powerline.zsh
sources+=/usr/local/lib/python2.6/dist-packages/powerline/bindings/zsh/powerline.zsh
sources+=/usr/lib/python2.7/site-packages/powerline/bindings/zsh/powerline.zsh
sources+=/usr/lib/python2.7/dist-packages/powerline/bindings/zsh/powerline.zsh
sources+=/usr/local/lib/python2.7/site-packages/powerline/bindings/zsh/powerline.zsh
sources+=/usr/local/lib/python2.7/dist-packages/powerline/bindings/zsh/powerline.zsh
sources+=/usr/lib/python3.6/site-packages/powerline/bindings/zsh/powerline.zsh
sources+=/usr/lib/python3.6/dist-packages/powerline/bindings/zsh/powerline.zsh
sources+=/usr/local/lib/python3.6/site-packages/powerline/bindings/zsh/powerline.zsh
sources+=/usr/local/lib/python3.6/dist-packages/powerline/bindings/zsh/powerline.zsh
fi
foreach file (`echo $sources`)
if [[ -a $file ]]; then
source $file
fi
end
# Check dotfiles version
check_dotfiles_version
dotfiles_version=$?
if [[ $dotfiles_version = 1 ]]; then
echo "Dotfiles repository not present at $HOME/dotfiles"
elif [[ $dotfiles_version = 2 ]]; then
echo "Dotfiles repository at $HOME/dotfiles is outdated"
elif [[ $dotfiles_version = 3 ]]; then
echo "Dotfiles repository at $HOME/dotfiles is ahead of origin"
elif [[ $dotfiles_version = 4 ]]; then
echo "Dotfiles repository at $HOME/dotfiles has diverged from origin"
fi
# TMuxinator Completion
# source ~/bin/tmuxinator.zsh
# Dircolors
# eval $(dircolors ~/.dircolors)
# Keybindings
bindkey -M vicmd 'K' run-help
@@ -119,128 +215,26 @@ bindkey -M viins ',,' vi-cmd-mode
#bindkey '^o' ranger-cd
#zle -N terminal-clock
#bindkey '^t' terminal-clock
# TMuxinator Completion
# source ~/bin/tmuxinator.zsh
zstyle :compinstall filename '~/.zshrc'
autoload -Uz compinit
compinit
DEFAULT_USER="$USER"
# Syntax highlighting configuration
# ZSH_HIGHLIGHT_STYLES[path]='bold'
# ZSH_HIGHLIGHT_STYLES[path_prefix]=none
# ZSH_HIGHLIGHT_STYLES[path_approx]='fg=yellow'
# ZSH_HIGHLIGHT_STYLES[globbing]='fg=yellow'
# Source files
#typeset -ga sources
#sources+=$DOTFILES_DIR/shortcuts.txt
# Check dotfiles version
if [ -n "$NO_GIT_CHECK" ] && [ ! "$NO_GIT_CHECK" -eq 1 ]; then
# 5 second timeout to avoid delaying
timeout -k 5 5s $(which zsh) -c 'source ~/.zsh_functions && check_dotfiles_version'
dotfiles_version=$?
if [[ $dotfiles_version = 1 ]]; then
echo "Dotfiles repository not present at $DOTFILES_DIR"
elif [[ $dotfiles_version = 2 ]]; then
echo "Dotfiles repository at $DOTFILES_DIR is outdated"
elif [[ $dotfiles_version = 3 ]]; then
echo "Dotfiles repository at $DOTFILES_DIR is ahead of origin"
elif [[ $dotfiles_version = 4 ]]; then
echo "Dotfiles repository at $DOTFILES_DIR has diverged from origin"
elif [[ $dotfiles_version = 5 ]]; then
echo "Dotfiles repository not set in $HOME/.dotfiles-config"
echo "Please run ./install.sh in dotfiles repository and exit to create ~/.dotfiles-config"
echo "The following will not work until this is set:"
echo " - oh-my-zsh will not be started"
echo " - gruvbox color palette will not be loaded"
echo ""
fi
fi
# Powerline
if program_installed powerline; then
if [ -n "$POWERLINE_ROOT" ] && \
[ -f "$POWERLINE_ROOT" ] && \
[ -f "$POWERLINE_ROOT/bindings/zsh/powerline.zsh" ]; then
# Skip lookup if location is known
source "$POWERLINE_ROOT/bindings/zsh/powerline.zsh"
else
found=0
set_powerline_zsh_sources
for file in $POWERLINE_ZSH_SOURCES; do
if [[ -a $file ]]; then
# TODO: Store the POWERLINE_ROOT in .profile.local if found
source $file
found=1
break
fi
done
if [ $found -eq 0 ]; then
echo "Powerline binding not found.\nPlease set \$POWERLINE_ROOT in .profile.local"
fi
fi
fi
#alias sl="sl -laF"
# Tool options
export SHELLCHECK_OPTS="-e SC2029 -e SC2155"
# Limit claude-code: cap CPU/memory/process count
# Adjust the -p values if these limits are ever too tight.
# if program_installed claude && program_installed systemd-run; then
# claude() {
# systemd-run --user --scope --quiet \
# -p CPUQuota=50% \
# -p MemoryMax=6G \
# -p TasksMax=512 \
# -- command claude "$@"
# }
# fi
# Syntax highlighting
if [ -f /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]; then
source /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
fi
# fzf fuzzy finder
if program_installed fzf; then
# TODO: This is broken on debian fzf (no --zsh)
source <(fzf --zsh)
fi
# thefuck command correction
if program_installed fuck; then
eval "$(thefuck --alias)"
fi
# pyenv-virtualenv (for managing virtualenvs for different python versions)
if program_installed pyenv; then
export PYENV_ROOT="$HOME/.pyenv"
export PYENV_VIRTUALENV_DISABLE_PROMPT=1
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
# eval "$(pyenv virtualenv-init -)"
fi
# oh-my-zsh plugins (must come after all initialization above)
plugins=(archlinux colored-man-pages colorize command-not-found cp docker docker-compose fzf git history history-substring-search nmap npm pip poetry pyenv macos rsync snap ssh sudo systemd terraform thefuck tmux tmuxinator vi-mode virtualenv yarn zsh-interactive-cd zsh-navigation-tools)
# Local overrides
if [ -f $HOME/.zshrc_local ]; then
source $HOME/.zshrc_local
fi
# Fun stupid opt-in stuff on new shell. Enable these in .profile.local
if program_installed neofetch; then
if [ ! -n "$NO_NEOFETCH" ] || [ "$NO_NEOFETCH" -eq 1 ]; then
else
neofetch
fi
if [ -f ~/.fzf.zsh ]; then
source ~/.fzf.zsh
fi
if [ -n "$NO_ZSHRC_COWSAY" ] && [ ! "$NO_ZSHRC_COWSAY" -eq 1 ]; then
# Make a random (cow?) with a random face say something
fortune -a | fmt -80 -s | cowthink -$(shuf -n 1 -e b d g p s t w y) -f $(shuf -n 1 -e $(cowsay -l | tail -n +2)) -n
if [ -f /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]; then
source /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
fi
# pyenv-virtualenv (for managing virtualenvs for different python versions)
#export PYENV_VIRTUALENV_DISABLE_PROMPT=1
#eval "$(pyenv init -)"
#eval "$(pyenv virtualenv-init -)"
-261
View File
@@ -1,261 +0,0 @@
# Based on the official Anthropic reference dev container:
# https://github.com/anthropics/claude-code/tree/main/.devcontainer
FROM node:22
ARG TZ
ENV TZ="$TZ"
ARG CLAUDE_CODE_VERSION=latest
# Install basic development tools, iptables/ipset (for the egress firewall),
# openssh-client (for git-over-ssh and agent forwarding), and the toolchain
# nvim's plugin config (config/nvim, telescope/treesitter/mason) expects.
#
# Deliberately NOT installing bubblewrap/socat: those exist to let Claude
# Code's own internal per-command sandboxing (~/.claude/settings.json's
# "sandbox" block) and /remote-control run, but that inner sandbox is
# redundant on top of this container - it can't even function inside it
# anyway (bwrap can't create a namespace here, and fixing that would need
# privilege escalation approaching --privileged, undoing this container's
# own hardening). bin/claude-sandbox mounts a settings.json copy with
# sandbox.enabled forced to false, so there's nothing here to satisfy.
RUN apt-get update && apt-get install -y --no-install-recommends \
less \
git \
openssh-client \
procps \
fzf \
zsh \
man-db \
unzip \
gnupg2 \
gh \
iptables \
ipset \
iproute2 \
dnsutils \
aggregate \
jq \
dnsmasq \
ulogd2 \
ulogd2-json \
nano \
vim \
ripgrep \
fd-find \
build-essential \
python3 \
python3-pip \
python3-venv \
xz-utils \
cowsay \
fortune-mod \
fortunes-min \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& ln -s "$(command -v fdfind)" /usr/local/bin/fd
# cowsay/cowthink/fortune install to /usr/games on Debian, which isn't on
# PATH by default - config/nvim's start-screen header (plugins/ui.lua) shells
# out to them by bare name. Set via ENV (not a profile.d script) so it's
# picked up by every process unconditionally, including nvim's non-login
# subprocess spawns, which wouldn't source /etc/profile.d.
ENV PATH=/usr/games:$PATH
# Replace the base image's built-in `node` user (uid/gid 1000,
# $HOME=/home/node) with one matching the host user who runs
# bin/claude-sandbox - so absolute paths under $HOME (dotfiles, and
# ~/.claude/plugins/installed_plugins.json's own installPath bookkeeping,
# which Claude Code resolves as a literal host-absolute string rather than
# relative to whatever $HOME happens to be) resolve identically inside and
# outside the container, instead of needing a one-off dual-mount for every
# individual thing that assumes the real host path. Defaults below
# reproduce today's node/1000/1000//home/node exactly, so a build that
# never overrides them (bin/claude-sandbox always does; the test suite
# never does) exercises this same rename path rather than a separate,
# untested one.
ARG SANDBOX_USER=node
ARG SANDBOX_UID=1000
ARG SANDBOX_GID=1000
ARG SANDBOX_HOME=/home/node
RUN userdel -r node 2>/dev/null; \
groupdel node 2>/dev/null; \
groupadd -g "$SANDBOX_GID" "$SANDBOX_USER" && \
useradd -u "$SANDBOX_UID" -g "$SANDBOX_GID" -m -d "$SANDBOX_HOME" -s /bin/bash "$SANDBOX_USER"
ENV SANDBOX_USER=$SANDBOX_USER
ENV SANDBOX_HOME=$SANDBOX_HOME
# Ensure the sandbox user has access to /usr/local/share
RUN mkdir -p /usr/local/share/npm-global && \
chown -R $SANDBOX_USER:$SANDBOX_USER /usr/local/share
# Persist bash history.
RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
&& mkdir /commandhistory \
&& touch /commandhistory/.bash_history \
&& chown -R $SANDBOX_USER /commandhistory
# Set `DEVCONTAINER` environment variable to help with orientation
ENV DEVCONTAINER=true
# Disable non-essential network traffic (telemetry/error reporting) so the
# firewall allowlist can stay small.
ENV CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
# Lets Claude Code (and the SessionStart hook in ~/.claude/hooks) know it's
# running inside this container rather than directly on the host.
ENV CLAUDE_SANDBOX=1
# Dedicated unprivileged user for ulogd2 (see entrypoint.sh) - unlike
# dnsmasq, the ulogd2 Debian package doesn't create one of its own. ulogd2
# opens its log/output files and the NFLOG netlink socket before dropping to
# this user, so it doesn't need to own anything itself.
RUN useradd -r -s /usr/sbin/nologin ulogd
# Create the ssh directory and set permissions. The project directory itself
# is bind-mounted at runtime at its real host path (see docker-compose.yml),
# not a fixed /workspace, so it can share ~/.claude project/session history
# with the host. ~/.claude is bind-mounted too, not baked in here.
RUN mkdir -p $SANDBOX_HOME/.ssh && \
chown -R $SANDBOX_USER:$SANDBOX_USER $SANDBOX_HOME/.ssh && \
chmod 700 $SANDBOX_HOME/.ssh
# Pre-create the nvim data/state/cache dirs owned by the sandbox user,
# since they're mounted as named volumes in docker-compose.yml: Docker
# seeds a fresh named volume from whatever already exists at the mount
# path in the image, ownership included, so this is what keeps those
# volumes writable by that user instead of defaulting to root.
RUN mkdir -p $SANDBOX_HOME/.local/share/nvim $SANDBOX_HOME/.local/state/nvim $SANDBOX_HOME/.cache/nvim && \
chown -R $SANDBOX_USER:$SANDBOX_USER $SANDBOX_HOME/.local $SANDBOX_HOME/.cache
ARG GIT_DELTA_VERSION=0.18.2
RUN ARCH=$(dpkg --print-architecture) && \
wget "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb"
# gosu lets entrypoint.sh (root) drop to the node user for the actual
# session after setting up the firewall, without leaving any setuid/sudo
# escalation path available to node afterward. Verified against the
# published SHA256SUMS since it runs as root.
ARG GOSU_VERSION=1.19
RUN ARCH=$(dpkg --print-architecture) && \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-${ARCH}" && \
wget -O /tmp/gosu.sha256sums "https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/SHA256SUMS" && \
grep -E "gosu-${ARCH}\$" /tmp/gosu.sha256sums | sed "s#gosu-${ARCH}#/usr/local/bin/gosu#" | sha256sum -c - && \
rm /tmp/gosu.sha256sums && \
chmod +x /usr/local/bin/gosu
# Install the latest stable Neovim release. Debian's packaged version is too
# old for this config's plugins (blink.cmp etc. need a recent Neovim), so
# fetch the current release from GitHub directly rather than pinning a
# version that may go stale.
RUN asset_url=$(curl -fsSL https://api.github.com/repos/neovim/neovim/releases/latest \
| jq -r '[.assets[] | select(.name | test("linux.*x86_64.*\\.tar\\.gz$"))][0].browser_download_url') && \
wget -O nvim.tar.gz "$asset_url" && \
tar -C /usr/local --strip-components=1 -xzf nvim.tar.gz && \
rm nvim.tar.gz
# Install the latest stable Go toolchain. go.dev's own release JSON gives
# both the download URL and a SHA256 per file, verified below since this
# runs as root - same pattern as gosu above. `go build`/`go mod download`
# need proxy.golang.org and sum.golang.org reachable at runtime - see
# fixed-domains.sh.
RUN ARCH=$(dpkg --print-architecture) && \
release_json=$(curl -fsSL 'https://go.dev/dl/?mode=json') && \
filename=$(echo "$release_json" | jq -r --arg arch "$ARCH" \
'[.[] | select(.stable == true)][0].files[] | select(.os == "linux" and .arch == $arch and .kind == "archive") | .filename') && \
sha256=$(echo "$release_json" | jq -r --arg arch "$ARCH" \
'[.[] | select(.stable == true)][0].files[] | select(.os == "linux" and .arch == $arch and .kind == "archive") | .sha256') && \
wget -O go.tar.gz "https://go.dev/dl/${filename}" && \
echo "${sha256} go.tar.gz" | sha256sum -c - && \
tar -C /usr/local -xzf go.tar.gz && \
rm go.tar.gz
ENV PATH=$PATH:/usr/local/go/bin
# System libraries Chromium needs (fonts, libnss, libatk, etc.) for
# Playwright, installed here since it needs apt/root - the actual
# `playwright` package and browser binary install as node, further down,
# so they land in node's own npm prefix/cache correctly.
ARG PLAYWRIGHT_VERSION=1.61.1
RUN npx -y playwright@${PLAYWRIGHT_VERSION} install-deps chromium
# Set up non-root user
USER $SANDBOX_USER
# Install global packages
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=$PATH:/usr/local/share/npm-global/bin
# A global npm install only puts a package's CLI on PATH, not its module on
# Node's require() resolution path - NODE_PATH is what makes `require
# ("playwright")` work from any script anywhere, not just the project's own
# node_modules (which it likely doesn't have without its own npm install).
ENV NODE_PATH=/usr/local/share/npm-global/lib/node_modules
# Set the default shell to zsh rather than sh
ENV SHELL=/bin/zsh
# Set the default editor and visual
ENV EDITOR=nano
ENV VISUAL=nano
# Default powerline10k theme
ARG ZSH_IN_DOCKER_VERSION=1.2.0
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \
-p git \
-p fzf \
-a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
-a "source /usr/share/doc/fzf/examples/completion.zsh" \
-a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
-x
# Install Claude Code
RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}
# Install the tree-sitter CLI, which nvim-treesitter (plugins/treesitter.lua)
# needs to install/update parsers. Pinned below latest: newer tree-sitter-cli
# prebuilds require glibc >=2.39, which this (Debian 12 bookworm) image
# doesn't have.
RUN npm install -g tree-sitter-cli@0.24.7
# Playwright, for testing frontend changes against a locally-running dev
# server - localhost traffic never goes through the egress firewall, so
# this needs no allowlist changes for that. System deps were already
# installed as root above; this installs the npm package and downloads the
# matching Chromium build into node's own cache, both at build time so a
# session never needs network access for either.
#
# Chromium's own internal process sandbox is questionable here: it launches
# without --no-sandbox and without error, but its setuid sandbox helper
# (chrome_sandbox) isn't actually setuid-root in this image, and this host
# has the same unprivileged-userns restriction that broke bubblewrap - so
# its namespace-based isolation almost certainly isn't active either, it's
# just not hard-failing the way bubblewrap did. Not treated as a real
# security boundary either way: this container is already the boundary that
# matters, and a compromised session already has full `node`-level bash
# access, which a Chromium exploit couldn't exceed regardless of whichever
# of its own internal sandbox layers happen to be active.
RUN npm install -g playwright@${PLAYWRIGHT_VERSION} && \
npx playwright install chromium
# Copy and set up firewall + DNS-proxy + entrypoint scripts, the gitea-pr
# helper from this repo's bin/ directory, the claude wrapper that pins
# --dangerously-skip-permissions, and this user's real nvim config.
COPY docker/claude-sandbox/fixed-domains.sh /usr/local/bin/
COPY docker/claude-sandbox/dns-proxy.sh /usr/local/bin/
COPY docker/claude-sandbox/init-firewall.sh /usr/local/bin/
COPY docker/claude-sandbox/log-shipper.sh /usr/local/bin/
COPY docker/claude-sandbox/ulogd.conf /etc/ulogd-sandbox.conf
COPY docker/claude-sandbox/entrypoint.sh /usr/local/bin/
COPY docker/claude-sandbox/claude-wrapper.sh /usr/local/bin/claude
COPY bin/gitea-pr /usr/local/bin/gitea-pr
COPY config/nvim $SANDBOX_HOME/.config/nvim
USER root
RUN chmod +x /usr/local/bin/dns-proxy.sh /usr/local/bin/init-firewall.sh /usr/local/bin/log-shipper.sh /usr/local/bin/entrypoint.sh /usr/local/bin/gitea-pr /usr/local/bin/claude && \
chown -R $SANDBOX_USER:$SANDBOX_USER $SANDBOX_HOME/.config/nvim && \
echo 'export PATH="$PATH:/usr/local/share/npm-global/bin"' > /etc/profile.d/npm-global-path.sh
# Entrypoint runs as root (no USER node here) so it can set up the firewall
# directly, with no sudo/setuid escalation path available to node at any
# point afterward. It drops to node itself via gosu before execing nvim.
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
-516
View File
@@ -1,516 +0,0 @@
# claude-sandbox
Runs Claude Code inside a Docker container with `--dangerously-skip-permissions`
(no permission prompts, all tool use auto-accepted) against a project mounted
from the host. Based on
[Anthropic's reference dev container](https://github.com/anthropics/claude-code/tree/main/.devcontainer):
same base image, non-root user, and egress-firewall approach, adapted to run
via `docker compose` directly instead of through a VS Code dev container, and
extended with this user's own nvim config and a `gitea-pr` helper for
opening/iterating on PRs.
This directory (`docker/claude-sandbox/`) only holds the tooling. It is
**not** where any project lives — the project you point it at is bind-mounted
into the container at runtime from wherever it actually sits on disk.
## Prerequisites
- Docker with the `docker compose` v2 plugin.
- `ssh-agent` running with the key for your Gitea remote(s) added
(`ssh-add -l` should list it). The container never sees your private key,
only a forwarded agent socket. `bin/claude-sandbox` warns (non-fatally) at
launch if the agent has zero identities loaded - the socket still forwards
fine either way, but git-over-ssh will fail until you `ssh-add` a key. That
takes effect immediately in an already-running session too (it's a live
reference to the same host agent process, not a snapshot) - no need to
restart the sandbox after `ssh-add`ing.
For a sandbox that needs to keep working unattended after your client
disconnects, see "Headless git push without agent forwarding" below —
`SANDBOX_SSH_KEY` is an alternative to this forwarded-agent requirement,
not just an addition to it.
- `GITEA_SSH_HOST` and `GITEA_API` exported in your shell (same variables
`bin/gitea-pr` already needs — see its `require_config`), and a token
available either via `GITEA_TOKEN` in the environment or at
`~/.config/gitea/token`.
- Optional: a **read-only** fine-grained GitHub PAT (GitHub → Settings →
Developer settings → Fine-grained tokens → generate, with Contents and
Metadata set to read-only, no write scopes) for `gh`/private-repo HTTPS
access, available via `GH_TOKEN` in the environment or at
`~/.config/gh/sandbox-token`. Don't use a normal `gh auth login` session
here — that's write-capable, which defeats the point (see "GitHub is
read-only" below). Without it, public GitHub repos still clone/fetch fine
anonymously over HTTPS; only private repos and `gh` API calls need it.
- Optional: `LOKI_URL` (a base URL, e.g. `http://loki.internal:3100`) to ship
DNS-query and firewall allow/reject logs to a Loki push endpoint - see
"Network request logging" below. Logging is skipped entirely if unset.
## Usage
```sh
bin/claude-sandbox # sandbox the current directory
bin/claude-sandbox ~/repos/some-project
bin/claude-sandbox ~/repos/some-project src/foo.py # open a specific file
```
The entry point is nvim, opened on the bind-mounted project directory, using
this user's real nvim config (`config/nvim`, including
[claudecode.nvim](https://github.com/coder/claudecode.nvim) — `<leader>ac`
toggles a Claude Code terminal). `claude` is also on `PATH` directly for use
from any shell/terminal inside the container. Either way it always runs with
`--dangerously-skip-permissions`, regardless of how it's invoked.
Typical loop: open the project, ask Claude to implement something and open a
PR (it has `gitea-pr` on `PATH`); from the host, `gitea-pr comments <PR#>` to
pull review feedback, feed it back in, repeat.
## Config parity with your host Claude Code setup
`~/.claude` and `~/.claude.json` are bind-mounted read-write from the host
into the container, so global `CLAUDE.md`, settings, hooks, plugins, MCP
server config, and your existing login/credentials all carry over
automatically — the container is a faithful copy of this setup, not a
separate identity. There's no separate first-run login.
The project directory is mounted at the *same absolute path* inside the
container as on the host (not a fixed `/workspace`), so Claude Code's
project/session-history keying lines up between host and container. A
session started in the sandbox can be resumed on the host afterward with
`claude --resume <session-id>`.
The container's own user is built to match yours exactly — same username,
uid, gid, and home directory (`bin/claude-sandbox` passes these as Docker
build args; see `Dockerfile`). This is what makes `$HOME`-relative
assumptions actually work inside the container instead of just superficially
resembling your setup — your dotfiles, and Claude Code's own plugin
bookkeeping (which records install locations as literal host-absolute
paths, not relative to whatever `$HOME` happens to be), resolve to the same
paths on both sides. The build defaults to `node`/1000/1000/`/home/node` if
these aren't supplied, so building the image directly (bypassing
`bin/claude-sandbox`) still works, just without this parity. On a shared
machine, the built image is tagged per-user (`claude-sandbox:<username>`) so
two accounts building concurrently don't clobber each other's image.
Caveat: MCP servers or hooks that shell out to host-specific binaries (e.g. a
browser for Playwright, a language runtime not installed in this image) may
not work inside the container unless that tooling is also added to the
`Dockerfile`.
Your global `~/.gitconfig` (`user.name`/`user.email` and the like) is also
carried over, so commits made inside the sandbox have the right identity
without any extra setup - falls back to an empty config if you don't have
one, it's not a hard requirement. It's a throwaway copy rather than the
real file, in its own directory rather than mounted directly at
`~/.gitconfig` (`GIT_CONFIG_GLOBAL` points git at it instead) - `gh auth
setup-git` needs to write a credential-helper entry into it, and both a
read-only mount and a single-file bind mount at `~/.gitconfig` break that
write in different ways. Nothing written to it during a session reaches
your real `~/.gitconfig`.
### Headless git push without agent forwarding
Agent forwarding (the default — see Prerequisites) ties the sandbox's SSH
access to whichever client's agent is currently forwarded. On a devbox
reached over SSH+tmux, that agent belongs to whichever client connected —
when that client disconnects, the forwarded socket dies with it, breaking
git-over-ssh for anything still running headless in the sandbox.
For sandboxes meant to keep running unattended, generate a dedicated,
purpose-built keypair instead — not your personal key:
```sh
ssh-keygen -t ed25519 -N '' -f ~/.ssh/claude-sandbox_ed25519
```
Passphrase-less is deliberate: headless operation has no human to answer a
prompt, and this is meant to be a low-privilege credential that only ever
lives inside the container, not your personal identity. Add the public
half as a Gitea deploy key scoped to only the repos that actually need push
access, then:
```sh
SANDBOX_SSH_KEY=~/.ssh/claude-sandbox_ed25519 bin/claude-sandbox ~/repos/some-project
```
The key is bind-mounted read-only at `/root/.ssh-sandbox-key-src` — under
`/root`, not `$SANDBOX_HOME` — so only entrypoint.sh's root-run startup code
can ever read it; the sandbox user (which the session actually runs as) has
no path to that directory at all. At container start, `entrypoint.sh` loads
it into a fresh `ssh-agent` running inside the container itself —
independent of the forwarded socket — copies it out just long enough to do
that, and deletes the copy immediately afterward. A compromised session can
use the agent's socket to sign git-over-ssh operations (bounded by whatever
the deploy key has push access to), but can't read the raw key material
itself. When `SANDBOX_SSH_KEY` is set, a live forwarded agent is no longer
required to start the sandbox at all.
## Shared scratch space
`~/shared` inside the container is a read-write bind mount of a real host
directory (`$SANDBOX_SHARE_DIR`, default `~/.local/share/claude-sandbox/shared`,
created automatically) - separate from the project mount, for passing files
(screenshots, temp scripts) back and forth with a sandboxed session without
touching the actual project checkout. It persists across sessions since it's
a real directory, not a container volume. Set `SANDBOX_SHARE_DIR` to point
it somewhere else.
`~/specs` is also bind-mounted read-write, at the fixed host path `~/specs`
(created automatically if it doesn't exist yet) - not configurable like
`SANDBOX_SHARE_DIR`, since it exists to make the global `CLAUDE.md`
convention of writing specs to `~/specs/` (also carried into the container)
actually work there instead of writing into a path that vanishes with the
session.
## Testing frontend changes
Playwright (Chromium) is pre-installed, browser included - `require
("playwright")` works from any script, anywhere in the container, not just
a project with its own `npm install playwright`. Both the npm package and
the matching Chromium build are baked in at image build time, so a session
never needs network access for either.
Testing against a project's own local dev server needs no firewall changes
at all - `localhost`/loopback traffic never goes through the egress
firewall in the first place. Testing against a real external site works
the same as any other network use: the domain needs to be on the allowlist
(`ALLOWED_DOMAINS`, or `bin/claude-sandbox-allow <domain>` for a running
session) - verified the firewall applies to the browser's own traffic
exactly like everything else (a non-allowlisted domain gets blocked from
inside the browser too, not just from `curl`/`git`).
If a project has its *own* pinned `playwright`/`@playwright/test`
devDependency at a different version than the one baked in here, its
`npm install` may still try to download a matching Chromium build from
Playwright's own CDN, which isn't on the default allowlist - allow it the
same way as any other one-off domain need if that comes up.
Chromium's own internal process sandbox is best-effort here, not something
to rely on: it launches without erroring, but its setuid sandbox helper
isn't actually setuid in this image, and this host has the same
unprivileged-userns restriction that keeps Claude Code's own internal
sandboxing (see Security notes) from working either - so its namespace-
based isolation almost certainly isn't active, Chromium just doesn't
hard-fail the way `bubblewrap` does when it's missing. Not treated as a
real boundary: this container is already the boundary that matters, and a
compromised session already has full bash access as the container's own
user regardless, which a Chromium exploit couldn't exceed.
## Sandbox self-awareness
A `SessionStart` hook (`~/.claude/hooks/sandbox-context.sh`, registered in
`~/.claude/settings.json`) checks for the `CLAUDE_SANDBOX` environment
variable this container sets, and when present, injects a reminder that the
session is running in a disposable, firewalled container and should stay
within the task at hand — and that if a task genuinely needs to break out of
the sandbox (broader network access, host-level changes, GUI/hardware), it
should stop and tell the user rather than trying to work around the
firewall, since the same session can be resumed unrestricted on the host via
`claude --resume`. It's a no-op outside the container. This is on top of the
existing `deny-dangerous-bash.sh` `PreToolUse` hook, which also applies here
since `~/.claude` is shared.
`git-push-guard.sh` (also `PreToolUse`/`Bash`, also `CLAUDE_SANDBOX`-gated)
blocks any `git push` whose resolved remote isn't the Gitea host, and any
push to `main`/`master` even there — the sandbox workflow is feature branch
+ `gitea-pr create`, never a direct push. Like the other hooks, this is a
fast-fail guard against an overeager agent, not a hard security boundary: a
sandbox session has full shell access and could in principle edit
`.git/config` to route around it. For "never pushes to GitHub" specifically,
the firewall backs this up with real enforcement (see below); for "never
pushes to main/master", the authoritative version of that is branch
protection on the Gitea repo itself, which this doesn't set up for you.
sandbox-context.sh also tells the agent the actual workflow to follow
proactively, not just what's blocked: do new work in a `git worktree`
rather than switching branches in the mounted checkout in place (that's the
user's real working copy), then push the feature branch and run
`gitea-pr create` from the worktree. The worktree has to live *under* the
mounted project directory (e.g. `.worktrees/<branch>`, not `../<branch>`) -
only the exact bind-mounted path is backed by real host storage, so a
worktree created as a sibling of the project (or anywhere under `$HOME`)
looks fine but silently disappears with the container, worktree and all.
## Network egress firewall
On start, the container runs an allowlist-only `iptables`/`ipset` firewall
(`init-firewall.sh`), matching the reference dev container. Allowed by
default: DNS resolution via a local DNS proxy (see "DNS is allowlisted too"
below), the Docker host itself (not its whole subnet), GitHub's published IP
ranges (port 443 only - see "GitHub is read-only" below),
`release-assets.githubusercontent.com` (GitHub release assets - e.g. mason's
own registry index - redirect here, a separate CDN from github.com's own
IPs), `registry.npmjs.org`, `pypi.org` and `files.pythonhosted.org` (mason's
Python-based LSP tools - `ruff`, `basedpyright`, `debugpy` - install via
pip), `proxy.golang.org` and `sum.golang.org` (`go build`/`go mod download`'s
module proxy and checksum database), `api.anthropic.com`, and whatever hosts
`GITEA_SSH_HOST`/`GITEA_API`
resolve to — that last group isn't port-restricted, so git-over-ssh to Gitea
works without a separate blanket "port 22 to anywhere" rule. Set
`ALLOWED_DOMAINS` (space-separated) in the environment to allow more (e.g.
`crates.io` for a Rust project needing more than what mason/npm already
cover). Everything else is rejected. Set `ENABLE_FIREWALL=0` to disable it
entirely (e.g. for debugging a blocked domain).
IPv6 is blocked outright (`ip6tables` policy DROP on INPUT/OUTPUT/FORWARD,
and dnsmasq strips AAAA answers so nothing wastes a connection attempt on an
address that could never be reached) - there's no IPv6 equivalent of the
allowlist above, so leaving it open would bypass the entire firewall on any
host with IPv6 egress enabled.
If your Gitea host is only resolvable via a LAN-local DNS server, make sure
the Docker host itself can resolve it — containers inherit the host's
resolver by default.
### DNS is allowlisted too
Resolution itself is restricted, not just the IP connections made
afterward: `dns-proxy.sh` runs a local `dnsmasq` bound to 127.0.0.1 with no
fallback resolver, and `/etc/resolv.conf` is repointed at it. Only names
matching the same allowlist as above (plus `github.com`/
`githubusercontent.com`/`githubassets.com`, needed since GitHub is
allowlisted by IP range rather than by resolving those names - see
`fixed-domains.sh`) get forwarded to Docker's real resolver; anything else
gets no upstream configured at all and is refused outright. Without this, a
compromised session could still exfiltrate data by encoding it into query
names to an attacker-controlled domain even though it could never open a
direct connection there - IP-based filtering alone doesn't stop that, since
the DNS query itself was never restricted.
### Adding a domain to a running session
`ALLOWED_DOMAINS` only takes effect at container start. For a domain a
*running* session needs right now, run from the host (not inside the
sandbox — it has no privilege to do this itself):
```sh
bin/claude-sandbox-allow crates.io # this session only
bin/claude-sandbox-allow crates.io --global # + persist in config/profile
```
It first adds the domain to the running dnsmasq's allowlist (appends a
`server=` line to its servers-file and sends `SIGHUP` to reload just that
file - no restart), then resolves it through the now-unblocked container
resolver and adds the IP(s) to the running firewall's `ipset`, all via
`docker exec -u root`. `--global` also appends the domain to
`ALLOWED_DOMAINS` in `config/profile` for future sessions; source your shell
(or open a new one) for that part to take effect. If more than one
claude-sandbox container is running, pass `--container <name>` to
disambiguate (`docker ps` to find it).
The sandbox-context.sh `SessionStart` hook tells the agent to ask for this
by name when a task is blocked on a single missing domain, rather than
stopping the whole session over it.
## Network request logging
If `LOKI_URL` is set (see Prerequisites), every DNS query and every new
outbound connection attempt - allowed or rejected - is shipped to that Loki
endpoint. Two sources, both tailed and pushed by `log-shipper.sh`:
- `dns-proxy.sh`'s dnsmasq logs every query and whether it was forwarded
(allowed) or refused (not on the allowlist) - labeled `job=claude-sandbox-dns`.
- `init-firewall.sh` tags its terminal ACCEPT/REJECT rules with `NFLOG`
(group 1 for allowed, group 2 for rejected), which `ulogd2`
(`ulogd.conf`) turns into JSON lines with source/dest IP, port, and an
`action` field - labeled `job=claude-sandbox-net`. Only new connection
attempts are logged, not every packet: the firewall's existing
ESTABLISHED,RELATED rule short-circuits the rest of a connection before it
ever reaches the tagged rules.
This is visibility on top of the firewall/DNS-proxy enforcement above, not a
substitute for it - nothing about logging changes what's actually allowed to
resolve or connect.
### Viewing logs in Grafana
Nothing sandbox-specific needed on the Grafana side beyond a Loki data
source pointed at wherever `LOKI_URL` resolves to (Connections → Data
sources → Loki, if one isn't already configured against that instance) -
`log-shipper.sh` pushes directly to Loki's HTTP API, there's no Prometheus
scrape target or exporter involved.
In Grafana's Explore view (or a dashboard panel with a Loki query), both
streams are selectable by their `job` label:
```logql
{job="claude-sandbox-dns"} # every DNS query + verdict
{job="claude-sandbox-net"} # every connection attempt, as JSON
```
Useful filters on top of those:
```logql
# DNS lookups that got refused (not on the allowlist)
{job="claude-sandbox-dns"} |= "REFUSED"
# Rejected connections only (parses ulogd2's JSON, filters on its action field)
{job="claude-sandbox-net"} | json | action="blocked"
# Both streams together, e.g. to watch a live session across sources
{source="claude-sandbox"}
```
`{job="claude-sandbox-net"}` lines are one full ulogd2 JSON object per
connection attempt (`| json` in a query unpacks fields like `dest_ip`,
`dest_port`, `action`) - Loki's Explore view shows these expandable/
pretty-printed by default, no extra parsing setup needed.
## GitHub is read-only
The sandbox can read from GitHub (clone, fetch, `gh` API calls) but can
never push there, and this is enforced at the network layer, not just by the
`git-push-guard.sh` hook above: GitHub's IP ranges are only allowlisted on
port 443. Port 22 is unreachable, so SSH-based push (which could otherwise
use the forwarded agent's real key - agent forwarding doesn't distinguish
"push" from "pull") has no transport to use at all, regardless of what any
hook does or doesn't catch. HTTPS access itself has no write-capable
credential either: `gh auth setup-git` (run at container start if `GH_TOKEN`
is set - see Prerequisites) points git's github.com credential helper at
`gh`, which only ever holds whatever token `GH_TOKEN` provides. Use a
read-only PAT there and even a push attempt over HTTPS gets rejected by
GitHub itself (403), not just blocked locally.
## Security notes
**Threat model, honestly stated:** this reduces the blast radius of a
misbehaving or prompt-injected coding agent - it does not make it safe to
point at arbitrary untrusted or adversarial input, and it does not turn
`--dangerously-skip-permissions` into something without real risk. Point it
at repositories you already trust. Everything below is about how bad a
compromised session *inside* that trust boundary can get, not a claim that
one can't happen.
- `--dangerously-skip-permissions` removes your chance to review tool calls
before they run. Only point this at trusted repositories.
- The mounted project itself has **no write protection**. `deny-dangerous-
bash.sh` and `git-push-guard.sh` are regex pattern-matching on the literal
Bash command string - a fast-fail guard against an overeager agent doing
something obviously destructive, not a boundary against one that's
actively trying to get around them (a different scripting language, an
encoded command, editing `.git/config` directly all sail right through).
Nothing here stops a compromised or badly-instructed session from
modifying or deleting the real working copy it's mounted against.
- Allowing egress to a domain at all creates a channel for exfiltrating data
through *that service's own request surface* (query strings, lookups,
etc.), not just to a literal attacker-controlled destination. Domain-level
allowlisting closes off reaching somewhere new; it can't inspect or
restrict what gets sent to somewhere already trusted for normal operation
(installing packages, calling the model API). That's a structural limit of
allowlist-only firewalls in general, not something specific to this setup,
and nothing here addresses it.
- This is container-level isolation (Linux namespaces plus a small
capability set), not VM-level isolation - no gVisor/Firecracker or
similar. It shares the host kernel, so a container-escape vulnerability
would defeat every other control described here at once.
- Everything in this document has been verified by hand, at a point in
time - there's no automated test suite re-checking these properties on
every change, so a future edit could silently reintroduce a gap. This has
already happened once in this file's own history (a missing `ipset`
dedup flag broke firewall startup entirely, twice, on two different
ipsets, before being caught).
- Because `~/.claude` (including `.credentials.json`) is bind-mounted in,
anything reachable from inside the container (the allowed domains above)
is a potential exfiltration path for those credentials if a malicious repo
manages to manipulate a session. The firewall limits this to
Anthropic/npm/GitHub/your Gitea host, not the open internet.
- `settings.json`, `CLAUDE.md`, and `plugins/` under `~/.claude` are
layered back read-only on top of the (otherwise read-write) `~/.claude`
mount. Hooks execute automatically with no separate approval step, so
without this a compromised session could plant a backdoored hook, rewrite
`CLAUDE.md` with injected instructions, or tamper with plugin code -
something that would then run **unsandboxed on the host** the next time
Claude Code starts there, turning a single compromised sandbox session
into persistent host compromise. Everything else under `~/.claude`
(session/project history, credentials refresh, caches) stays read-write,
since it's runtime state Claude Code needs to update.
`~/.claude.json` is a single file mixing that same kind of live state with
config (e.g. `mcpServers`, if ever set) and can't be split at the mount
level, so it's left fully read-write - accepted as a residual risk rather
than solved.
- DNS resolution itself is allowlisted (see "DNS is allowlisted too" above),
not just the IP connections made afterward, closing off exfiltration via
encoding data into query names to an attacker-controlled domain - verified
a non-allowlisted domain gets refused outright (`status: REFUSED`) rather
than resolving and then only failing to connect.
- Don't mount other host secrets (`~/.ssh` private keys, cloud credential
files) into the container. Git push auth goes through the forwarded SSH
agent instead, which never exposes the key material itself - though note
the agent will sign for *any* Gitea repo your key has access to, not just
the mounted project, and the firewall's Gitea allowlist is host-based
rather than repo-scoped, so a compromised session can still reach (and
push to) other repos on that host you have access to. This doesn't extend
to GitHub: port 22 there is blocked outright, so the forwarded agent has
no transport to use for it at all (see "GitHub is read-only" above).
- `SANDBOX_SSH_KEY` (see "Headless git push without agent forwarding") is a
second, independent credential-loading path with the same "signs for any
Gitea repo it has access to" shape as the forwarded agent above - the
mitigation is that it's meant to be a dedicated deploy key scoped to only
the repos that actually need push access, not your personal identity.
That scoping is a convention, not something enforced here: nothing checks
what `SANDBOX_SSH_KEY` actually points at, so pointing it at your real
`~/.ssh/id_ed25519`/`id_rsa` instead of a dedicated key would load your
full personal identity into the container for the whole session (`bin/
claude-sandbox` warns if this looks like it might be happening - see
below - but doesn't block it). The key source itself is mounted read-only
at `/root/...`, not under the sandbox user's own home, specifically so
that user (which the session actually runs as) has no path to read the
raw key material - only the agent socket it's loaded into.
- The sandbox's own user (matching your host user — see "Config parity"
above) has no `sudo`/setuid path back to root once the container is up:
`entrypoint.sh` runs as root, sets up the firewall directly, then drops to
that user via `gosu` before execing `nvim`. A compromised session can't
re-run `init-firewall.sh` with a wider allowlist to open its own
exfiltration path. Verified: capabilities (`NET_ADMIN`/`NET_RAW`) are
cleared from the effective/permitted sets on the `gosu` drop, and that
user gets a permission error running `iptables` directly.
## Tests
`test/` has a [bats](https://github.com/bats-core/bats-core) suite covering
the security invariants above - not a substitute for reading this document,
but a way to actually catch a future change silently breaking one of them
instead of finding out the hard way:
```sh
bats docker/claude-sandbox/test/ # everything
bats docker/claude-sandbox/test/static.bats # fast, no Docker/build needed
SKIP_BUILD=1 bats docker/claude-sandbox/test/runtime.bats # reuse the current image
```
`static.bats` greps the repo's own files - no sudo in the `Dockerfile`, every
`ipset add` is duplicate-safe, no `docker.sock`/host-credential mounts, the
`~/.claude` read-only overlay lines are present, `bin/claude-sandbox-allow`
rejects a malicious domain argument. `runtime.bats` builds the image, boots
a real container running the actual `entrypoint.sh` (a `SANDBOX_TEST_MODE`
env var makes it stop just short of execing `nvim`, so the suite can attach
to a fully-initialized container instead of reimplementing its own copy of
the setup sequence), and asserts against it: IPv6 blocked, DNS/IP allowlist
enforcement in both directions, GitHub read-only, no working `sudo`/
`iptables`/`init-firewall.sh` as `node`, capabilities actually stripped
(not just absent from a `docker exec` that doesn't have them to begin
with), `ulogd2` unprivileged, and the dynamic-ipset CDN-rotation fix.
Every mutation in `runtime.bats` (firewall rules, `ipset` entries) happens
inside the test container's own network namespace, same as any other
container - none of it touches the host. Container/network names are
derived from bats' own per-run temp directory, not anything guessable or
likely to collide with a real container you have running.
## Files
| File | Purpose |
| --- | --- |
| `Dockerfile` | Base image, dev tools, nvim, Claude Code + wrapper, `gitea-pr` |
| `fixed-domains.sh` | Shared domain list sourced by both `init-firewall.sh` and `dns-proxy.sh` |
| `dns-proxy.sh` | Starts the DNS-allowlisting `dnsmasq`, repoints `resolv.conf` at it |
| `init-firewall.sh` | IPv4/IPv6 egress allowlist firewall, run at container start |
| `ulogd.conf` | `ulogd2` config: turns `init-firewall.sh`'s NFLOG-tagged packets into JSON |
| `log-shipper.sh` | Tails the dnsmasq/ulogd2 logs and ships them to `$LOKI_URL` |
| `entrypoint.sh` | Runs as root: DNS proxy, firewall, logging, drops to `node` via `gosu`, execs `nvim` |
| `claude-wrapper.sh` | Shadows `claude` on `PATH` to always add `--dangerously-skip-permissions` |
| `docker-compose.yml` | Wires up mounts/env; invoked via `bin/claude-sandbox`, not directly |
`bin/claude-sandbox` (repo root `bin/`) is the entry point — it resolves the
project path, checks for a live `ssh-agent`, loads `GITEA_TOKEN`/`GH_TOKEN`
if not already set, and runs `docker compose run`. `bin/claude-sandbox-allow`
adds a domain to a running session's firewall from the host - see "Adding a
domain to a running session" above.
-6
View File
@@ -1,6 +0,0 @@
#!/bin/bash
# Shadows the real `claude` binary (installed under npm-global/bin, later on
# PATH) so every invocation - interactive shell, or claudecode.nvim's
# terminal - runs with --dangerously-skip-permissions, without callers
# needing to know that.
exec /usr/local/share/npm-global/bin/claude --dangerously-skip-permissions "$@"

Some files were not shown because too many files have changed in this diff Show More