├── dnsmasq.conf ├── fishfile ├── gpg-agent.conf ├── github.unite.user-style.css ├── bin ├── git-unstash ├── git-grep ├── git-undo ├── git-amend ├── git-conflicts ├── git-contributors ├── git-abort ├── git-tree ├── git-delete ├── hiroshima ├── git-empty ├── git-rekt ├── git-ignore ├── git-restore ├── git-tail ├── git-yolo ├── checkout ├── git-publish ├── artisan ├── git-history ├── git-redate ├── git-tidy ├── killport ├── git-feature ├── git-refactor ├── git-obliterate ├── git-io ├── git-save ├── git-squash ├── edit ├── git-repl ├── git-shortcut ├── git-issue ├── npm ├── git-sus ├── git-sync └── brewdump ├── git └── hooks │ └── post-merge ├── README.md ├── Caddyfile ├── ssh └── config ├── gitconfig ├── gitignore ├── config.fish ├── Brewfile ├── zshrc └── twitter.unite.user-style.css /dnsmasq.conf: -------------------------------------------------------------------------------- 1 | address=/.local/127.0.0.1 2 | port=53 3 | -------------------------------------------------------------------------------- /fishfile: -------------------------------------------------------------------------------- 1 | oh-my-fish/theme-bobthefish 2 | jethrokuan/z 3 | -------------------------------------------------------------------------------- /gpg-agent.conf: -------------------------------------------------------------------------------- 1 | pinentry-program /opt/homebrew/bin/pinentry-mac 2 | -------------------------------------------------------------------------------- /github.unite.user-style.css: -------------------------------------------------------------------------------- 1 | header.Header, div.orghead { display: none } 2 | -------------------------------------------------------------------------------- /bin/git-unstash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | git stash pop; 5 | } 6 | 7 | main "$@" 8 | -------------------------------------------------------------------------------- /bin/git-grep: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | git ls-files | grep -i 5 | } 6 | 7 | main "$@" 8 | -------------------------------------------------------------------------------- /bin/git-undo: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | git reset --soft HEAD^; 5 | } 6 | 7 | main "$@" 8 | -------------------------------------------------------------------------------- /bin/git-amend: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | git commit -a --amend --no-edit 5 | } 6 | 7 | main "$@" 8 | -------------------------------------------------------------------------------- /bin/git-conflicts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | git diff --name-only --diff-filter=U 5 | } 6 | 7 | main "$@" 8 | -------------------------------------------------------------------------------- /bin/git-contributors: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | git shortlog --summary --numbered 5 | } 6 | 7 | main "$@" 8 | -------------------------------------------------------------------------------- /bin/git-abort: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | git reset --hard HEAD 5 | git clean -df 6 | } 7 | 8 | main "$@" 9 | -------------------------------------------------------------------------------- /bin/git-tree: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | git log --all --graph --decorate --oneline 5 | } 6 | 7 | main "$@" 8 | -------------------------------------------------------------------------------- /bin/git-delete: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | local file="${1}" 5 | git rm --cached "${1}"; 6 | } 7 | 8 | main "$@" 9 | -------------------------------------------------------------------------------- /bin/hiroshima: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | main () { 4 | docker rm -f $(docker ps -a -q) 5 | docker system prune -a -f 6 | } 7 | 8 | main "$@" 9 | -------------------------------------------------------------------------------- /bin/git-empty: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | local message=("${@}") 5 | git commit --allow-empty -m "${message}" 6 | } 7 | 8 | main "$@" 9 | -------------------------------------------------------------------------------- /bin/git-rekt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Alias to obliterate because sometimes I like to be cheeky when removing files from git histroy 4 | git obliterate "$@" 5 | -------------------------------------------------------------------------------- /bin/git-ignore: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | local file="${1}" 5 | git update-index --assume-unchanged "${file}"; 6 | } 7 | 8 | main "$@" 9 | -------------------------------------------------------------------------------- /bin/git-restore: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | local file="${1}" 5 | git checkout $(git rev-list -n 1 HEAD -- "${file}")^ -- "${file}"; 6 | } 7 | 8 | main "$@" 9 | -------------------------------------------------------------------------------- /bin/git-tail: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | git for-each-ref --sort=-committerdate --format='%(committerdate:relative)%09%(refname:short)' refs/heads 5 | } 6 | 7 | main "$@" 8 | -------------------------------------------------------------------------------- /bin/git-yolo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | main () { 4 | local message=$(curl -s http://whatthecommit.com/index.txt) 5 | 6 | git save "$message" && git push 7 | } 8 | 9 | main "$@" 10 | -------------------------------------------------------------------------------- /bin/checkout: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | if ([ "$#" -eq 0 ]); then 5 | git checkout - 6 | 7 | else 8 | git checkout $@ 9 | fi 10 | 11 | } 12 | 13 | main "$@" 14 | -------------------------------------------------------------------------------- /bin/git-publish: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | local remote="${1-origin}" 5 | local branch=$(git rev-parse --abbrev-ref HEAD) 6 | git push -u $remote $branch 7 | } 8 | 9 | main "$@" 10 | -------------------------------------------------------------------------------- /bin/artisan: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | local DIR=$(pwd) 5 | while [ ! -z "$DIR" ] && [ ! -f "$DIR/artisan" ]; do 6 | DIR="${DIR%\/*}" 7 | done 8 | exec php $DIR/artisan "$@" 9 | } 10 | 11 | main "$@" 12 | -------------------------------------------------------------------------------- /bin/git-history: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | git log --pretty=format:"%C(yellow)%h%C(reset) %C(green)%ad%C(reset) %C(red)|%C(reset) %s %C(bold blue)[%an]%C(reset)%C(yellow)%d%C(reset)" --graph --date=short 5 | } 6 | 7 | main "$@" 8 | -------------------------------------------------------------------------------- /git/hooks/post-merge: -------------------------------------------------------------------------------- 1 | #/usr/bin/env bash 2 | 3 | changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)" 4 | 5 | check_run() { 6 | echo "$changed_files" | grep --quiet "$1" && eval "$2" 7 | } 8 | 9 | check_run package.json "yarn install" 10 | -------------------------------------------------------------------------------- /bin/git-redate: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | local backdate=$(date -v "$1" "+%Y-%m-%dT%H:%M:%S") 5 | shift 6 | local args=("$@") 7 | 8 | GIT_AUTHOR_DATE="$backdate" GIT_COMMITTER_DATE="$backdate" git commit "${args[@]}" 9 | } 10 | 11 | main "$@" 12 | -------------------------------------------------------------------------------- /bin/git-tidy: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | git remote prune origin 2>/dev/null; 5 | echo "$(git branch -vv | grep origin | tr '[]*?+' ' ')" | egrep -v "$(git branch -r | awk '{print $1}')" | awk '{print $1}' | xargs git branch -D 2>/dev/null 6 | } 7 | 8 | main "$@" 9 | -------------------------------------------------------------------------------- /bin/killport: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function killport() { 4 | lsof -i TCP:$1 | grep LISTEN | awk '{print $2}' | xargs kill -9 5 | } 6 | 7 | main () { 8 | local args=("$@") 9 | 10 | case "$1" in 11 | *) killport "${args[@]}" ;; 12 | esac 13 | } 14 | 15 | main "$@" 16 | -------------------------------------------------------------------------------- /bin/git-feature: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | local string=$(echo "$*" | sed -e 's/[[:space:]]*$//' | iconv -t ascii//TRANSLIT | sed -E s/[^a-zA-Z0-9]+/-/g | sed -E s/^-+\|-+$//g | tr A-Z a-z) 5 | 6 | git checkout -b "feature/${string}" 7 | } 8 | 9 | main "$@" 10 | -------------------------------------------------------------------------------- /bin/git-refactor: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | local string=$(echo "$*" | sed -e 's/[[:space:]]*$//' | iconv -t ascii//TRANSLIT | sed -E s/[^a-zA-Z0-9]+/-/g | sed -E s/^-+\|-+$//g | tr A-Z a-z) 5 | 6 | git checkout -b "refactor/${string}" 7 | } 8 | 9 | main "$@" 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dotfiles 2 | 3 | These are my dotfiles. There are many like them, but these are mine. 4 | 5 | My dotfiles are my best friend. They are my life. I must master them as I master my life. 6 | 7 | My dotfiles, without me, are useless. Without my dotfiles, I am useless. I must use my dotfiles true. 8 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | primcloud.local, www.primcloud.local { 2 | reverse_proxy 0.0.0.0:63000 3 | } 4 | 5 | api.primcloud.local { 6 | reverse_proxy { 7 | to 0.0.0.0:63002 8 | header_up X-Forwarded-For 173.30.188.213 9 | header_up X-Forwarded-Host 173.30.188.213 10 | header_up X-Real-IP 173.30.188.213 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bin/git-obliterate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ## Source: https://raw.githubusercontent.com/tj/git-extras/master/bin/git-obliterate 3 | 4 | file="$1" 5 | test -z "$file" && echo "file required." 1>&2 && exit 1 6 | git filter-branch -f --index-filter "git rm -r --cached '$file' --ignore-unmatch" --prune-empty --tag-name-filter cat -- --all 7 | -------------------------------------------------------------------------------- /ssh/config: -------------------------------------------------------------------------------- 1 | # Added by OrbStack: 'orb' SSH host for Linux machines 2 | # This only works if it's at the top of ssh_config (before any Host blocks). 3 | # This won't be added again if you remove it. 4 | Include ~/.orbstack/ssh/config 5 | 6 | ServerAliveInterval 60 7 | 8 | Host * 9 | IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock" 10 | -------------------------------------------------------------------------------- /bin/git-io: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Usage: git io URL [CODE] 4 | # 5 | # Turns a github.com URL 6 | # into a git.io URL 7 | 8 | URL="$1" 9 | CODE="$2" 10 | 11 | SHORT_URL=$(curl -si https://git.io -F "url=${URL}" ${CODE:+-F "code=${CODE}"} | grep 'Location:' | sed 's/Location: //') 12 | 13 | echo "Copied URL to clipboard." 14 | echo "${SHORT_URL}" | pbcopy 15 | -------------------------------------------------------------------------------- /bin/git-save: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | local files=("${@}") 5 | local length=${#files[@]} 6 | local message=${files[$(($length - 1))]} 7 | unset files[${#files[@]}-1] 8 | 9 | if [ ${#files[@]} -eq 0 ]; then 10 | git add -A 11 | git commit -m "${message}" 12 | else 13 | git add ${files[@]} 14 | git commit -m "${message}" 15 | fi 16 | } 17 | 18 | main "$@" 19 | -------------------------------------------------------------------------------- /bin/git-squash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Compliments of Brandon Slinkard (https://github.com/slinkardbrandon) 4 | 5 | function main () { 6 | local message="${1}" 7 | local branch=$(git branch | grep \* | cut -d ' ' -f2); 8 | 9 | git sync; 10 | git checkout $branch; 11 | git reset --soft develop; 12 | git save $message; 13 | 14 | # git fetch; 15 | #   git checkout develop 16 | #   git pull; 17 | #   git checkout $GIT_BRANCH; 18 | #   git reset --soft develop; 19 | #   git add .; 20 | #   git commit -m $@; 21 | } 22 | 23 | main "$@" 24 | -------------------------------------------------------------------------------- /bin/edit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | dotfiles () { 4 | shift 5 | local args=("$@") 6 | "${EDITOR}" "${DOTFILES}/${args[@]}" 7 | } 8 | 9 | edit () { 10 | local args=("$@") 11 | 12 | if [ "$#" -gt "0" ]; then 13 | "${EDITOR}" "${args[@]}" 14 | else 15 | "${EDITOR}" . 16 | fi 17 | } 18 | 19 | main () { 20 | local args=("$@") 21 | 22 | case "$1" in 23 | dotfiles) dotfiles "${args[@]}" ;; 24 | caddy) vim /opt/homebrew/etc/sites-enabled/default ;; 25 | caddyfile) vim /opt/homebrew/etc/Caddyfile ;; 26 | *) edit "${args[@]}" ;; 27 | esac 28 | } 29 | 30 | main "$@" 31 | -------------------------------------------------------------------------------- /gitconfig: -------------------------------------------------------------------------------- 1 | [user] 2 | name = Josh Manders 3 | email = josh@joshmanders.com 4 | signingkey = E185112892584284 5 | [push] 6 | default = simple 7 | followTags = true 8 | [filter "media"] 9 | clean = git-media-clean %f 10 | smudge = git-media-smudge %f 11 | [core] 12 | editor = code --wait 13 | excludesfile = ~/.gitignore 14 | ignorecase = false 15 | [http] 16 | sslVerify = false 17 | [rerere] 18 | enabled = true 19 | [branch] 20 | autosetuprebase = always 21 | [help] 22 | autocorrect = 1 23 | [commit] 24 | gpgsign = true 25 | [diff] 26 | tool = icdiff 27 | [difftool] 28 | prompt = false 29 | [difftool "icdiff"] 30 | cmd = /usr/local/bin/icdiff --line-numbers $LOCAL $REMOTE 31 | [alias] 32 | ch = clubhouse 33 | [tag] 34 | gpgsign = true 35 | [github] 36 | api-token = ghp_XXXXXXXXXXXXXXXXXXXXXX 37 | [init] 38 | defaultBranch = master 39 | -------------------------------------------------------------------------------- /bin/git-repl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## Source: https://raw.githubusercontent.com/tj/git-extras/master/bin/git-repl 4 | 5 | 6 | echo "Type 'exit' or 'quit' to close repl." 7 | while true; do 8 | # Current branch 9 | branch=$(git symbolic-ref HEAD 2> /dev/null | cut -d/ -f3-) 10 | 11 | # Prompt 12 | if test -n "${branch}"; then 13 | prompt="git (${branch})> " 14 | else 15 | prompt="git> " 16 | fi 17 | 18 | # Readline 19 | read -e -r -p "${prompt}" cmd 20 | 21 | # EOF 22 | test $? -ne 0 && break 23 | 24 | # History 25 | history -s "${cmd}" 26 | 27 | # Built-in commands 28 | case ${cmd} in 29 | "") continue;; 30 | quit|exit) break;; 31 | esac 32 | 33 | if [[ ${cmd} == !* ]]; then 34 | eval ${cmd:1} 35 | elif [[ ${cmd} == git* ]]; then 36 | eval ${cmd} 37 | else 38 | eval git "${cmd}" 39 | fi 40 | done 41 | 42 | echo 43 | -------------------------------------------------------------------------------- /bin/git-shortcut: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # For this to work, you have to have 2 global config items set for shortcut in git. 4 | # git config --global shortcut.user 5 | # git config --global shortcut.api-token 6 | function main () { 7 | local id="$1" 8 | local user=$(git config --global --get shortcut.user) 9 | local token=$(git config --global --get shortcut.api-token) 10 | local story=$(curl -s -X GET -H "Content-Type: application/json" -H "Shortcut-Token: ${token}" "https://api.app.shortcut.com/api/v3/stories/${id}") 11 | local name=$(echo "${story}" | jq -r '.name') 12 | local description=$(echo "${name}" | sed -e 's/[[:space:]]*$//' | iconv -t ascii//TRANSLIT | sed -E s/[^a-zA-Z0-9]+/-/g | sed -E s/^-+\|-+$//g | tr A-Z a-z) 13 | 14 | git checkout -b "${user}/sc-${id}/${description}" 15 | git empty "${name}" 16 | } 17 | 18 | main "$@" -------------------------------------------------------------------------------- /bin/git-issue: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # For this to work, you have to have a global config set for github in git. 4 | # git config --global github.api-token 5 | function main () { 6 | local id="$1" 7 | local origin=$(git config --get remote.origin.url) 8 | local repo=$(echo "${origin}" | sed -E 's/git@github.com\:(.*)\.git/\1/') 9 | local token=$(git config --global --get github.api-token) 10 | local url="https://api.github.com/repos/${repo}/issues/${id}" 11 | local issue=$(curl -s -X GET -H "Authorization: Bearer ${token}" "${url}") 12 | local title=$(echo "${issue}" | jq -r '.title') 13 | local type=$(echo "${issue}" | jq -r '.type.name') 14 | local branch="$(echo "${type}" | tr '[:upper:]' '[:lower:]')-${id}" 15 | # local description=$(echo "${name}" | sed -e 's/[[:space:]]*$//' | iconv -t ascii//TRANSLIT | sed -E s/[^a-zA-Z0-9]+/-/g | sed -E s/^-+\|-+$//g | tr A-Z a-z) 16 | 17 | git checkout -b "${branch}"; 18 | git empty "${type} ${id} - ${title}" 19 | } 20 | 21 | main "$@" -------------------------------------------------------------------------------- /bin/npm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Prevent the wrapper directory from shadowing the real npm binary. 6 | sanitize_path() { 7 | local cleaned_entries=() entry path_entries=() 8 | local self_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P) 9 | 10 | IFS=':' read -r -a path_entries <<< "${PATH:-}" 11 | for entry in "${path_entries[@]}"; do 12 | [[ "$entry" == "$self_dir" ]] && continue 13 | cleaned_entries+=("$entry") 14 | done 15 | 16 | if [[ "${#cleaned_entries[@]}" -eq 0 ]]; then 17 | echo "npm wrapper: unable to find alternate PATH entry" >&2 18 | return 1 19 | else 20 | printf '%s' "${cleaned_entries[0]}" 21 | for ((i = 1; i < ${#cleaned_entries[@]}; i++)); do 22 | printf ':%s' "${cleaned_entries[i]}" 23 | done 24 | fi 25 | } 26 | 27 | main() { 28 | local sanitized_path=$(sanitize_path) || exit 127 29 | export PATH="$sanitized_path" 30 | 31 | if [[ -n "${NPM_CMD:-}" ]]; then 32 | command npm "$@" 33 | return 34 | fi 35 | 36 | if [[ "$#" -gt 0 && "$1" == "install" ]]; then 37 | if [[ "$#" -eq 1 ]]; then 38 | command npm ci 39 | else 40 | command npm "$@" 41 | fi 42 | else 43 | command npm "$@" 44 | fi 45 | } 46 | 47 | main "$@" 48 | -------------------------------------------------------------------------------- /bin/git-sus: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | local since="yesterday.midnight" 5 | local until="midnight" 6 | local author_email 7 | local branch="" 8 | local arg 9 | 10 | for arg in "$@"; do 11 | case "$arg" in 12 | --since=*) 13 | since="${arg#*=}" 14 | ;; 15 | --until=*) 16 | until="${arg#*=}" 17 | ;; 18 | --branch=*) 19 | branch="${arg#*=}" 20 | ;; 21 | *) 22 | echo "Unknown option: $arg" 23 | exit 1 24 | ;; 25 | esac 26 | done 27 | 28 | if [[ -n "$until" && -z "$since" ]]; then 29 | echo "Error: --until requires --since to be set." 30 | exit 1 31 | fi 32 | 33 | author_email=$(git config user.email) 34 | if [[ -z "$author_email" ]]; then 35 | echo "Error: No Git user email configured." 36 | exit 1 37 | fi 38 | 39 | local cmd="git log" 40 | 41 | if [[ -n "$branch" ]]; then 42 | cmd+=" $branch" 43 | else 44 | cmd+=" --all" 45 | fi 46 | cmd+=" --no-merges --oneline --author=\"$author_email\"" 47 | 48 | if [[ -n "$since" ]]; then 49 | cmd+=" --since=\"$since\"" 50 | if [[ -n "$until" ]]; then 51 | cmd+=" --until=\"$until\"" 52 | fi 53 | fi 54 | 55 | eval "$cmd" 56 | } 57 | 58 | main "$@" 59 | -------------------------------------------------------------------------------- /bin/git-sync: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | function main () { 4 | local default_branch=$(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'); 5 | local branch="${1-$default_branch}" 6 | local remote="${2-origin}" 7 | git fetch 8 | git checkout "${branch}" 9 | git pull "${2-origin}" "${branch}" 10 | git tidy 11 | if git remote | grep -q '^upstream$'; then 12 | echo "Fetching from upstream..." 13 | git fetch upstream 14 | 15 | # determine upstream's default branch (not assume "main") 16 | upstream_main=$(git symbolic-ref refs/remotes/upstream/HEAD 2>/dev/null | sed 's@^refs/remotes/upstream/@@') 17 | if [ -z "$upstream_main" ]; then 18 | upstream_main=$(git ls-remote --symref upstream HEAD 2>/dev/null | awk '/^ref:/ { sub("refs/heads/","",$2); print $2; exit }') 19 | fi 20 | if [ -z "$upstream_main" ]; then 21 | if git ls-remote --heads upstream main | grep -q .; then 22 | upstream_main=main 23 | elif git ls-remote --heads upstream master | grep -q .; then 24 | upstream_main=master 25 | else 26 | upstream_main=master 27 | fi 28 | fi 29 | 30 | echo "Rebasing onto upstream/${upstream_main}..." 31 | git rebase "upstream/${upstream_main}" 32 | fi 33 | } 34 | 35 | main "$@" 36 | -------------------------------------------------------------------------------- /gitignore: -------------------------------------------------------------------------------- 1 | ### Linux 2 | 3 | \*~ 4 | 5 | # temporary files which can be created if a process still has a handle open of a deleted file 6 | 7 | .fuse_hidden\* 8 | 9 | # KDE directory preferences 10 | 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | 15 | .Trash-\* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | 19 | .nfs\* 20 | 21 | ### macOS 22 | 23 | \*.DS_Store 24 | .AppleDouble 25 | .LSOverride 26 | 27 | # Icon must end with two \r 28 | 29 | # Icon 30 | 31 | # Thumbnails 32 | 33 | .\_\* 34 | 35 | # Files that might appear in the root of a volume 36 | 37 | .DocumentRevisions-V100 38 | .fseventsd 39 | .Spotlight-V100 40 | .TemporaryItems 41 | .Trashes 42 | .VolumeIcon.icns 43 | .com.apple.timemachine.donotpresent 44 | 45 | # Directories potentially created on remote AFP share 46 | 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk 52 | 53 | ### Windows 54 | 55 | # Windows thumbnail cache files 56 | 57 | Thumbs.db 58 | ehthumbs.db 59 | ehthumbs_vista.db 60 | 61 | # Folder config file 62 | 63 | Desktop.ini 64 | 65 | # Recycle Bin used on file shares 66 | 67 | $RECYCLE.BIN/ 68 | 69 | # Windows Installer files 70 | 71 | _.cab 72 | _.msi 73 | _.msm 74 | _.msp 75 | 76 | # Windows shortcuts 77 | 78 | \*.lnk 79 | 80 | # vscode 81 | 82 | .vscode 83 | -------------------------------------------------------------------------------- /config.fish: -------------------------------------------------------------------------------- 1 | # Disable fish greeting on startup 2 | function fish_greeting 3 | end 4 | 5 | # Don't display date on bobthefish theme 6 | set -g theme_display_date no 7 | 8 | # Set bobthefish color 9 | set -g theme_color_scheme dracula 10 | 11 | # Where are my dotfiles? 12 | set -Ux DOTFILES $HOME/.files 13 | 14 | # I like to use VSCode; for now. 15 | set -Ux EDITOR code 16 | 17 | # Central timezone, what's up? 18 | set -Ux TZ America/Chicago 19 | 20 | # GPG Signing 21 | set -Ux GPG_TTY (tty) 22 | 23 | # Reset $PATH 24 | set -g fish_user_paths 25 | 26 | # Add Homebrews bin to $PATH 27 | fish_add_path /opt/homebrew/bin 28 | 29 | # Add Homebrew's sbin to $PATH 30 | fish_add_path /opt/homebrew/sbin 31 | 32 | # Add Composer global bin to $PATH 33 | fish_add_path $HOME/.composer/vendor/bin 34 | 35 | # Setup Android Studio 36 | set -Ux JAVA_HOME /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home 37 | set -Ux ANDROID_HOME $HOME/Library/Android/sdk 38 | fish_add_path $ANDROID_HOME/emulator 39 | fish_add_path $ANDROID_HOME/platform-tools 40 | 41 | # Add dotfiles bin to $PATH 42 | fish_add_path $DOTFILES/bin 43 | 44 | # Hack to auto expand aliases in sudo. 45 | alias sudo="sudo " 46 | 47 | # Because sometimes you gotta be harsh. 48 | alias fucking="sudo" 49 | 50 | # And sometimes you gotta be nice. 51 | alias please="sudo" 52 | 53 | # Get my IP Address. 54 | alias ip="curl ifconfig.co" 55 | 56 | # Run remote commands over ssh. 57 | alias remote="ssh $1 -T $2" 58 | 59 | # LOL don't be Jamon. 60 | # https://twitter.com/jamonholmgren/status/967548502648668161 61 | alias rm="trash" 62 | 63 | # Kubernetes helper 64 | alias k="kubectl" 65 | 66 | # Laravel Artisan helpers 67 | alias art="artisan" 68 | alias tinker="artisan tinker" 69 | alias fresh="artisan migrate:fresh" 70 | alias migrate="artisan migrate" 71 | alias rollback="artisan migrate:rollback" 72 | alias solo="artisan solo" 73 | 74 | # Git helpers 75 | alias wip="git save \"WIP\"" 76 | alias push="git push" 77 | 78 | # I use neovim, btw 79 | alias neovim="vscode" 80 | alias nvim=neovim 81 | 82 | # uWu big papi 83 | function vscode 84 | eval $EDITOR; 85 | end -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | tap "buildpacks/tap" 2 | tap "heroku/brew" 3 | tap "homebrew/bundle" 4 | tap "homebrew/cask" 5 | tap "homebrew/cask-fonts" 6 | tap "homebrew/cask-versions" 7 | tap "homebrew/core" 8 | tap "homebrew/services" 9 | tap "nats-io/nats-tools" 10 | tap "nektos/tap" 11 | tap "weaveworks/tap" 12 | brew "aws-iam-authenticator" 13 | brew "ca-certificates" 14 | brew "gdbm" 15 | brew "mpdecimal" 16 | brew "openssl@1.1" 17 | brew "awscli" 18 | brew "bdw-gc" 19 | brew "icu4c" 20 | brew "boost" 21 | brew "brotli" 22 | brew "c-ares" 23 | brew "caddy", restart_service: true 24 | brew "cilium-cli" 25 | brew "cloc" 26 | brew "deno" 27 | brew "dnsmasq" 28 | brew "doctl" 29 | brew "double-conversion" 30 | brew "ncurses" 31 | brew "pcre2" 32 | brew "fish" 33 | brew "fmt" 34 | brew "gflags" 35 | brew "glog" 36 | brew "libevent" 37 | brew "lz4" 38 | brew "folly" 39 | brew "gettext" 40 | brew "git" 41 | brew "gmp" 42 | brew "libffi" 43 | brew "m4" 44 | brew "libtool" 45 | brew "libunistring" 46 | brew "pkg-config" 47 | brew "guile" 48 | brew "libidn2" 49 | brew "libnghttp2" 50 | brew "libtasn1" 51 | brew "nettle" 52 | brew "p11-kit" 53 | brew "gnutls" 54 | brew "libgpg-error" 55 | brew "libassuan" 56 | brew "libgcrypt" 57 | brew "libksba" 58 | brew "libusb" 59 | brew "npth" 60 | brew "pinentry" 61 | brew "gnupg" 62 | brew "go" 63 | brew "helm" 64 | brew "httpie" 65 | brew "jemalloc" 66 | brew "oniguruma" 67 | brew "jq" 68 | brew "krb5" 69 | brew "kubernetes-cli" 70 | brew "libev" 71 | brew "libuv" 72 | brew "macos-term-size" 73 | brew "mas" 74 | brew "nghttp2" 75 | brew "node" 76 | brew "nspr" 77 | brew "nss" 78 | brew "openjdk" 79 | brew "pcre" 80 | brew "pinentry-mac" 81 | brew "postgresql", restart_service: true 82 | brew "redis", restart_service: true 83 | brew "trash" 84 | brew "watchman" 85 | brew "wget" 86 | brew "yarn" 87 | brew "nats-io/nats-tools/nats" 88 | brew "weaveworks/tap/eksctl" 89 | cask "1password" 90 | cask "avibrazil-rdm" 91 | cask "cleanmymac" 92 | cask "docker" 93 | cask "figma" 94 | cask "firefox" 95 | cask "fliqlo" 96 | cask "font-fira-code" 97 | cask "font-hack-nerd-font" 98 | cask "kap" 99 | cask "mosaic" 100 | cask "ngrok" 101 | cask "parallels" 102 | cask "postman" 103 | cask "safari-technology-preview" 104 | cask "slack" 105 | cask "synergy" 106 | cask "tableplus" 107 | cask "unite" 108 | cask "visual-studio-code" 109 | cask "zoom" 110 | -------------------------------------------------------------------------------- /zshrc: -------------------------------------------------------------------------------- 1 | # Where are my dotfiles? 2 | export DOTFILES="${HOME}/.files" 3 | 4 | # I like to use VSCode; for now. 5 | export EDITOR=code 6 | 7 | # Central timezone, what's up? 8 | export TZ=America/Chicago 9 | 10 | # Consistent default $PATH, nawm sayn. 11 | export PATH=`cat /etc/paths | tr "\\n" ":" | sed 's/:$//'` 12 | export PATH="/usr/local/sbin:${PATH}" 13 | 14 | # Go-lang PATH. 15 | export GOPATH="${HOME}/Public/golang-code" 16 | 17 | # Yarn global bin prefix. 18 | export PREFIX="/usr/local" 19 | 20 | # Global Composer bin. 21 | export GLOBAL_COMPOSER_BIN="${HOME}/.composer/vendor/bin" 22 | 23 | # Global yarn bin. 24 | YARN_GLOBAL_DIR=`yarn global dir` 25 | export GLOBAL_YARN_BIN="${YARN_GLOBAL_DIR}/node_modules/.bin" 26 | 27 | # Local Node Modules bin. 28 | export LOCAL_NODE_MODULES_BIN="./node_modules/.bin" 29 | 30 | # Local Composer bin. 31 | export LOCAL_COMPOSER_BIN="./vendor/bin" 32 | 33 | # Now lets add our own to $PATH. 34 | export PATH="${PATH}:${GOPATH}/bin" 35 | export PATH="${PATH}:${DOTFILES}/bin" 36 | export PATH="${PATH}:${GLOBAL_COMPOSER_BIN}" 37 | export PATH="${PATH}:${LOCAL_COMPOSER_BIN}" 38 | export PATH="${PATH}:${GLOBAL_YARN_BIN}" 39 | export PATH="${PATH}:${LOCAL_NODE_MODULES_BIN}" 40 | export PATH="${PATH}:${ANDROID_HOME}/tools" 41 | export PATH="${PATH}:${ANDROID_HOME}/platform-tools" 42 | 43 | # Cask needs to keep all applications together. 44 | export HOMEBREW_CASK_OPTS="--appdir=/Applications" 45 | 46 | # Android Emulation. 47 | export ANDROID_HOME="$HOME/Library/Android/sdk" 48 | 49 | # For historical purposes. 50 | export HISTSIZE=10000 51 | export SAVEHIST=8500 52 | 53 | # Is antigen installed? 54 | if [ ! -d "${HOME}/.antigen" ]; then 55 | # Nope! Install it. 56 | git clone https://github.com/zsh-users/antigen.git ${HOME}/.antigen 57 | fi 58 | 59 | # Now source it. 60 | source ${HOME}/.antigen/antigen.zsh 61 | 62 | # Oh My ZSH! 63 | COMPLETION_WAITING_DOTS="true" 64 | antigen use oh-my-zsh 65 | 66 | # Load themes. 67 | antigen theme https://gist.github.com/joshmanders/3d6a1fae12cafb52b9346c4ace705db9 bos-style 68 | # Lets load up some bundles. 69 | antigen bundle git 70 | antigen bundle zsh-users/zsh-history-substring-search 71 | antigen bundle rupa/z 72 | antigen bundle zsh-users/zsh-syntax-highlighting 73 | 74 | # bind UP and DOWN arrow keys. 75 | zmodload zsh/terminfo 76 | bindkey "$terminfo[kcuu1]" history-substring-search-up 77 | bindkey "$terminfo[kcud1]" history-substring-search-down 78 | 79 | # Apply that shizzle! 80 | antigen apply 81 | 82 | # Not sure what this is, yet. 83 | setopt nocorrectall 84 | 85 | # Auto suggestions, woohoo! 86 | source /usr/local/share/zsh-autosuggestions/zsh-autosuggestions.zsh 87 | 88 | # Load aliases. 89 | source ${DOTFILES}/aliases 90 | 91 | # Use direnv 92 | eval "$(direnv hook zsh)" 93 | -------------------------------------------------------------------------------- /twitter.unite.user-style.css: -------------------------------------------------------------------------------- 1 | // body {zoom: 125%;} 2 | 3 | header[role="banner"] { 4 | align-items: start !important; 5 | max-width: 100px !important; 6 | } 7 | 8 | header[role="banner"] > div > div > div { 9 | justify-content: center; 10 | padding-top: 0; 11 | } 12 | 13 | header[role="banner"] > div > div > div > div { 14 | margin-top: 0 !important; 15 | } 16 | 17 | header[role="banner"] > div > div > div > div:nth-child(1) { 18 | padding-top: 0 !important; 19 | } 20 | 21 | main[role="main"] { 22 | align-items: center !important; 23 | } 24 | 25 | main[role="main"] > div > div > div { 26 | justify-content: center !important; 27 | } 28 | 29 | a[aria-label="Tweet"][role="button"] { 30 | position: fixed !important; 31 | right: 32px !important; 32 | bottom: 24px !important; 33 | max-width: 125px !important; 34 | transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1); 35 | } 36 | 37 | a[data-testid="SideNav_NewTweet_Button"] { 38 | position: fixed !important; 39 | right: 32px !important; 40 | bottom: 24px !important; 41 | max-width: 125px !important; 42 | transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1); 43 | } 44 | 45 | @media all and (max-width: 800px) { 46 | a[data-testid="SideNav_NewTweet_Button"] { 47 | position: relative !important; 48 | right: auto !important; 49 | bottom: auto !important; 50 | } 51 | 52 | a[aria-label="Tweet"][role="button"] { 53 | position: relative !important; 54 | right: auto !important; 55 | bottom: auto !important; 56 | } 57 | } 58 | 59 | header[role="banner"] > div > div > div { 60 | justify-content: center; 61 | } 62 | 63 | header[role="banner"] > div > div > div > div { 64 | margin-top: auto; 65 | } 66 | 67 | header[role="banner"] > div > div > div > div:nth-child(1) { 68 | padding-top: 59px; 69 | } 70 | 71 | header[role="banner"] 72 | > div 73 | > div 74 | > div 75 | > div 76 | > div:nth-child(2) 77 | > nav 78 | > a 79 | > div 80 | > div:nth-child(2), 81 | header[role="banner"] 82 | > div 83 | > div 84 | > div 85 | > div 86 | > div:nth-child(2) 87 | > nav 88 | > div 89 | > div 90 | > div:nth-child(2) { 91 | opacity: 0 !important; 92 | transition: 0.5s cubic-bezier(0.2, 0.8, 0.2, 1); 93 | } 94 | 95 | header[role="banner"] 96 | > div 97 | > div 98 | > div 99 | > div 100 | > div:nth-child(2) 101 | > nav:hover 102 | > a 103 | > div 104 | > div:nth-child(2), 105 | header[role="banner"] 106 | > div 107 | > div 108 | > div 109 | > div 110 | > div:nth-child(2) 111 | > nav:hover 112 | > div 113 | > div 114 | > div:nth-child(2) { 115 | opacity: 1 !important; 116 | } 117 | 118 | nav[aria-label="Primary"][role="navigation"] > a > div > div:nth-child(2), 119 | nav[aria-label="Primary"][role="navigation"] > div > div > div:nth-child(2) { 120 | opacity: 0 !important; 121 | transition: 0.5s cubic-bezier(0.2, 0.8, 0.2, 1); 122 | } 123 | 124 | nav[aria-label="Primary"][role="navigation"]:hover > a > div > div:nth-child(2), 125 | nav[aria-label="Primary"][role="navigation"]:hover 126 | > div 127 | > div 128 | > div:nth-child(2) { 129 | opacity: 1 !important; 130 | } 131 | 132 | div[data-testid="primaryColumn"] { 133 | border-left-width: 0; 134 | border-right-width: 0; 135 | } 136 | 137 | div[data-testid="sidebarColumn"] { 138 | visibility: hidden !important; 139 | width: 120px !important; 140 | z-index: 1; 141 | } 142 | 143 | div[aria-label="Timeline: Trending now"] { 144 | display: none; 145 | } 146 | 147 | form[aria-label="Search Twitter"][role="search"] { 148 | visibility: visible !important; 149 | position: fixed !important; 150 | max-width: 342px !important; 151 | top: 8px; 152 | right: 24px; 153 | } 154 | 155 | form[aria-label="Search Twitter"][role="search"] > div:nth-child(1) > div { 156 | background-color: transparent !important; 157 | } 158 | 159 | input[placeholder="Search Twitter"] { 160 | padding-left: 34px !important; 161 | margin-left: -24px !important; 162 | z-index: 1 !important; 163 | width: 150px !important; 164 | transition: 0.5s cubic-bezier(0.2, 0.8, 0.2, 1); 165 | } 166 | 167 | input[placeholder="Search Twitter"]::placeholder { 168 | font-size: 15px !important; 169 | } 170 | 171 | input[placeholder="Search Twitter"]:focus { 172 | padding-left: 10px !important; 173 | margin-left: 0 !important; 174 | width: 300px !important; 175 | } 176 | 177 | @media all and (max-width: 1265px) { 178 | div[data-testid="sidebarColumn"] { 179 | visibility: hidden !important; 180 | width: 100px !important; 181 | } 182 | 183 | form[aria-label="Search Twitter"][role="search"] { 184 | visibility: hidden !important; 185 | } 186 | } 187 | 188 | div[data-testid="primaryColumn"] 189 | > div 190 | > div 191 | > div 192 | > div 193 | > div 194 | > div 195 | > div 196 | > div 197 | > div 198 | > div 199 | > div 200 | > div 201 | > form[aria-label="Search Twitter"][role="search"] { 202 | visibility: visible !important; 203 | position: relative !important; 204 | max-width: 100% !important; 205 | top: auto; 206 | right: auto; 207 | } 208 | 209 | @media all and (max-width: 1265px) { 210 | div[data-testid="primaryColumn"] 211 | > div 212 | > div 213 | > div 214 | > div 215 | > div 216 | > div 217 | > div 218 | > div 219 | > div 220 | > div 221 | > div 222 | > div 223 | > form[aria-label="Search Twitter"][role="search"] { 224 | visibility: visible !important; 225 | } 226 | } 227 | 228 | @media all and (max-width: 1300px) { 229 | div[data-testid="primaryColumn"] 230 | > div 231 | > div 232 | > div 233 | > div 234 | > div 235 | > div 236 | > div 237 | > div 238 | > div 239 | > div 240 | > div 241 | > div 242 | > form[aria-label="Search Twitter"][role="search"] { 243 | display: block !important; 244 | } 245 | } 246 | 247 | div[data-testid="primaryColumn"] 248 | > div 249 | > div 250 | > div 251 | > div 252 | > div 253 | > div 254 | > div 255 | > div 256 | > div 257 | > div 258 | > div 259 | > div 260 | > form[aria-label="Search Twitter"][role="search"] 261 | > div 262 | > div 263 | > div 264 | > div:nth-child(2) 265 | > input[placeholder="Search Twitter"] { 266 | width: 100% !important; 267 | } 268 | 269 | div[data-testid="primaryColumn"] 270 | > div 271 | > div 272 | > div 273 | > div 274 | > div 275 | > div 276 | > div 277 | > div 278 | > div 279 | > div 280 | > div 281 | > div 282 | > form[aria-label="Search Twitter"][role="search"] 283 | > div 284 | > div 285 | > div 286 | > div:nth-child(2) 287 | > input[placeholder="Search Twitter"]:focus { 288 | width: 100% !important; 289 | } 290 | 291 | div[data-testid="SideNav_AccountSwitcher_Button"] > div:nth-child(2), 292 | div[data-testid="SideNav_AccountSwitcher_Button"] > div:nth-child(3) { 293 | opacity: 0; 294 | transition: 0.5s cubic-bezier(0.2, 0.8, 0.2, 1); 295 | } 296 | 297 | div[data-testid="SideNav_AccountSwitcher_Button"]:hover > div:nth-child(2), 298 | div[data-testid="SideNav_AccountSwitcher_Button"]:hover > div:nth-child(3) { 299 | opacity: 1; 300 | } 301 | 302 | div[data-testid="primaryColumn"] > div > div:nth-child(2) > div > div { 303 | max-width: 100%; 304 | } 305 | 306 | svg[style="left: calc(105.5px);"] { 307 | left: calc(17.5px) !important; 308 | } 309 | 310 | div[data-testid="DMDrawer"] { 311 | visibility: hidden; 312 | } 313 | 314 | [data-testid="placementTracking"] article { 315 | display: none; 316 | } 317 | 318 | div[data-testid="primaryColumn"], 319 | div[data-testid="primaryColumn"] > div > div, 320 | div[data-testid="primaryColumn"] > div > div > div:nth-child(2), 321 | div[data-testid="primaryColumn"] > div > div > div:nth-child(3), 322 | div[data-testid="primaryColumn"] > div > div > div:nth-child(4), 323 | div[data-testid="primaryColumn"] > div > div > div:nth-child(2) > div > div { 324 | max-width: 900px !important; 325 | } 326 | 327 | -------------------------------------------------------------------------------- /bin/brewdump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | # -e : exit on first error 4 | # -u : error on unset variables 5 | # -o pipefail : pipeline fails if any command fails 6 | 7 | ############################################### 8 | # Interactive Homebrew cleaner + Brewfile dump 9 | # 10 | # High-level behavior: 11 | # - Finds "leaf top-level" formulae: 12 | # * Installed explicitly (installed_on_request == true) 13 | # * Not marked as installed_as_dependency 14 | # * Present in `brew leaves` (nothing else depends on them) 15 | # - Finds "top-level" casks based on installed_on_request / heuristics. 16 | # - Lets you review each candidate and choose: 17 | # * k: keep 18 | # * r: mark for removal 19 | # * s: skip the rest (treat all remaining as keep) 20 | # - At the end: 21 | # * Optionally batch-uninstalls all marked formulae and casks. 22 | # * Optionally runs `brew autoremove` to clean unused dependencies. 23 | # * In non-dry-run mode, can dump a clean Brewfile of what remains. 24 | ############################################### 25 | 26 | DRY_RUN=false 27 | # If the first argument is "--dry-run", enable simulation mode: 28 | # - No uninstall commands are executed. 29 | # - No Brewfile is generated. 30 | # - `brew autoremove` is not actually run; commands are just printed. 31 | if [[ "${1:-}" == "--dry-run" ]]; then 32 | DRY_RUN=true 33 | echo ">>> DRY RUN ENABLED — no changes will be made." 34 | echo 35 | fi 36 | 37 | # Ensure Homebrew is available; abort early with a clear message if not. 38 | if ! command -v brew >/dev/null 2>&1; then 39 | echo "Error: Homebrew (brew) not found. Install Homebrew first." 40 | exit 1 41 | fi 42 | 43 | # Ensure jq is available; required to parse `brew info --json`. 44 | if ! command -v jq >/dev/null 2>&1; then 45 | echo "Error: jq is required for this script." 46 | echo "Install it with: brew install jq" 47 | exit 1 48 | fi 49 | 50 | # Print a horizontal divider line across the terminal for readability. 51 | divider() { 52 | printf '%*s\n' "${COLUMNS:-80}" '' | tr ' ' '-' 53 | } 54 | 55 | # Ask a yes/no question. 56 | # Arguments: 57 | # $1: question text 58 | # $2: "default_yes" or "default_no" 59 | # Behavior: 60 | # - Shows [Y/n] if default_yes, [y/N] if default_no. 61 | # - Returns 0 for yes, 1 for no. 62 | prompt_yes_no() { 63 | local question="$1" 64 | local default="$2" 65 | local prompt 66 | 67 | if [[ "$default" == "default_yes" ]]; then 68 | prompt=" [Y/n] " 69 | else 70 | prompt=" [y/N] " 71 | fi 72 | 73 | while true; do 74 | read -r -p "$question$prompt" reply 75 | case "$reply" in 76 | [Yy]* ) return 0 ;; # explicit yes 77 | [Nn]* ) return 1 ;; # explicit no 78 | "" ) # user just hit Enter 79 | if [[ "$default" == "default_yes" ]]; then 80 | return 0 81 | else 82 | return 1 83 | fi 84 | ;; 85 | * ) 86 | echo "Please answer y or n." 87 | ;; 88 | esac 89 | done 90 | } 91 | 92 | # Arrays to track which formulae and casks the user has chosen to remove. 93 | declare -a REMOVED_FORMULAE=() 94 | declare -a REMOVED_CASKS=() 95 | 96 | # Flag to stop further prompting: 97 | # 0 = normal (keep prompting), 98 | # 1 = user chose [s]kip rest (treat all remaining as keep). 99 | SKIP_REST=0 100 | 101 | ############################################### 102 | # inspect_formula 103 | # Show details for a single formula and ask: 104 | # [k]eep / [r]emove / [s]kip rest 105 | # - name (arg 1) is the formula name. 106 | ############################################### 107 | inspect_formula() { 108 | local name="$1" 109 | 110 | # Get formula info in JSON form. Abort this formula if info fails. 111 | local json 112 | if ! json="$(brew info --json=v2 "$name" 2>/dev/null)"; then 113 | echo "Failed to get info for formula $name" 114 | return 115 | fi 116 | 117 | # Extract relevant fields from JSON. 118 | local desc homepage installed_on_request installed_as_dependency deps opt_deps build_deps uses 119 | 120 | desc="$(echo "$json" | jq -r '.formulae[0].desc // "No description available."')" 121 | homepage="$(echo "$json" | jq -r '.formulae[0].homepage // "No homepage"')" 122 | 123 | # installed_on_request: did you explicitly install this formula? 124 | # installed_as_dependency: was it recorded as being installed as a dependency? 125 | # We defensively check that `.installed` is an array with at least one entry. 126 | installed_on_request="$( 127 | echo "$json" | 128 | jq -r '.formulae[0].installed 129 | | if (type == "array" and length > 0) 130 | then .[0].installed_on_request // false 131 | else false 132 | end' 133 | )" 134 | 135 | installed_as_dependency="$( 136 | echo "$json" | 137 | jq -r '.formulae[0].installed 138 | | if (type == "array" and length > 0) 139 | then .[0].installed_as_dependency // false 140 | else false 141 | end' 142 | )" 143 | 144 | # Dependency lists (for information only; we don't act on them directly). 145 | deps="$(echo "$json" | jq -r '.formulae[0].dependencies | join(", ") // ""')" 146 | opt_deps="$(echo "$json" | jq -r '.formulae[0].optional_dependencies | join(", ") // ""')" 147 | build_deps="$(echo "$json" | jq -r '.formulae[0].build_dependencies | join(", ") // ""')" 148 | 149 | # Reverse dependencies: which installed formulae depend on this one? 150 | # This is informational; the "leaf-ness" is handled earlier via `brew leaves`. 151 | uses="$(brew uses --installed "$name" 2>/dev/null | tr '\n' ' ')" 152 | 153 | # Present a summary to the user. 154 | divider 155 | echo "FORMULA: $name" 156 | echo "Description : $desc" 157 | echo "Homepage : $homepage" 158 | echo 159 | echo "Reasoning:" 160 | echo " - Installed on request? $installed_on_request" 161 | echo " - Installed as dependency? $installed_as_dependency" 162 | echo 163 | echo "Dependencies:" 164 | echo " - Required: ${deps:-}" 165 | echo " - Optional: ${opt_deps:-}" 166 | echo " - Build: ${build_deps:-}" 167 | echo 168 | echo "Other packages that depend on this:" 169 | echo " - ${uses:-}" 170 | echo 171 | 172 | # Interactive decision: keep, remove, or skip the rest. 173 | while true; do 174 | read -r -p "Keep this formula? [k]eep / [r]emove / [s]kip rest (default: keep) " answer 175 | case "$answer" in 176 | ""|"k"|"K") 177 | # Default or explicit keep: do nothing, move on. 178 | echo "Keeping $name" 179 | return 0 180 | ;; 181 | "r"|"R") 182 | # Mark formula for removal; actual uninstall happens later in batch. 183 | echo "Marking formula '$name' for removal..." 184 | REMOVED_FORMULAE+=("$name") 185 | return 0 186 | ;; 187 | "s"|"S") 188 | # Skip rest: stop prompting for any further items. 189 | # This item is treated as "keep". 190 | echo "Skipping remaining items. Treating the rest as keep." 191 | SKIP_REST=1 192 | echo "Keeping $name" 193 | return 0 194 | ;; 195 | *) 196 | echo "Please type k, r, or s." 197 | ;; 198 | esac 199 | done 200 | } 201 | 202 | ############################################### 203 | # inspect_cask 204 | # Show details for a single cask and ask: 205 | # [k]eep / [r]emove / [s]kip rest 206 | # - name (arg 1) is the cask token. 207 | ############################################### 208 | inspect_cask() { 209 | local name="$1" 210 | 211 | # Get cask info in JSON form. Abort this cask if info fails. 212 | local json 213 | if ! json="$(brew info --cask --json=v2 "$name" 2>/dev/null)"; then 214 | echo "Failed to get info for cask $name" 215 | return 216 | fi 217 | 218 | # Extract fields from cask JSON. 219 | local desc homepage depends_on_formula depends_on_cask installed_on_request 220 | 221 | desc="$(echo "$json" | jq -r '.casks[0].desc // "No description available."')" 222 | homepage="$(echo "$json" | jq -r '.casks[0].homepage // "No homepage"')" 223 | depends_on_formula="$(echo "$json" | jq -r '.casks[0].depends_on.formula // [] | join(", ")')" 224 | depends_on_cask="$(echo "$json" | jq -r '.casks[0].depends_on.cask // [] | join(", ")')" 225 | 226 | # installed_on_request is not always tracked for casks; 227 | # if it's missing, we assume it was explicitly installed. 228 | installed_on_request="$( 229 | echo "$json" | 230 | jq -r '.casks[0].installed 231 | | if (type == "array" and length > 0 and .[0].installed_on_request != null) 232 | then .[0].installed_on_request 233 | else true 234 | end' 235 | )" 236 | 237 | # Present a summary to the user. 238 | divider 239 | echo "CASK: $name" 240 | echo "Description : $desc" 241 | echo "Homepage : $homepage" 242 | echo 243 | echo "Reasoning:" 244 | echo " - Installed on request? $installed_on_request" 245 | echo 246 | echo "Dependencies:" 247 | echo " - Formulae: ${depends_on_formula:-}" 248 | echo " - Casks: ${depends_on_cask:-}" 249 | echo 250 | 251 | # Interactive decision: keep, remove, or skip the rest. 252 | while true; do 253 | read -r -p "Keep this cask? [k]eep / [r]emove / [s]kip rest (default: keep) " answer 254 | case "$answer" in 255 | ""|"k"|"K") 256 | # Default or explicit keep. 257 | echo "Keeping $name" 258 | return 0 259 | ;; 260 | "r"|"R") 261 | # Mark cask for removal; actual uninstall is done in batch at the end. 262 | echo "Marking cask '$name' for removal..." 263 | REMOVED_CASKS+=("$name") 264 | return 0 265 | ;; 266 | "s"|"S") 267 | # Skip rest: don't ask about the remaining items. 268 | echo "Skipping remaining items. Treating the rest as keep." 269 | SKIP_REST=1 270 | echo "Keeping $name" 271 | return 0 272 | ;; 273 | *) 274 | echo "Please type k, r, or s." 275 | ;; 276 | esac 277 | done 278 | } 279 | 280 | echo "Collecting installed Homebrew packages (leaf top-level only)..." 281 | 282 | ############################################### 283 | # Determine which formulae to present for review 284 | # 285 | # Strategy: 286 | # 1) Use `brew leaves` to get formulae that nothing else depends on. 287 | # 2) Use `brew info --installed --json=v2` to find formulae that were: 288 | # - installed_on_request == true 289 | # - installed_as_dependency != true 290 | # 3) Take the intersection of (1) and (2). 291 | # 292 | # This yields "leaf top-level" formulae: 293 | # - explicitly installed by you, 294 | # - not primarily dependencies, 295 | # - not depended on by any other formula. 296 | ############################################### 297 | 298 | # Step 1: get leaf formulae = no other formula depends on them. 299 | LEAVES_RAW="$(brew leaves 2>/dev/null || echo '')" 300 | declare -a LEAVES=() 301 | while IFS= read -r line; do 302 | [[ -n "$line" ]] && LEAVES+=("$line") 303 | done <<< "$LEAVES_RAW" 304 | 305 | # Step 2: get info for all installed formulae in JSON. 306 | FORMULA_INFO_JSON="$(brew info --installed --json=v2 2>/dev/null || echo '')" 307 | 308 | # Step 3: extract top-level candidates from JSON based on installed flags. 309 | TOPLEVEL_CANDIDATES_RAW="$( 310 | echo "$FORMULA_INFO_JSON" | jq -r ' 311 | .formulae[]? as $f 312 | | $f.installed as $inst 313 | | if ($inst | type == "array" and length > 0 314 | and $inst[0].installed_on_request == true 315 | and ($inst[0].installed_as_dependency != true)) 316 | then $f.name 317 | else empty 318 | end 319 | ' 320 | )" 321 | 322 | # Step 4: intersect candidates with leaves to get "leaf top-level formulae". 323 | declare -a TOP_LEVEL_FORMULAE=() 324 | while IFS= read -r name; do 325 | [[ -z "$name" ]] && continue 326 | # Check if this name appears in the LEAVES array (exact match). 327 | if printf '%s\n' "${LEAVES[@]}" | grep -qx "$name"; then 328 | TOP_LEVEL_FORMULAE+=("$name") 329 | fi 330 | done <<< "$TOPLEVEL_CANDIDATES_RAW" 331 | 332 | ############################################### 333 | # Determine which casks to present for review 334 | # 335 | # There is no `brew leaves` equivalent for casks, 336 | # so we use a simpler heuristic: 337 | # - If .installed is missing or not an array, treat as top-level. 338 | # - If installed_on_request is true or null, treat as top-level. 339 | # - Otherwise, skip it. 340 | ############################################### 341 | 342 | CASK_INFO_JSON="$(brew info --cask --json=v2 --installed 2>/dev/null || echo '')" 343 | 344 | declare -a TOP_LEVEL_CASKS=() 345 | while IFS= read -r line; do 346 | [[ -n "$line" ]] && TOP_LEVEL_CASKS+=("$line") 347 | done < <( 348 | echo "$CASK_INFO_JSON" | jq -r ' 349 | .casks[]? as $c 350 | | $c.installed as $inst 351 | | if ($inst | type != "array" or length == 0) 352 | then $c.token 353 | elif ($inst[0].installed_on_request == true or $inst[0].installed_on_request == null) 354 | then $c.token 355 | else empty 356 | end 357 | ' 358 | ) 359 | 360 | # Count how many items we will present to the user. 361 | FORMULA_COUNT=${#TOP_LEVEL_FORMULAE[@]} 362 | CASK_COUNT=${#TOP_LEVEL_CASKS[@]} 363 | 364 | echo 365 | echo "Leaf top-level items (for review):" 366 | echo " - $FORMULA_COUNT formulae" 367 | echo " - $CASK_COUNT casks" 368 | echo 369 | 370 | ############################################### 371 | # Decide whether to process formulae, casks, or both. 372 | # 373 | # Prompt: 374 | # [f] : only formulae 375 | # [c] : only casks 376 | # [b] or Enter : both 377 | ############################################### 378 | 379 | PROCESS_FORMULAE=true 380 | PROCESS_CASKS=true 381 | 382 | while true; do 383 | read -r -p "Process [f]ormulae, [c]asks, or [b]oth? (default: both) " mode 384 | case "$mode" in 385 | ""|"b"|"B") 386 | # Default: process both. 387 | break 388 | ;; 389 | "f"|"F") 390 | # Only formulae. 391 | PROCESS_CASKS=false 392 | break 393 | ;; 394 | "c"|"C") 395 | # Only casks. 396 | PROCESS_FORMULAE=false 397 | break 398 | ;; 399 | *) 400 | echo "Choose f, c, or b." 401 | ;; 402 | esac 403 | done 404 | 405 | ############################################### 406 | # Iterate through formulae and prompt the user 407 | # for each leaf top-level formula. 408 | ############################################### 409 | 410 | if $PROCESS_FORMULAE && (( ${#TOP_LEVEL_FORMULAE[@]} > 0 )); then 411 | echo 412 | echo "===== Processing leaf top-level formulae =====" 413 | for f in "${TOP_LEVEL_FORMULAE[@]}"; do 414 | # If user chose [s]kip rest earlier, stop prompting. 415 | [[ $SKIP_REST -eq 1 ]] && break 416 | inspect_formula "$f" || true 417 | done 418 | elif $PROCESS_FORMULAE; then 419 | echo "No leaf top-level formulae found." 420 | fi 421 | 422 | ############################################### 423 | # Iterate through casks and prompt the user 424 | # for each top-level cask. 425 | ############################################### 426 | 427 | if $PROCESS_CASKS && [[ $SKIP_REST -eq 0 ]]; then 428 | if (( ${#TOP_LEVEL_CASKS[@]} > 0 )); then 429 | echo 430 | echo "===== Processing top-level casks =====" 431 | for c in "${TOP_LEVEL_CASKS[@]}"; do 432 | # Again, respect [s]kip rest if set. 433 | [[ $SKIP_REST -eq 1 ]] && break 434 | inspect_cask "$c" || true 435 | done 436 | else 437 | echo "No top-level casks found." 438 | fi 439 | fi 440 | 441 | ############################################### 442 | # Summary and batch uninstall 443 | # 444 | # At this point, REMOVED_FORMULAE and REMOVED_CASKS 445 | # contain all the items the user marked for removal. 446 | # We show a summary and then, if confirmed, run: 447 | # brew uninstall 448 | # brew uninstall --cask 449 | # (or print the commands in dry-run mode). 450 | ############################################### 451 | 452 | divider 453 | echo "Summary of selections:" 454 | echo " Formulae marked for removal: ${REMOVED_FORMULAE[*]:-}" 455 | echo " Casks marked for removal: ${REMOVED_CASKS[*]:-}" 456 | echo 457 | 458 | if ((${#REMOVED_FORMULAE[@]} > 0)) || ((${#REMOVED_CASKS[@]} > 0)); then 459 | echo "About to remove:" 460 | ((${#REMOVED_FORMULAE[@]} > 0)) && echo " Formulae: ${REMOVED_FORMULAE[*]}" 461 | ((${#REMOVED_CASKS[@]} > 0)) && echo " Casks: ${REMOVED_CASKS[*]}" 462 | echo 463 | 464 | # Confirm batch removal. 465 | if prompt_yes_no "Proceed with these removals?" default_yes; then 466 | # Handle formulae. 467 | if ((${#REMOVED_FORMULAE[@]} > 0)); then 468 | if $DRY_RUN; then 469 | # Dry-run: just show the command that would be run. 470 | echo ">>> DRY RUN: brew uninstall ${REMOVED_FORMULAE[*]}" 471 | else 472 | # Real mode: uninstall all selected formulae at once. 473 | echo "Running: brew uninstall ${REMOVED_FORMULAE[*]}" 474 | brew uninstall "${REMOVED_FORMULAE[@]}" 475 | fi 476 | fi 477 | 478 | # Handle casks. 479 | if ((${#REMOVED_CASKS[@]} > 0)); then 480 | if $DRY_RUN; then 481 | # Dry-run: just show the command. 482 | echo ">>> DRY RUN: brew uninstall --cask ${REMOVED_CASKS[*]}" 483 | else 484 | # Real mode: uninstall all selected casks at once. 485 | echo "Running: brew uninstall --cask ${REMOVED_CASKS[*]}" 486 | brew uninstall --cask "${REMOVED_CASKS[@]}" 487 | fi 488 | fi 489 | else 490 | echo "Removals cancelled. No uninstall commands run." 491 | fi 492 | else 493 | echo "Nothing was marked for removal." 494 | fi 495 | 496 | echo 497 | 498 | ############################################### 499 | # Autoremove step 500 | # 501 | # Ask whether to run: 502 | # brew autoremove 503 | # to clean up now-unused dependencies. In dry-run mode 504 | # we just show the command and do nothing. 505 | ############################################### 506 | 507 | if prompt_yes_no "Run 'brew autoremove' to remove unused dependencies?" default_yes; then 508 | if $DRY_RUN; then 509 | echo ">>> DRY RUN: brew autoremove" 510 | else 511 | brew autoremove 512 | fi 513 | else 514 | echo "Skipping autoremove." 515 | fi 516 | 517 | echo 518 | 519 | ############################################### 520 | # Brewfile generation 521 | # 522 | # In non-dry-run mode, offer to generate a clean Brewfile 523 | # describing the current installed state after all removals: 524 | # brew bundle dump --file=Brewfile.clean --force 525 | # 526 | # In dry-run mode, we skip this to avoid confusing the 527 | # simulated state with reality. 528 | ############################################### 529 | 530 | if $DRY_RUN; then 531 | echo "Dry run: skipping Brewfile generation (no actual changes made)." 532 | else 533 | if prompt_yes_no "Generate clean Brewfile (remaining packages only) in ./Brewfile.clean?" default_yes; then 534 | # Ensure the `brew bundle` command is available. 535 | if ! brew help bundle >/dev/null 2>&1; then 536 | echo "Error: 'brew bundle' is not available. You may need:" 537 | echo " brew tap Homebrew/bundle" 538 | exit 1 539 | fi 540 | # Dump a Brewfile of everything still installed. 541 | brew bundle dump --file=Brewfile.clean --force 542 | echo "Clean Brewfile written to ./Brewfile.clean" 543 | else 544 | echo "Skipping Brewfile generation." 545 | fi 546 | fi 547 | 548 | echo 549 | echo "Done." 550 | --------------------------------------------------------------------------------