diff --git a/config/shared/addpaths b/config/shared/addpaths index f9325a5..49fe484 100755 --- a/config/shared/addpaths +++ b/config/shared/addpaths @@ -3,13 +3,25 @@ set -euo pipefail usage() { - echo "Usage: $(basename "$0") -c COMMENT_SYMBOL [-e EXCLUDE_PATTERN]... TARGET" - echo " -c Comment symbol (e.g., '#' or '//')" + echo "Usage: $(basename "$0") [options] TARGET" + echo "" + echo "Commands:" + echo " add Add/update path comment in first 5 lines of each file" + echo " undo Remove any path comment from first 5 lines of each file" + echo "" + echo "Options:" + echo " -c Comment symbol (e.g., '#' or '//') [required for add]" echo " -e Exclude pattern (can be specified multiple times)" - echo " TARGET File or directory to process" exit 1 } +[[ $# -lt 1 ]] && usage + +cmd="$1" +shift + +[[ "$cmd" != "add" && "$cmd" != "undo" ]] && usage + comment_sym="" excludes=() @@ -24,66 +36,63 @@ done shift $((OPTIND - 1)) [[ $# -ne 1 ]] && usage -[[ -z "$comment_sym" ]] && usage + +if [[ "$cmd" == "add" ]]; then + [[ -z "$comment_sym" ]] && { + echo "Error: -c is required for add" >&2 + usage + } +fi target="$(realpath "$1")" base_dir="$(pwd)" +strip_path_comment() { + local file="$1" + awk 'NR <= 5 && /path: / { next } { print }' "$file" >"$file.tmp" + mv "$file.tmp" "$file" +} + process_file() { local file="$1" # shellcheck disable=SC2295 local rel_path="${file#$base_dir/}" - local path_comment="$comment_sym path: $rel_path" - # Read first two lines - local line1 line2 - IFS= read -r line1 <"$file" 2>/dev/null || line1="" - IFS= read -r line2 < <(tail -n +2 "$file") 2>/dev/null || line2="" + strip_path_comment "$file" + + local line1 + IFS= read -r line1 <"$file" 2>/dev/null || line1="" - # Handle shebang case if [[ "$line1" =~ ^#! ]]; then - if [[ "$line2" == *"path: "* ]]; then - # Replace existing path comment on line 2 - { - echo "$line1" - echo "$path_comment" - tail -n +3 "$file" - } >"$file.tmp" - else - # Insert new path comment after shebang - { - echo "$line1" - echo "$path_comment" - tail -n +2 "$file" - } >"$file.tmp" - fi + { + echo "$line1" + echo "$path_comment" + tail -n +2 "$file" + } >"$file.tmp" else - if [[ "$line1" == *"path: "* ]]; then - # Replace existing path comment on line 1 - { - echo "$path_comment" - tail -n +2 "$file" - } >"$file.tmp" - else - # Insert new path comment at top - { - echo "$path_comment" - cat "$file" - } >"$file.tmp" - fi + { + echo "$path_comment" + cat "$file" + } >"$file.tmp" fi mv "$file.tmp" "$file" } -if [[ -f "$target" ]]; then - process_file "$target" -elif [[ -d "$target" ]]; then - find_cmd=(find "$target") +dispatch() { + local file="$1" + if [[ "$cmd" == "add" ]]; then + process_file "$file" + else + strip_path_comment "$file" + fi +} - # Always exclude hidden files and directories - find_cmd+=(\( -name ".*" -prune \)) +if [[ -f "$target" ]]; then + dispatch "$target" +elif [[ -d "$target" ]]; then + find_cmd=(find "$target" \( -name ".*" -prune \)) if [[ ${#excludes[@]} -gt 0 ]]; then find_cmd+=(-o \() @@ -98,7 +107,7 @@ elif [[ -d "$target" ]]; then find_cmd+=(-o -type f -print0) while IFS= read -r -d '' file; do - process_file "$file" + dispatch "$file" done < <("${find_cmd[@]}") else echo "Error: $target is not a file or directory" >&2 diff --git a/config/shared/bin/catfiles b/config/shared/bin/catfiles new file mode 100755 index 0000000..81b69ea --- /dev/null +++ b/config/shared/bin/catfiles @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="." +declare -a SKIP_DIRS=() +declare -a PATTERNS=() + +usage() { + cat < 0 )) || usage + +# Build prune expression +PRUNE_EXPR=() +if (( ${#SKIP_DIRS[@]} > 0 )); then + PRUNE_EXPR+=( \( ) + for i in "${!SKIP_DIRS[@]}"; do + PRUNE_EXPR+=( -type d -name "${SKIP_DIRS[$i]}" -prune ) + [[ $i -lt $((${#SKIP_DIRS[@]} - 1)) ]] && PRUNE_EXPR+=( -o ) + done + PRUNE_EXPR+=( \) -o ) +fi + +# Build pattern expression +PATTERN_EXPR=( \( ) +for i in "${!PATTERNS[@]}"; do + PATTERN_EXPR+=( -name "${PATTERNS[$i]}" ) + [[ $i -lt $((${#PATTERNS[@]} - 1)) ]] && PATTERN_EXPR+=( -o ) +done +PATTERN_EXPR+=( \) ) + +# Execute +find "$ROOT" \ + "${PRUNE_EXPR[@]}" \ + -type f \ + "${PATTERN_EXPR[@]}" \ + -print0 | +while IFS= read -r -d '' file; do + printf "\n===== %s =====\n" "$file" + cat "$file" +done + diff --git a/config/shared/bin/wt b/config/shared/bin/wt new file mode 100755 index 0000000..619abd8 --- /dev/null +++ b/config/shared/bin/wt @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +set -euo pipefail + +######################################## +# Utilities +######################################## + +fail() { + echo "Error: $1" >&2 + exit 1 +} + +ensure_repo() { + git rev-parse --is-inside-work-tree >/dev/null 2>&1 || + fail "Not inside a git repository" +} + +repo_root() { + git rev-parse --show-toplevel +} + +detect_base() { + if git show-ref --verify --quiet refs/heads/main; then + echo "main" + elif git show-ref --verify --quiet refs/heads/master; then + echo "master" + else + git branch --show-current + fi +} + +ensure_clean() { + git diff --quiet || fail "Working tree is dirty" + git diff --cached --quiet || fail "Index is dirty" +} + +######################################## +# Core Paths +######################################## + +ROOT=$(repo_root) +WT_DIR="$ROOT/.worktrees" + +######################################## +# Commands +######################################## + +cmd_new() { + local name="$1" + local base="${2:-$(detect_base)}" + local path="$WT_DIR/$name" + + [ -d "$path" ] && fail "Worktree directory already exists" + git show-ref --verify --quiet "refs/heads/$name" && + fail "Branch '$name' already exists" + + mkdir -p "$WT_DIR" + + git worktree add -b "$name" "$path" "$base" + + echo "Created worktree:" + echo " branch: $name" + echo " path: $path" +} + +cmd_list() { + git worktree list +} + +cmd_path() { + echo "$WT_DIR/$1" +} + +cmd_rm() { + local name="$1" + local path="$WT_DIR/$name" + + [ -d "$path" ] || fail "Worktree not found" + + git worktree remove "$path" + git branch -D "$name" >/dev/null 2>&1 || true + + echo "Removed worktree '$name'" +} + +cmd_finish() { + local name="$1" + shift + local keep_worktree=0 + local path="$WT_DIR/$name" + local base + + while [[ $# -gt 0 ]]; do + case "$1" in + --keep-worktree) + keep_worktree=1 + shift + ;; + *) + fail "Unknown option for finish: $1" + ;; + esac + done + + [ -d "$path" ] || fail "Worktree not found" + git show-ref --verify --quiet "refs/heads/$name" || + fail "Branch '$name' not found" + + base=$(detect_base) + + echo "Base branch: $base" + echo "Feature branch: $name" + + git checkout "$base" >/dev/null + ensure_clean + + if ! git merge --squash "$name"; then + echo + echo "Resolve conflicts, then run:" + echo " git commit" + echo " wt rm $name" + exit 1 + fi + + git commit -m "$name" + + if [ -d "$path" ] && [ "$keep_worktree" -eq 0 ]; then + if ! git worktree remove "$path"; then + git worktree remove --force "$path" || fail "Failed to remove worktree '$name'" + fi + fi + + if [ "$keep_worktree" -eq 0 ]; then + git branch -D "$name" + fi + + if [ "$keep_worktree" -eq 1 ]; then + echo "Merged (squash) into '$base' and kept worktree '$path'" + else + echo "Merged (squash) and removed '$name'" + fi +} + +cmd_prune() { + git worktree prune + echo "Pruned stale worktree metadata" +} + +######################################## +# Entry +######################################## + +ensure_repo + +case "${1:-}" in +new) + [ $# -lt 2 ] && fail "Usage: wt new [base]" + cmd_new "$2" "${3:-}" + ;; +list) + cmd_list + ;; +path) + [ $# -lt 2 ] && fail "Usage: wt path " + cmd_path "$2" + ;; +finish) + [ $# -lt 2 ] && fail "Usage: wt finish " + cmd_finish "$2" "${@:3}" + ;; +rm) + [ $# -lt 2 ] && fail "Usage: wt rm " + cmd_rm "$2" + ;; +prune) + cmd_prune + ;; +*) + cat < [base] + wt list + wt path + wt finish [--keep-worktree] + wt rm + wt prune +EOF + exit 1 + ;; +esac diff --git a/config/shared/git b/config/shared/git index 8b31831..4292589 100644 --- a/config/shared/git +++ b/config/shared/git @@ -21,4 +21,3 @@ last = log -1 HEAD tags = tag -l undo = reset --mixed HEAD~1 - diff --git a/config/shared/gitignore b/config/shared/gitignore index ffe8af7..7ab056f 100644 --- a/config/shared/gitignore +++ b/config/shared/gitignore @@ -19,3 +19,6 @@ logs/ coverage/ .devflow/ .dev-flow/ +.flow/ +.local/ +.worktrees/ diff --git a/config/shared/nvim b/config/shared/nvim index 6b4008c..e65a7f3 160000 --- a/config/shared/nvim +++ b/config/shared/nvim @@ -1 +1 @@ -Subproject commit 6b4008ca3863be7fd468e841e87f0132b5727214 +Subproject commit e65a7f3f13861b705abce8007ed2b5754f5c34c1 diff --git a/config/shared/tmux b/config/shared/tmux index 414c1f0..eab8e64 100644 --- a/config/shared/tmux +++ b/config/shared/tmux @@ -7,6 +7,9 @@ bind C-Space send-prefix # set -s default-terminal "tmux-256color" # set -sa terminal-overrides "$term:rgb" +set -g window-size largest +set -g aggressive-resize on + set -s escape-time 10 set -s focus-events on set -s set-clipboard on @@ -70,7 +73,7 @@ bind v split-window -h -c "#{pane_current_path}" # For some reason, choose-session is what I had before and it was working; however, # it does not appear in man page and does not allow customization # bind o choose-session -bind o choose-tree -s -O name +bind o choose-tree -Zs -O name bind N new-window bind n new-window -c "#{pane_current_path}" bind c confirm-before -p "kill-pane \#P? (y/n)" kill-pane @@ -82,5 +85,3 @@ unbind d bind e detach bind d command-prompt -I "flow " 'run-shell "/home/tomas/bin/dev-tmux-wrapper.sh %1 --from #{session_name}"' - - diff --git a/config/shared/zsh b/config/shared/zsh index 67e2abc..5854e29 100644 --- a/config/shared/zsh +++ b/config/shared/zsh @@ -3,6 +3,8 @@ export LANG=en_US.UTF-8 export LC_CTYPE=en_US.UTF-8 export LC_COLLATE=C +export EDITOR=nvim + # eval "$(dircolors)"; echo "$LS_COLORS" export LS_COLORS='rs=0:di=01;34:ln=01;33:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=00:tw=30;42:ow=34;42:st=37;44:ex=01;32' @@ -13,6 +15,8 @@ SAVEHIST=10000 setopt auto_cd interactive_comments prompt_subst share_history setopt append_history hist_ignore_dups hist_ignore_all_dups hist_reduce_blanks +fpath=(~/.zsh/completions $fpath) + autoload -Uz compinit zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' # Case-insensitive zstyle ':completion:*' use-cache on @@ -76,10 +80,63 @@ alias ga='git add' alias gcm='git commit -m' alias gp='git push' alias gst='git status' -alias gd='git diff --patience --color-moved=dimmed-zebra --word-diff=plain --function-context --ignore-space-change -U3' +# alias gd='git diff --patience --color-moved=dimmed-zebra --word-diff=plain --function-context --ignore-space-change -U3' +alias gd='git diff --patience --color-moved=dimmed-zebra --ignore-space-change -U5' alias glg='git log --oneline --graph --decorate --all' alias k='kubectl' # opencode -export PATH=/home/tomas/.opencode/bin:$PATH +export PATH="$HOME/.opencode/bin:$PATH" + +git-safe-delete() { + local branch="$1" + local base="${2:-main}" + + if [[ -z "$branch" ]]; then + echo "Usage: git-safe-delete [base]" + return 1 + fi + + if [[ "$branch" == "$(git branch --show-current)" ]]; then + echo "Cannot delete current branch." + return 1 + fi + + git fetch origin >/dev/null 2>&1 + + if git diff --quiet "$base..$branch"; then + echo "No content differences with $base." + git branch -D "$branch" + echo "Deleted $branch" + else + echo "Branch differs from $base." + echo "Diff summary:" + git diff --stat "$base..$branch" + echo + read "confirm?Force delete anyway? (y/N): " + [[ "$confirm" == "y" ]] && git branch -D "$branch" + fi +} + +_git_safe_delete() { + _arguments \ + '1:local branch:($(git for-each-ref --format="%(refname:short)" refs/heads))' \ + '2:base branch (optional):($(git for-each-ref --format="%(refname:short)" refs/heads))' +} + +compdef _git_safe_delete git-safe-delete + + + +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion + +# pnpm +export PNPM_HOME="/home/tomas/.local/share/pnpm" +case ":$PATH:" in + *":$PNPM_HOME:"*) ;; + *) export PATH="$PNPM_HOME:$PATH" ;; +esac +# pnpm end