From 1804357162b974b2b880add1cd49df64071e2fba Mon Sep 17 00:00:00 2001 From: David Tomaschik Date: Tue, 14 Apr 2026 10:27:17 -0700 Subject: [PATCH 1/6] Update skel --- dotfiles/gitconfig.d/aliases | 9 +++++++++ dotfiles/ssh/rc | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 dotfiles/gitconfig.d/aliases diff --git a/dotfiles/gitconfig.d/aliases b/dotfiles/gitconfig.d/aliases new file mode 100644 index 0000000..766b723 --- /dev/null +++ b/dotfiles/gitconfig.d/aliases @@ -0,0 +1,9 @@ +[alias] +commit-assumed = "!f() { \ + file=\"$1\"; \ + shift; \ + git update-index --no-assume-unchanged \"$file\" && \ + git add \"$file\" && \ + git commit \"$@\" && \ + git update-index --assume-unchanged \"$file\"; \ + }; f" diff --git a/dotfiles/ssh/rc b/dotfiles/ssh/rc index 0c605e8..8745f16 100755 --- a/dotfiles/ssh/rc +++ b/dotfiles/ssh/rc @@ -9,7 +9,7 @@ if [ -S "${SSH_AUTH_SOCK}" ] ; then SSH_REMOTE_AUTH_SOCK="${SSH_AUTH_SOCK}" export SSH_REMOTE_AUTH_SOCK # Always update the symlink to the latest session's socket. - # This ensures that tmux (which uses the static path) always points to a + # This ensures that tmux (which uses the static path) always points to a # current agent. mkdir -p "$(dirname "${REMOTE_LINK}")" ln -sf "${SSH_AUTH_SOCK}" "${REMOTE_LINK}" From fa6a8784872cdb7e5f8292145c0e47353868f214 Mon Sep 17 00:00:00 2001 From: David Tomaschik Date: Sat, 18 Apr 2026 19:20:43 -0700 Subject: [PATCH 2/6] Fix SSH agent forwarding clobbered by local agent in shenv (#14) * Fix SSH agent forwarding clobbered by local agent in shenv ssh/rc saves the raw forwarded socket in SSH_REMOTE_AUTH_SOCK before rewriting SSH_AUTH_SOCK to the stable symlink. shenv was ignoring that variable, so it saw SSH_AUTH_SOCK as "our link" and fell through to the systemd lookup, which could overwrite the symlink with a local agent socket and silently drop the forwarded one. Now shenv checks SSH_REMOTE_AUTH_SOCK first, giving forwarded sockets priority over any local agent. https://claude.ai/code/session_01RhXaFzxJA5D2BcGcz18ipA * Fix shenv clobbering forwarded SSH socket with local agent in tmux ssh/rc env changes (including SSH_REMOTE_AUTH_SOCK) are lost because ssh/rc runs as a sshd child process, not the user's shell. The shell always receives SSH_AUTH_SOCK set to the raw forwarded socket path. Fresh SSH login worked fine (step 1 catches the raw socket). The bug was in tmux new windows: SSH_AUTH_SOCK there is our stable symlink, so step 1 fails, then steps 2/3 look up the system agent and overwrite the symlink that ssh/rc just set to the forwarded socket. Fix: only run the system agent lookup when the stable symlink is already broken. A valid symlink means ssh/rc (or a previous shenv run) already set it correctly; don't clobber it. https://claude.ai/code/session_01RhXaFzxJA5D2BcGcz18ipA * Remove pointless exports from ssh/rc, add process-model comment ssh/rc runs as a sshd child process so exports never reach the user's shell. SSH_REMOTE_AUTH_SOCK was set and exported but never used (a leftover from a prior failed fix attempt). SSH_AUTH_SOCK was reassigned to the symlink path and exported, also to no effect. Remove both. https://claude.ai/code/session_01RhXaFzxJA5D2BcGcz18ipA --------- Co-authored-by: Claude --- dotfiles/shenv | 14 +++++++++----- dotfiles/ssh/rc | 8 ++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/dotfiles/shenv b/dotfiles/shenv index cdb0382..368dfd3 100755 --- a/dotfiles/shenv +++ b/dotfiles/shenv @@ -113,13 +113,17 @@ _is_link_path() { _CANDIDATE="" -# 1. If current environment has a valid socket that is NOT our link, it's a prime candidate (e.g. SSH forwarding). +# 1. If current environment has a valid socket that is NOT our link, it's a prime candidate +# (e.g. fresh SSH login: sshd sets SSH_AUTH_SOCK to the raw forwarded socket before ssh/rc +# rewrites it to the stable symlink; the shell inherits the original raw path). if [ -S "${SSH_AUTH_SOCK:-}" ] && ! _is_link_path "${SSH_AUTH_SOCK}"; then _CANDIDATE="${SSH_AUTH_SOCK}" fi -# 2. If no candidate yet, or we're currently using the link, try to find the "real" system agent. -if [ -z "${_CANDIDATE}" ] || _is_link_path "${SSH_AUTH_SOCK:-}"; then +# 2. Only look for a system agent if the stable link is already broken. If the link is +# valid (e.g. a tmux pane where SSH_AUTH_SOCK points to our symlink which ssh/rc just +# updated to the forwarded socket), leave it alone — don't clobber it with a local agent. +if [ -z "${_CANDIDATE}" ] && [ ! -S "${_SSH_AUTH_LINK}" ]; then _FOUND="" if [ "$(uname)" = "Darwin" ]; then _FOUND=$(launchctl getenv SSH_AUTH_SOCK 2>/dev/null) @@ -143,8 +147,8 @@ if [ -z "${_CANDIDATE}" ] || _is_link_path "${SSH_AUTH_SOCK:-}"; then fi fi -# 3. Last resort: search common paths if we still don't have a valid candidate. -if [ ! -S "${_CANDIDATE}" ]; then +# 3. Last resort: search common paths if we still don't have a candidate and the link is broken. +if [ ! -S "${_CANDIDATE}" ] && [ ! -S "${_SSH_AUTH_LINK}" ]; then _U=$(id -u) for _p in "/run/user/${_U}/keyring/ssh" "/run/user/${_U}/ssh-agent.socket" "/run/user/${_U}/openssh_agent" "/run/user/${_U}/gnupg/S.gpg-agent.ssh"; do if [ -S "${_p}" ] && ! _is_link_path "${_p}"; then diff --git a/dotfiles/ssh/rc b/dotfiles/ssh/rc index 8745f16..70dff92 100755 --- a/dotfiles/ssh/rc +++ b/dotfiles/ssh/rc @@ -2,19 +2,19 @@ # Roughly based on this article: # https://werat.github.io/2017/02/04/tmux-ssh-agent-forwarding.html +# +# NOTE: this file is executed by sshd as a child process, NOT sourced by the +# user's shell. Any variable assignments or exports here have no effect on the +# shell environment the user will land in. REMOTE_LINK="${HOME}/.ssh/ssh_auth_sock" if [ -S "${SSH_AUTH_SOCK}" ] ; then - SSH_REMOTE_AUTH_SOCK="${SSH_AUTH_SOCK}" - export SSH_REMOTE_AUTH_SOCK # Always update the symlink to the latest session's socket. # This ensures that tmux (which uses the static path) always points to a # current agent. mkdir -p "$(dirname "${REMOTE_LINK}")" ln -sf "${SSH_AUTH_SOCK}" "${REMOTE_LINK}" - SSH_AUTH_SOCK="${REMOTE_LINK}" - export SSH_AUTH_SOCK fi # if stdin is a tty, don't do the cookie step From fec16225e421f56ad2690e6e3c5871a699435ba7 Mon Sep 17 00:00:00 2001 From: David Tomaschik Date: Tue, 21 Apr 2026 14:39:32 -0700 Subject: [PATCH 3/6] Build update-authorized-keys --- bin/update-authorized-keys | 310 +++++++++++++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100755 bin/update-authorized-keys diff --git a/bin/update-authorized-keys b/bin/update-authorized-keys new file mode 100755 index 0000000..59a4d4e --- /dev/null +++ b/bin/update-authorized-keys @@ -0,0 +1,310 @@ +#!/usr/bin/env bash + +# update-authorized-keys - Manage ~/.ssh/authorized_keys from multiple sources +# +# BEHAVIOR: +# 1. Collects SSH public keys from one or more source directories (default: ~/.ssh/authorized_keys.d). +# 2. Skips empty files and files symlinked to /dev/null (masking). +# 3. Deterministically concatenates keys into a "managed block" wrapped in markers: +# # BEGIN UPDATE-AUTHORIZED-KEYS +# # END UPDATE-AUTHORIZED-KEYS +# 4. Deduplicates managed keys: if the same key (including options) is found in multiple files, +# it is included once with a comment listing all source filenames. +# 5. Preserves "manual" keys found in the target file outside the markers. +# 6. Removes manual keys that exactly match a managed key (options + key data). +# 7. Validates every proposed key individually using 'ssh-keygen -l -f'. +# 8. Optionally validates the whole file with 'authorized-keys-test' if available. +# 9. Displays a unified diff and prompts for confirmation before atomic replacement. +# 10. Supports a --dry-run mode and a --self-test mode for verifying logic. + +set -o nounset +set -o errexit +set -o pipefail + +CLEANUP_FILES=() +cleanup() { + rm -rf "${CLEANUP_FILES[@]}" +} +trap cleanup EXIT + +# Configuration +DEFAULT_DIR="${HOME}/.ssh/authorized_keys.d" +DEFAULT_TARGET="${HOME}/.ssh/authorized_keys" +BEGIN_MARKER="# BEGIN UPDATE-AUTHORIZED-KEYS" +END_MARKER="# END UPDATE-AUTHORIZED-KEYS" + +# State +SOURCE_DIRS=() +TARGET_FILE="${DEFAULT_TARGET}" +DRY_RUN=0 + +usage() { + cat < "${d1}/k1" + echo "${key1}" > "${d2}/k1_dup" + echo "${key2}" > "${d2}/k2" + echo "${long_opt} ${key1}" > "${d1}/k1_long" + echo "${long_opt} ${key2}" > "${d1}/k2_long" + ln -s /dev/null "${d1}/masked" + + cat < "${target}" +${key_man} +${key1} # This should be removed as it's now managed +EOF + + echo "Executing script in test mode..." + # Pipe "y" to handle the TTY check if we are not in a TTY during test + echo "y" | "$0" --dir "${d1}" --extra-dir "${d2}" --target "${target}" > /dev/null + + local content=$(cat "${target}") + + echo -n "Check markers... " + if [[ "${content}" == *"${BEGIN_MARKER}"* && "${content}" == *"${END_MARKER}"* ]]; then echo "OK"; else echo "FAIL"; exit 1; fi + + echo -n "Check managed deduplication... " + if grep -q "Source: k1, k1_dup" "${target}"; then echo "OK"; else echo "FAIL"; exit 1; fi + + echo -n "Check long option deduplication (should NOT deduplicate different keys)... " + if grep -q "k1_long" "${target}" && grep -q "k2_long" "${target}"; then echo "OK"; else echo "FAIL"; exit 1; fi + + echo -n "Check manual key preservation... " + if grep -q "manual" "${target}"; then echo "OK"; else echo "FAIL"; exit 1; fi + + echo -n "Check manual key filtering... " + local manual_count=$(grep -c "${key1}" "${target}") + # key1 appears twice in managed block (once plain, once with long opt) + # and it was in manual block. The manual one should be removed. + # So we expect 2 occurrences in the final file (both in managed block). + if [[ ${manual_count} -eq 2 ]]; then echo "OK"; else echo "FAIL (Found ${manual_count} occurrences, expected 2)"; exit 1; fi + + echo -n "Check masking... " + if ! grep -q "masked" "${target}"; then echo "OK"; else echo "FAIL"; exit 1; fi + + echo "Self-test passed successfully!" + exit 0 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --dir) + [[ -z "${2:-}" ]] && { echo "Error: --dir requires an argument" >&2; exit 1; } + SOURCE_DIRS+=("$2"); shift 2 ;; + --extra-dir) + [[ -z "${2:-}" ]] && { echo "Error: --extra-dir requires an argument" >&2; exit 1; } + SOURCE_DIRS+=("$2"); shift 2 ;; + --target) + [[ -z "${2:-}" ]] && { echo "Error: --target requires an argument" >&2; exit 1; } + TARGET_FILE="$2"; shift 2 ;; + --dry-run) DRY_RUN=1; shift ;; + --self-test) run_self_test ;; + --help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +if [[ ${#SOURCE_DIRS[@]} -eq 0 ]]; then + SOURCE_DIRS+=("${DEFAULT_DIR}") +fi + +mkdir -p "$(dirname "${TARGET_FILE}")" +TMP_FILE=$(mktemp) +CLEANUP_FILES+=("${TMP_FILE}") + +collect_keys() { + local dirs=("${@}") + for dir in "${dirs[@]}"; do + if [[ ! -d "${dir}" ]]; then continue; fi + # Use a glob to avoid parsing ls + for file in "${dir}"/*; do + [[ ! -e "${file}" ]] && continue + [[ ! -f "${file}" || ! -s "${file}" ]] && continue + if [[ -L "${file}" && "$(readlink "${file}")" == "/dev/null" ]]; then continue; fi + while read -r line; do + [[ -z "${line}" || "${line}" =~ ^[[:space:]]*# ]] && continue + # Use a specific delimiter that is unlikely to be in the key or filename + # If using tabs, ensure we only split on the first one in AWK + printf "%s\t%s\n" "$(basename "${file}")" "${line}" + done < "${file}" + done + done +} + +# Use a HEREDOC for the complex AWK script to avoid shell interpolation issues +MANAGED_BLOCK=$(collect_keys "${SOURCE_DIRS[@]}" | awk -F'\t' ' +{ + # Splitting on the first tab manually to be robust + tab_idx = index($0, "\t") + source = substr($0, 1, tab_idx - 1) + full_line = substr($0, tab_idx + 1) + + # Signature detection: all options + key type + key data + # (Excludes the comment at the end) + n = split(full_line, parts, " ") + sig = "" + for (i=1; i<=n; i++) { + sig = (sig == "" ? parts[i] : sig " " parts[i]) + # A key line is [options] [comment] + # We stop after the base64 part. Key types start with known prefixes. + if (parts[i] ~ /^(ssh-|ecdsa-|sk-)/ && i < n) { + sig = sig " " parts[i+1] + break + } + } + # Fallback if no key type found (should not happen with valid keys) + if (sig == "") sig = full_line + + if (!(sig in keys)) { + keys[sig] = full_line + order[++count] = sig + } + sources[sig] = (sources[sig] ? sources[sig] ", " : "") source +} +END { + for (i=1; i<=count; i++) { + sig = order[i] + print "# Source: " sources[sig] + print keys[sig] + } +}') + +MANUAL_KEYS="" +if [[ -f "${TARGET_FILE}" ]]; then + MANUAL_KEYS=$(awk -v begin="${BEGIN_MARKER}" -v end="${END_MARKER}" ' + BEGIN { inside=0 } + $0 == begin { inside=1; next } + $0 == end { inside=0; next } + !inside { print $0 } + ' "${TARGET_FILE}") +fi + +MANAGED_SIGS_TMP=$(mktemp) +echo "${MANAGED_BLOCK}" | awk '/^[^#]/ { + n = split($0, parts, " ") + sig = "" + for (i=1; i<=n; i++) { + sig = (sig == "" ? parts[i] : sig " " parts[i]) + if (parts[i] ~ /^(ssh-|ecdsa-|sk-)/ && i < n) { + sig = sig " " parts[i+1] + break + } + } + if (sig != "") print sig +}' > "${MANAGED_SIGS_TMP}" + +FINAL_MANUAL_KEYS=$(echo "${MANUAL_KEYS}" | awk -v sigs_file="${MANAGED_SIGS_TMP}" ' +BEGIN { + while ((getline line < sigs_file) > 0) { + managed[line] = 1 + } + close(sigs_file) +} +{ + if ($0 ~ /^[[:space:]]*$/ || $0 ~ /^[[:space:]]*#/) { + print $0 + next + } + n = split($0, parts, " ") + sig = "" + for (i=1; i<=n; i++) { + sig = (sig == "" ? parts[i] : sig " " parts[i]) + if (parts[i] ~ /^(ssh-|ecdsa-|sk-)/ && i < n) { + sig = sig " " parts[i+1] + break + } + } + if (!(sig in managed)) { + print $0 + } +}') +rm -f "${MANAGED_SIGS_TMP}" + +{ + if [[ -n "${MANAGED_BLOCK}" ]]; then + echo "${BEGIN_MARKER}" + echo "${MANAGED_BLOCK}" + echo "${END_MARKER}" + fi + echo "${FINAL_MANUAL_KEYS}" +} > "${TMP_FILE}" + +echo "Validating proposed changes..." +VALID=1 +while read -r line; do + [[ -z "${line}" || "${line}" =~ ^[[:space:]]*# ]] && continue + if ! echo "${line}" | ssh-keygen -l -f - >/dev/null 2>&1; then + echo "ERROR: Invalid SSH key detected: ${line}" >&2 + VALID=0 + fi +done < "${TMP_FILE}" + +if command -v authorized-keys-test >/dev/null 2>&1; then + if ! authorized-keys-test "${TMP_FILE}"; then + echo "ERROR: Proposed file failed authorized-keys-test." >&2 + VALID=0 + fi +fi + +if [[ ${VALID} -eq 0 ]]; then + echo "Validation failed. Aborting." >&2 + exit 1 +fi + +if [[ -f "${TARGET_FILE}" ]]; then + diff -u "${TARGET_FILE}" "${TMP_FILE}" || true +else + echo "Target file does not exist. Proposed content:" + cat "${TMP_FILE}" +fi + +if [[ ${DRY_RUN} -eq 1 ]]; then + echo "Dry run complete. No changes made." + exit 0 +fi + +if [[ -t 0 ]]; then + echo -n "Apply these changes to ${TARGET_FILE}? [y/N] " + read -r response +elif [[ ! -t 0 ]]; then + # Read from pipe or file if provided + if ! read -r response; then + echo "Non-interactive shell detected and no input provided. Aborting." + exit 1 + fi +fi + +if [[ "${response}" =~ ^([yY][eE][sS]|[yY])$ ]]; then + chmod 0600 "${TMP_FILE}" + mv "${TMP_FILE}" "${TARGET_FILE}" + echo "Changes applied successfully." +else + echo "Aborted." + exit 1 +fi From db2c02bd2d3731a5936a39230eb378a0db19d14c Mon Sep 17 00:00:00 2001 From: David Tomaschik Date: Tue, 21 Apr 2026 15:59:38 -0700 Subject: [PATCH 4/6] Cleanup --- bin/disk-benchmark | 11 +++++- bin/google-chrome-burp | 6 ++- bin/{ => linux}/autostart.py | 0 bin/{ => linux}/backup.sh | 0 bin/{ => linux}/checksec.sh | 0 bin/{ => linux}/i3lock.sh | 0 bin/{ => linux}/nvidia_hold.sh | 0 bin/{ => linux}/pactl_helper | 0 bin/{ => linux}/qdisc_span.sh | 0 bin/{ => linux}/remove-wine-associations | 0 bin/{ => linux}/setup/apt_proxy.sh | 0 bin/{ => linux}/setup/i3.sh | 0 bin/{ => linux}/setup/spicerandr.sh | 0 bin/{ => linux}/smart-copy-paste | 0 bin/{ => linux}/switch_virt.sh | 0 bin/screenshot.sh | 25 ++++++++++++ dotfiles/aliases | 48 ++++++++++++++---------- dotfiles/config/fish/conf.d/aliases.fish | 48 ++++++++++++++---------- dotfiles/config/fish/config.fish | 2 + dotfiles/shenv | 2 + dotfiles/tmux.conf | 7 ++-- dotfiles/zshrc | 2 + dotfiles/zshrc.d/alert.zsh | 11 +++++- dotfiles/zshrc.d/android.zsh | 11 ++++-- dotfiles/zshrc.d/assemble.zsh | 6 ++- dotfiles/zshrc.d/functions.zsh | 8 +++- dotfiles/zshrc.d/lesshighlight.zsh | 18 +++++++-- 27 files changed, 150 insertions(+), 55 deletions(-) rename bin/{ => linux}/autostart.py (100%) rename bin/{ => linux}/backup.sh (100%) rename bin/{ => linux}/checksec.sh (100%) rename bin/{ => linux}/i3lock.sh (100%) rename bin/{ => linux}/nvidia_hold.sh (100%) rename bin/{ => linux}/pactl_helper (100%) rename bin/{ => linux}/qdisc_span.sh (100%) rename bin/{ => linux}/remove-wine-associations (100%) rename bin/{ => linux}/setup/apt_proxy.sh (100%) rename bin/{ => linux}/setup/i3.sh (100%) rename bin/{ => linux}/setup/spicerandr.sh (100%) rename bin/{ => linux}/smart-copy-paste (100%) rename bin/{ => linux}/switch_virt.sh (100%) diff --git a/bin/disk-benchmark b/bin/disk-benchmark index ad46705..45d6ef9 100755 --- a/bin/disk-benchmark +++ b/bin/disk-benchmark @@ -12,8 +12,17 @@ fi trap "test -f ${FILENAME} && rm -f ${FILENAME}" EXIT +IOENGINE="libaio" +DIRECT=1 +if [ "$(uname)" = "Darwin" ]; then + IOENGINE="posixaio" + # macOS doesn't support O_DIRECT in the same way, but fio's direct=1 + # handles it via F_NOCACHE if supported. + DIRECT=1 +fi + fio --loops=5 --size=${BENCHMARK_SIZE} --filename=${FILENAME} \ - --stonewall --ioengine=libaio --direct=1 \ + --stonewall --ioengine=${IOENGINE} --direct=${DIRECT} \ --name=Seqread --bs=1m --rw=read \ --name=Seqwrite --bs=1m --rw=write \ --name=512Kread --bs=512k --rw=randread \ diff --git a/bin/google-chrome-burp b/bin/google-chrome-burp index 0aab1fd..d3d617d 100755 --- a/bin/google-chrome-burp +++ b/bin/google-chrome-burp @@ -1,6 +1,10 @@ #!/bin/bash CHROME_BINS="google-chrome-beta google-chrome" +if [ "$(uname)" = "Darwin" ]; then + CHROME_BINS="${CHROME_BINS} /Applications/Google\ Chrome\ Beta.app/Contents/MacOS/Google\ Chrome\ Beta /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome" +fi + for bin in ${CHROME_BINS} ; do if command -v ${bin} >/dev/null 2>&1 ; then CHROME=$(command -v ${bin}) @@ -18,4 +22,4 @@ export HOME=${HOME}/.chrome-pentest mkdir -p ${HOME} # Launch chrome for burp -exec ${CHROME} --user-data-dir=${HOME}/chrome-pentest --proxy-server=127.0.0.1:8080 +exec "${CHROME}" --user-data-dir=${HOME}/chrome-pentest --proxy-server=127.0.0.1:8080 diff --git a/bin/autostart.py b/bin/linux/autostart.py similarity index 100% rename from bin/autostart.py rename to bin/linux/autostart.py diff --git a/bin/backup.sh b/bin/linux/backup.sh similarity index 100% rename from bin/backup.sh rename to bin/linux/backup.sh diff --git a/bin/checksec.sh b/bin/linux/checksec.sh similarity index 100% rename from bin/checksec.sh rename to bin/linux/checksec.sh diff --git a/bin/i3lock.sh b/bin/linux/i3lock.sh similarity index 100% rename from bin/i3lock.sh rename to bin/linux/i3lock.sh diff --git a/bin/nvidia_hold.sh b/bin/linux/nvidia_hold.sh similarity index 100% rename from bin/nvidia_hold.sh rename to bin/linux/nvidia_hold.sh diff --git a/bin/pactl_helper b/bin/linux/pactl_helper similarity index 100% rename from bin/pactl_helper rename to bin/linux/pactl_helper diff --git a/bin/qdisc_span.sh b/bin/linux/qdisc_span.sh similarity index 100% rename from bin/qdisc_span.sh rename to bin/linux/qdisc_span.sh diff --git a/bin/remove-wine-associations b/bin/linux/remove-wine-associations similarity index 100% rename from bin/remove-wine-associations rename to bin/linux/remove-wine-associations diff --git a/bin/setup/apt_proxy.sh b/bin/linux/setup/apt_proxy.sh similarity index 100% rename from bin/setup/apt_proxy.sh rename to bin/linux/setup/apt_proxy.sh diff --git a/bin/setup/i3.sh b/bin/linux/setup/i3.sh similarity index 100% rename from bin/setup/i3.sh rename to bin/linux/setup/i3.sh diff --git a/bin/setup/spicerandr.sh b/bin/linux/setup/spicerandr.sh similarity index 100% rename from bin/setup/spicerandr.sh rename to bin/linux/setup/spicerandr.sh diff --git a/bin/smart-copy-paste b/bin/linux/smart-copy-paste similarity index 100% rename from bin/smart-copy-paste rename to bin/linux/smart-copy-paste diff --git a/bin/switch_virt.sh b/bin/linux/switch_virt.sh similarity index 100% rename from bin/switch_virt.sh rename to bin/linux/switch_virt.sh diff --git a/bin/screenshot.sh b/bin/screenshot.sh index 1c4df8c..fe87177 100755 --- a/bin/screenshot.sh +++ b/bin/screenshot.sh @@ -5,8 +5,14 @@ set -ue TOOLS="flameshot scrot" +if [ "$(uname)" = "Darwin" ]; then + TOOLS="screencapture ${TOOLS}" +fi + SCREENDIR=${SCREENDIR:-${HOME}/Pictures/Screenshots} SCROT_FORMAT="%F-%T.png" +# Filename for screencapture +FILE_NAME=$(date "+%Y-%m-%d-%H%M%S.png") function default_screenshot_command { for tool in ${TOOLS} ; do @@ -41,10 +47,29 @@ function scrot_full_capture { scrot "${SCREENDIR}/${SCROT_FORMAT}" } +function mac_capture { + local mode="${1}" + local target="${SCREENDIR}/${FILE_NAME}" + case "${mode}" in + region) + screencapture -i "${target}" + ;; + window) + screencapture -i -w "${target}" + ;; + full) + screencapture "${target}" + ;; + esac +} + case "${CMD}" in region|window|full) mkdir -p "${SCREENDIR}" case "${TOOL}" in + screencapture) + mac_capture "${CMD}" + ;; flameshot) case "${CMD}" in region|window) diff --git a/dotfiles/aliases b/dotfiles/aliases index 91104f5..ee4febb 100755 --- a/dotfiles/aliases +++ b/dotfiles/aliases @@ -1,9 +1,6 @@ # General aliases, should only be sourced in interactive shells # Try to keep in sync with ~/.config/fish/conf.d/aliases.fish -# Cryptsetup alias -alias luksFormat='cryptsetup luksFormat --type=luks2 --pbkdf-memory=2560000 --pbkdf=argon2id -i 15000 -s 512 -h sha256 -c aes-xts-plain64' - # Timestamp in a machine-sortable form alias tstamp="date '+%Y%m%d-%H%M%S'" @@ -13,12 +10,6 @@ alias mdcode="sed 's/^/ /'" # Intel format plz alias objdump="command objdump -M intel" -# Drop caches for swap issues -alias drop_caches="echo 3 | sudo /usr/bin/tee /proc/sys/vm/drop_caches" - -# dump acpi temperature -alias gettemp='printf "%02.2f\n" "$(cat /sys/class/thermal/thermal_zone0/temp)e-3"' - # get git working directory alias gitroot="git rev-parse --show-toplevel" @@ -31,20 +22,37 @@ alias ipy="ipython3 --no-banner" # Skip the header on bc alias bc="command bc -q" -# Get a decently readable df -alias dfh="df -h -x tmpfs -x devtmpfs -x squashfs -x fuse -x efivarfs" - # Clear the GPG agent alias clear-gpg-agent="echo RELOADAGENT | gpg-connect-agent" -# Battery details -alias bat-details='upower -i $(upower -e | grep battery)' - -# Nvidia refresh rate -alias nvidia-refresh-rate='nvidia-settings --display=:0 -q RefreshRate -t' - # Earthly ssh alias earthly='earthly --ssh-auth-sock ""' -# to clipboard -alias toclip='xclip -selection clipboard' +if [ "$(uname)" = "Linux" ]; then + # Cryptsetup alias + alias luksFormat='cryptsetup luksFormat --type=luks2 --pbkdf-memory=2560000 --pbkdf=argon2id -i 15000 -s 512 -h sha256 -c aes-xts-plain64' + + # Drop caches for swap issues + alias drop_caches="echo 3 | sudo /usr/bin/tee /proc/sys/vm/drop_caches" + + # dump acpi temperature + alias gettemp='printf "%02.2f\n" "$(cat /sys/class/thermal/thermal_zone0/temp)e-3"' + + # Get a decently readable df + alias dfh="df -h -x tmpfs -x devtmpfs -x squashfs -x fuse -x efivarfs" + + # Battery details + alias bat-details='upower -i $(upower -e | grep battery)' + + # Nvidia refresh rate + alias nvidia-refresh-rate='nvidia-settings --display=:0 -q RefreshRate -t' + + # to clipboard + alias toclip='xclip -selection clipboard' +elif [ "$(uname)" = "Darwin" ]; then + # Get a decently readable df + alias dfh="df -h" + + # to clipboard + alias toclip='pbcopy' +fi diff --git a/dotfiles/config/fish/conf.d/aliases.fish b/dotfiles/config/fish/conf.d/aliases.fish index 3e2ef70..176b188 100644 --- a/dotfiles/config/fish/conf.d/aliases.fish +++ b/dotfiles/config/fish/conf.d/aliases.fish @@ -1,6 +1,3 @@ -# Cryptsetup alias -alias luksFormat 'cryptsetup luksFormat --type=luks2 --pbkdf-memory=2560000 --pbkdf=argon2id -i 15000 -s 512 -h sha256 -c aes-xts-plain64' - # Timestamp in a machine-sortable form alias tstamp "date '+%Y%m%d-%H%M%S'" @@ -10,12 +7,6 @@ alias mdcode "sed 's/^/ /'" # Intel format plz alias objdump "command objdump -M intel" -# Drop caches for swap issues -alias drop_caches "echo 3 | sudo /usr/bin/tee /proc/sys/vm/drop_caches" - -# dump acpi temperature -alias gettemp 'printf "%02.2f\n" (cat /sys/class/thermal/thermal_zone0/temp)e-3' - # get git working directory alias gitroot "git rev-parse --show-toplevel" @@ -28,23 +19,40 @@ alias ipy "ipython3 --no-banner" # Skip the header on bc alias bc "command bc -q" -# Get a decently readable df -alias dfh "df -h -x tmpfs -x devtmpfs -x squashfs -x fuse -x efivarfs" - # Clear the GPG agent alias clear-gpg-agent "echo RELOADAGENT | gpg-connect-agent" -# Battery details -alias bat-details 'upower -i (upower -e | grep battery)' - -# Nvidia refresh rate -alias nvidia-refresh-rate 'nvidia-settings --display=:0 -q RefreshRate -t' - # Earthly ssh alias earthly 'earthly --ssh-auth-sock ""' -# to clipboard -alias toclip 'xclip -selection clipboard' +if test (uname) = "Linux" + # Cryptsetup alias + alias luksFormat 'cryptsetup luksFormat --type=luks2 --pbkdf-memory=2560000 --pbkdf=argon2id -i 15000 -s 512 -h sha256 -c aes-xts-plain64' + + # Drop caches for swap issues + alias drop_caches "echo 3 | sudo /usr/bin/tee /proc/sys/vm/drop_caches" + + # dump acpi temperature + alias gettemp 'printf "%02.2f\n" (cat /sys/class/thermal/thermal_zone0/temp)e-3' + + # Get a decently readable df + alias dfh "df -h -x tmpfs -x devtmpfs -x squashfs -x fuse -x efivarfs" + + # Battery details + alias bat-details 'upower -i (upower -e | grep battery)' + + # Nvidia refresh rate + alias nvidia-refresh-rate 'nvidia-settings --display=:0 -q RefreshRate -t' + + # to clipboard + alias toclip 'xclip -selection clipboard' +else if test (uname) = "Darwin" + # Get a decently readable df + alias dfh "df -h" + + # to clipboard + alias toclip 'pbcopy' +end # On some systems, bat is batcat if not command -v bat >/dev/null 2>&1 diff --git a/dotfiles/config/fish/config.fish b/dotfiles/config/fish/config.fish index 787f7e8..4230e88 100644 --- a/dotfiles/config/fish/config.fish +++ b/dotfiles/config/fish/config.fish @@ -34,4 +34,6 @@ end fish_add_path --move --path {$HOME}/bin if test (uname) = "Darwin" fish_add_path --move --path {$HOME}/bin/macos +else if test (uname) = "Linux" + fish_add_path --move --path {$HOME}/bin/linux end diff --git a/dotfiles/shenv b/dotfiles/shenv index 368dfd3..2afe628 100755 --- a/dotfiles/shenv +++ b/dotfiles/shenv @@ -194,6 +194,8 @@ if [ "$(uname)" = "Darwin" ] ; then export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-$TMPDIR/runtime-$(id -u)}" export XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" export PATH="${HOME}/bin/macos:${PATH}" +elif [ "$(uname)" = "Linux" ] ; then + export PATH="${HOME}/bin/linux:${PATH}" fi if test -e "$HOME/.localenv"; then diff --git a/dotfiles/tmux.conf b/dotfiles/tmux.conf index 1b89759..b9ced0e 100644 --- a/dotfiles/tmux.conf +++ b/dotfiles/tmux.conf @@ -45,7 +45,7 @@ set -g window-status-current-style fg=colour235,bg=colour33,bold set -g status-interval 60 set -g status-left-length 30 set -g status-left '/#h: #S/ ' -set -g status-right '#{?pane_title,/#{pane_title}/ ,}#(cut -d " " -f 1-3 /proc/loadavg)#[default] #[fg=colour166]%H:%M#[default]' +set -g status-right '#{?pane_title,/#{pane_title}/ ,}#(uptime | rev | cut -d":" -f1 | rev | sed s/,//g)#[default] #[fg=colour166]%H:%M#[default]' # Advanced mouse mode from http://tangledhelix.com/blog/2012/07/16/tmux-and-mouse-mode/ # Toggle mouse on @@ -65,8 +65,9 @@ bind M \ display 'Mouse: OFF' # tmux X clipboard integration -bind C-c run "tmux show-buffer | xsel -i -b" -bind C-v run "tmux set-buffer -- \"$(xsel -o -b)\"; tmux paste-buffer" +if-shell 'test "$(uname)" = "Darwin"' \ + 'bind C-c run "tmux show-buffer | pbcopy"; bind C-v run "tmux set-buffer -- \"$(pbpaste)\"; tmux paste-buffer"' \ + 'bind C-c run "tmux show-buffer | xsel -i -b"; bind C-v run "tmux set-buffer -- \"$(xsel -o -b)\"; tmux paste-buffer"' # List of plugins set -g @plugin 'tmux-plugins/tpm' diff --git a/dotfiles/zshrc b/dotfiles/zshrc index be26196..499aca0 100755 --- a/dotfiles/zshrc +++ b/dotfiles/zshrc @@ -289,6 +289,8 @@ fi PATH="${HOME}/bin:${PATH}" if [[ "$(uname)" == "Darwin" ]]; then PATH="${HOME}/bin/macos:${PATH}" +elif [[ "$(uname)" == "Linux" ]]; then + PATH="${HOME}/bin/linux:${PATH}" fi # Load any local settings diff --git a/dotfiles/zshrc.d/alert.zsh b/dotfiles/zshrc.d/alert.zsh index 7eab354..4ddd99e 100644 --- a/dotfiles/zshrc.d/alert.zsh +++ b/dotfiles/zshrc.d/alert.zsh @@ -18,8 +18,15 @@ alert() { icon="error" fi - # Send the notification with the executed command - notify-send --urgency=low -i "$icon" "Finished: '$@'" + if [ "$(uname)" = "Darwin" ]; then + # macOS notification + local title="Finished: '$*'" + local msg="Exit code: $ret" + osascript -e "display notification \"$msg\" with title \"$title\"" + else + # Send the notification with the executed command + notify-send --urgency=low -i "$icon" "Finished: '$@'" + fi # Return the original exit code return $ret diff --git a/dotfiles/zshrc.d/android.zsh b/dotfiles/zshrc.d/android.zsh index c8810b8..28687d1 100644 --- a/dotfiles/zshrc.d/android.zsh +++ b/dotfiles/zshrc.d/android.zsh @@ -1,7 +1,12 @@ -ANDROID_HOME=$HOME/Library/Android/sdk +if [ "$(uname)" = "Darwin" ]; then + ANDROID_HOME=$HOME/Library/Android/sdk +else + ANDROID_HOME=$HOME/Android/Sdk +fi -if test -d $ANDROID_HOME ; then - PATH=$PATH:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools +if test -d "${ANDROID_HOME}" ; then + export ANDROID_HOME + PATH="${PATH}:${ANDROID_HOME}/emulator:${ANDROID_HOME}/platform-tools" else unset ANDROID_HOME fi diff --git a/dotfiles/zshrc.d/assemble.zsh b/dotfiles/zshrc.d/assemble.zsh index 3677c91..8763972 100644 --- a/dotfiles/zshrc.d/assemble.zsh +++ b/dotfiles/zshrc.d/assemble.zsh @@ -6,7 +6,11 @@ if have_command nasm && have_command objdump ; then local TMPF=`mktemp` local bytes local byte - $NASM -f elf -o $TMPF $1 + local format="elf" + if [[ "$OSTYPE" == darwin* ]]; then + format="macho64" + fi + $NASM -f $format -o $TMPF $1 $OBJDUMP -M intel -d $TMPF | grep '^ ' | cut -f2 | while read -A bytes ; do for byte in $bytes ; do echo -n "\\\\x$byte" diff --git a/dotfiles/zshrc.d/functions.zsh b/dotfiles/zshrc.d/functions.zsh index 8254646..db8188f 100644 --- a/dotfiles/zshrc.d/functions.zsh +++ b/dotfiles/zshrc.d/functions.zsh @@ -1,5 +1,11 @@ function dumpenv { - tr '\0' '\n' < /proc/${1}/environ + if [ "$(uname)" = "Linux" ]; then + tr '\0' '\n' < /proc/${1}/environ + elif [ "$(uname)" = "Darwin" ]; then + # macOS doesn't have /proc, use ps instead. + # Note: this may truncate if environment is very large. + ps -p ${1} -wwwe -o command= | tr ' ' '\n' | grep '=' + fi } if test -x "/sbin/starship" ; then diff --git a/dotfiles/zshrc.d/lesshighlight.zsh b/dotfiles/zshrc.d/lesshighlight.zsh index 99c466a..eeff672 100644 --- a/dotfiles/zshrc.d/lesshighlight.zsh +++ b/dotfiles/zshrc.d/lesshighlight.zsh @@ -1,8 +1,20 @@ -test -f /usr/share/source-highlight/src-hilite-lesspipe.sh && \ +# Find src-hilite-lesspipe.sh +_SRCHILITE="" +for _p in /usr/share/source-highlight/src-hilite-lesspipe.sh /opt/homebrew/bin/src-hilite-lesspipe.sh /usr/local/bin/src-hilite-lesspipe.sh ; do + if [ -f "$_p" ] ; then + _SRCHILITE="$_p" + break + fi +done + +if [ -n "$_SRCHILITE" ] ; then function srcless { if [ $# -ne 1 ] ; then - echo "$0 " > /dev/stderr + echo "Usage: srcless " > /dev/stderr return 1 fi - /usr/share/source-highlight/src-hilite-lesspipe.sh $1 | less -R + "$_SRCHILITE" "$1" | less -R } +fi + +unset _SRCHILITE _p From bd2c2287cd7f2e34cefa41ac6b551143d5addcc3 Mon Sep 17 00:00:00 2001 From: David Tomaschik Date: Thu, 23 Apr 2026 10:34:21 -0700 Subject: [PATCH 5/6] Add more functions --- dotfiles/zshrc.d/util.zsh | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 dotfiles/zshrc.d/util.zsh diff --git a/dotfiles/zshrc.d/util.zsh b/dotfiles/zshrc.d/util.zsh new file mode 100644 index 0000000..9d0dc08 --- /dev/null +++ b/dotfiles/zshrc.d/util.zsh @@ -0,0 +1,56 @@ +# utility function to "open" a file +o() { + if [[ "$OSTYPE" == "darwin"* ]]; then + open "$@" + elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + xdg-open "$@" + else + echo "Unknown OS" + fi +} + +# Copy from stdin to the system clipboard +syscopy() { + if command -v pbcopy >/dev/null 2>&1; then + # macOS + pbcopy "$@" + elif command -v wl-copy >/dev/null 2>&1; then + # Linux Wayland + wl-copy "$@" + elif command -v xclip >/dev/null 2>&1; then + # Linux X11 + xclip -selection clipboard "$@" + elif command -v xsel >/dev/null 2>&1; then + # Linux X11 (alternative) + xsel --clipboard --input "$@" + elif command -v clip.exe >/dev/null 2>&1; then + # Windows WSL + clip.exe "$@" + else + echo "Error: No clipboard utility found. Please install pbcopy, wl-copy, xclip, or xsel." >&2 + return 1 + fi +} + +# Paste from the system clipboard to stdout +syspaste() { + if command -v pbpaste >/dev/null 2>&1; then + # macOS + pbpaste "$@" + elif command -v wl-paste >/dev/null 2>&1; then + # Linux Wayland + wl-paste "$@" + elif command -v xclip >/dev/null 2>&1; then + # Linux X11 + xclip -selection clipboard -o "$@" + elif command -v xsel >/dev/null 2>&1; then + # Linux X11 (alternative) + xsel --clipboard --output "$@" + elif command -v powershell.exe >/dev/null 2>&1; then + # Windows WSL + powershell.exe -noprofile -command Get-Clipboard "$@" + else + echo "Error: No clipboard utility found. Please install pbpaste, wl-paste, xclip, or xsel." >&2 + return 1 + fi +} From 4e72b9b18cd0cafa633be06425ca5445354d9fa5 Mon Sep 17 00:00:00 2001 From: David Tomaschik Date: Tue, 28 Apr 2026 15:20:24 -0700 Subject: [PATCH 6/6] Update gitconfig --- dotfiles/gitconfig | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dotfiles/gitconfig b/dotfiles/gitconfig index 1683559..0a2a125 100644 --- a/dotfiles/gitconfig +++ b/dotfiles/gitconfig @@ -24,9 +24,9 @@ last = log -1 HEAD # Thanks to # http://durdn.com/blog/2012/11/22/must-have-git-aliases-advanced-examples/ - logs = log --pretty=format:"%C(yellow)%h%Cred%d\\ %Creset%s%Cblue\\ [%cn]" --decorate + logs = log --pretty=format:"%C(yellow)%h%Cred%d %Creset%s%Cblue [%cn]" --decorate lg = log -p - ll = log --pretty=format:"%C(yellow)%h%Cred%d\\ %Creset%s%Cblue\\ [%cn]" --decorate --numstat + ll = log --pretty=format:"%C(yellow)%h%Cred%d %Creset%s%Cblue [%cn]" --decorate --numstat files = ls-files ls = ls-files lol = log --graph --pretty=format:'%C(yellow)%h%Creset %an: %s - %Creset %C(yellow)%d%Creset %Cblue(%cr)%Creset' --abbrev-commit --date=relative @@ -92,8 +92,12 @@ process = git-lfs filter-process [include] + path = ~/.gitconfig.d/aliases path = ~/.gitconfig.d/override path = ~/.gitconfig.d/local -[includeIf "gitdir:~/personal/"] +[includeIf "gitdir/i:~/personal/"] path = ~/.gitconfig.d/personal + +[rerere] + enabled = true