From bbda2f646b0d799a5c431f0b120b91a36519024a Mon Sep 17 00:00:00 2001 From: Jackson Holiday Wheeler Date: Thu, 26 Feb 2026 17:20:04 +0800 Subject: [PATCH 01/15] tmux: add session/window status bar and clean up status line Restore sessions list with labels and pipe separator on the left, custom window styling, and simplified status-right without battery. Co-Authored-By: Claude Sonnet 4.6 --- tmux.conf | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tmux.conf b/tmux.conf index aed7681..86f0139 100644 --- a/tmux.conf +++ b/tmux.conf @@ -18,6 +18,9 @@ set -s escape-time 50 # more scrollback set-option -g history-limit 3000 +# When a session is destroyed, switch to another session instead of detaching +set-option -g detach-on-destroy off + # split panes using | and - # (and open new panes & windows in current directory) bind | split-window -h -c "#{pane_current_path}" @@ -59,9 +62,16 @@ bind C-l send-keys 'C-l' # status bar colors set-option -g status-interval 1 set -g status-style bg='#44475a',fg='#bd93f9' -set -g status-left '#[bg=#f8f8f2]#[fg=#282a36]#{?client_prefix,#[bg=#ff79c6],}' +set -g status-left '#[fg=#90d0f2]Sessions: #(~/.tmux/scripts/sessions.sh)#[fg=#6272a4]│ #[fg=#90d0f2]Windows: ' +set -g status-left-length 60 set -g message-style bg='#44475a',fg='#8be9fd' +# Window list styling +set -g window-status-current-style fg='#bd93f9',bold +set -g window-status-current-format ' #I:#W ' +set -g window-status-style fg='#bfbfbf' +set -g window-status-format ' #I:#W ' + # Enable mouse mode set -g mouse on @@ -82,8 +92,9 @@ set -g @plugin 'tmux-plugins/tpm' set -g @plugin 'xamut/tmux-spotify' set -g @plugin 'tmux-plugins/tmux-resurrect' -# Status bar w/ time & battery percentage -set -g status-right '%H:%M:%S #[fg=blue]%a %d-%m-%y W%V |#{battery_status_bg} Batt #{battery_percentage} ' +# Status bar w/ time, date, week number, and session list +set -g status-right '%H:%M:%S #[fg=green]%a %Y-%m-%d W%V' +set -g status-right-length 120 # tmux resurrect set -g @resurrect-strategy-nvim 'session' From 45545f57d254a77fc165d3efb52c83fbb88bdd76 Mon Sep 17 00:00:00 2001 From: Jackson Holiday Wheeler Date: Thu, 26 Feb 2026 17:23:13 +0800 Subject: [PATCH 02/15] tmux: remove unused spotify and battery plugins Co-Authored-By: Claude Sonnet 4.6 --- tmux.conf | 2 -- 1 file changed, 2 deletions(-) diff --git a/tmux.conf b/tmux.conf index 86f0139..7a9ff3a 100644 --- a/tmux.conf +++ b/tmux.conf @@ -87,9 +87,7 @@ set -g bell-action none ## Plugins -set -g @plugin 'tmux-plugins/tmux-battery' set -g @plugin 'tmux-plugins/tpm' -set -g @plugin 'xamut/tmux-spotify' set -g @plugin 'tmux-plugins/tmux-resurrect' # Status bar w/ time, date, week number, and session list From 175b42715193b21ce78f9186dabbb873913719c0 Mon Sep 17 00:00:00 2001 From: Jackson Holiday Wheeler Date: Mon, 2 Mar 2026 19:48:37 +0800 Subject: [PATCH 03/15] feat: SSH environment parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the local shell environment reproducible on any remote server with a single bootstrap command, and fix local aliases that broke over SSH. Changes: - shell/common/env: use `command -v nvim` for EDITOR (works on remote); source ~/.shell/ssh when $SSH_CONNECTION is set - shell/common/aliases: guard pbcopy/pbpaste behind $WAYLAND_DISPLAY; guard cat=bat behind command -v bat; add `kitten ssh` alias (guarded) - shell/common/ssh (new): OSC52 pbcopy for clipboard-over-SSH, safe fallbacks for missing tools, unalias Hyprland-specific aliases - atuin/config.toml (new): auto_sync=false, enter_accept=true - install.conf.yaml: add ~/.shell/ssh, ~/.config/atuin symlinks + create dir - install-ssh.conf.yaml (new): SSH-safe dotbot config (no Wayland-specific tools — hypr, waybar, kitty, mako, swaylock, kmonad) - bin/bootstrap-remote.sh (new): idempotent one-command server bootstrap (packages → dotfiles → nvim AppImage → NormalNvim → starship → atuin → tpm) Co-Authored-By: Claude Sonnet 4.6 --- atuin/config.toml | 5 +++ bin/bootstrap-remote.sh | 79 +++++++++++++++++++++++++++++++++++++++++ install-ssh.conf.yaml | 48 +++++++++++++++++++++++++ install.conf.yaml | 5 +++ shell/common/aliases | 15 +++++--- shell/common/env | 10 +++--- shell/common/ssh | 12 +++++++ 7 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 atuin/config.toml create mode 100755 bin/bootstrap-remote.sh create mode 100644 install-ssh.conf.yaml create mode 100644 shell/common/ssh diff --git a/atuin/config.toml b/atuin/config.toml new file mode 100644 index 0000000..df19f7f --- /dev/null +++ b/atuin/config.toml @@ -0,0 +1,5 @@ +auto_sync = false +enter_accept = true + +[sync] +records = true diff --git a/bin/bootstrap-remote.sh b/bin/bootstrap-remote.sh new file mode 100755 index 0000000..2234b02 --- /dev/null +++ b/bin/bootstrap-remote.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOTFILES_REPO="https://github.com/jhwheeler/dotfiles.git" +DOTFILES_DIR="$HOME/projects/dotfiles" +NVIM_FORK="git://github.com/jhwheeler/NormalNvim.git" + +info() { printf "\n\033[0;34m==> %s\033[0m\n" "$*"; } +ok() { printf "\033[0;32m✓ %s\033[0m\n" "$*"; } + +# ── Package detection ───────────────────────────────────────────────────────── +if command -v apt-get &>/dev/null; then + PKG="sudo apt-get install -y" + PKG_UPDATE="sudo apt-get update -qq" +elif command -v dnf &>/dev/null; then + PKG="sudo dnf install -y" + PKG_UPDATE=":" +elif command -v pacman &>/dev/null; then + PKG="sudo pacman -S --noconfirm" + PKG_UPDATE=":" +else + echo "Unsupported package manager. Install git, tmux, curl, bat, fzf, ripgrep manually." >&2 + exit 1 +fi + +# ── Base packages ───────────────────────────────────────────────────────────── +info "Installing base packages" +$PKG_UPDATE +$PKG git curl tmux bat fzf ripgrep fd-find 2>/dev/null || \ + $PKG git curl tmux bat fzf ripgrep fd # different distro naming + +# ── Dotfiles ────────────────────────────────────────────────────────────────── +info "Setting up dotfiles" +mkdir -p "$HOME/projects" +if [[ ! -d "$DOTFILES_DIR/.git" ]]; then + git clone "$DOTFILES_REPO" "$DOTFILES_DIR" +fi +cd "$DOTFILES_DIR" +./install -c install-ssh.conf.yaml + +# ── Neovim (AppImage - distro-agnostic, always latest) ──────────────────────── +info "Installing Neovim" +mkdir -p "$HOME/.local/bin" +if ! command -v nvim &>/dev/null; then + NVIM_URL="https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.appimage" + curl -L "$NVIM_URL" -o "$HOME/.local/bin/nvim" + chmod +x "$HOME/.local/bin/nvim" + ok "Neovim installed" +else + ok "Neovim already installed ($(nvim --version | head -1))" +fi + +# ── NormalNvim config ───────────────────────────────────────────────────────── +info "Setting up Neovim config (NormalNvim fork)" +if [[ ! -d "$HOME/.config/nvim/.git" ]]; then + git clone "$NVIM_FORK" "$HOME/.config/nvim" +fi +nvim --headless "+Lazy sync" +qa 2>/dev/null || true +ok "Neovim plugins synced" + +# ── Starship ────────────────────────────────────────────────────────────────── +info "Installing Starship prompt" +if ! command -v starship &>/dev/null; then + curl -sS https://starship.rs/install.sh | sh -s -- --yes +fi + +# ── Atuin ───────────────────────────────────────────────────────────────────── +info "Installing Atuin (local history)" +if ! command -v atuin &>/dev/null; then + curl --proto '=https' --tlsv1.2 -LsSf https://setup.atuin.sh | sh +fi + +# ── TPM plugins ─────────────────────────────────────────────────────────────── +info "Installing tmux plugins" +"$HOME/.tmux/plugins/tpm/bin/install_plugins" 2>/dev/null || true + +echo "" +echo "Bootstrap complete! Start a new shell or run: source ~/.bashrc" +echo "Then attach tmux: tmux new -s main" diff --git a/install-ssh.conf.yaml b/install-ssh.conf.yaml new file mode 100644 index 0000000..93bfe6b --- /dev/null +++ b/install-ssh.conf.yaml @@ -0,0 +1,48 @@ +- defaults: + link: + relink: true + +- clean: ["~"] + +- create: + - ~/.config + - ~/.shell + - ~/.tmux/scripts + - ~/.config/atuin + +- link: + # Multiplexer + ~/.tmux.conf: tmux.conf + ~/.tmux/scripts/sessions.sh: tmux/scripts/sessions.sh + + # Shell + ~/.shell/env: shell/common/env + ~/.shell/functions: shell/common/functions + ~/.shell/login: shell/common/login + ~/.shell/ssh: shell/common/ssh + ~/.bash_aliases: shell/common/aliases + ~/.bash_profile: shell/bash/bash_profile + ~/.bashrc: shell/bash/bashrc + ~/.inputrc: shell/bash/inputrc + + # Prompt + ~/.config/starship.toml: starship.toml + + # Git + ~/.gitconfig: git/gitconfig + ~/.gitignore_global: git/gitignore_global + ~/.git_template: git/git_template + + # Atuin history + ~/.config/atuin/config.toml: atuin/config.toml + + # Lazygit + ~/.config/lazygit: lazygit + +- shell: + - [git submodule update --init --recursive dotbot, Updating dotbot] + - [touch ~/.shell/secrets, Creating secrets file] + - [ + "[ -d ~/.tmux/plugins/tpm ] || git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm", + Installing TPM, + ] diff --git a/install.conf.yaml b/install.conf.yaml index edde347..1d0fcd6 100644 --- a/install.conf.yaml +++ b/install.conf.yaml @@ -9,6 +9,7 @@ - ~/.shell - ~/.claude/hooks - ~/.tmux/scripts + - ~/.config/atuin - link: ~/.dotfiles: '' @@ -51,6 +52,7 @@ ~/.shell/env: shell/common/env ~/.shell/functions: shell/common/functions ~/.shell/login: shell/common/login + ~/.shell/ssh: shell/common/ssh ~/.bash_aliases: shell/common/aliases ~/.bash_profile: shell/bash/bash_profile ~/.bashrc: shell/bash/bashrc @@ -60,6 +62,9 @@ ~/.claude/settings.json: claude/settings.json ~/.claude/hooks/notify.sh: claude/hooks/notify.sh + # Atuin history + ~/.config/atuin/config.toml: atuin/config.toml + # Fuzzy finder ~/.fzf.bash: fzf/fzf.bash diff --git a/shell/common/aliases b/shell/common/aliases index 3e6fd12..a1134b0 100644 --- a/shell/common/aliases +++ b/shell/common/aliases @@ -15,9 +15,11 @@ alias wget='wget -c' # Colorize grep output alias grep='grep --color=auto' -# Copying/pasting to/from Wayland clipboard -alias pbcopy='wl-copy' -alias pbpaste='wl-paste' +# Copying/pasting to/from Wayland clipboard — only set when Wayland is running +if [[ -n "$WAYLAND_DISPLAY" ]]; then + alias pbcopy='wl-copy' + alias pbpaste='wl-paste' +fi # Common typos alias exti='exit' @@ -64,8 +66,11 @@ alias s='lynx_search' # How Do I? alias h='function hdi(){ howdoi $* -c -n 5; }; hdi' -# bat is cat with syntax highlighting -alias cat='bat' +# bat is cat with syntax highlighting — only alias if installed +command -v bat &>/dev/null && alias cat='bat' + +# kitty SSH integration — fixes colors/terminfo on all SSH connections from kitty +command -v kitten &>/dev/null && alias ssh='kitten ssh' # NPM alias n='pnpm run' diff --git a/shell/common/env b/shell/common/env index 834d79d..4112b3a 100644 --- a/shell/common/env +++ b/shell/common/env @@ -5,12 +5,10 @@ _path_prepend() { case ":$PATH:" in *":$1:"*) ;; *) export PATH="$1:$PATH" ;; esac; } _path_append() { case ":$PATH:" in *":$1:"*) ;; *) export PATH="$PATH:$1" ;; esac; } -# Use Vim -if [[ -n $SSH_CONNECTION ]]; then - export EDITOR='vim' -else - export EDITOR='nvim' -fi +command -v nvim &>/dev/null && export EDITOR='nvim' || export EDITOR='vim' + +# SSH-specific overrides (clipboard, etc.) +[[ -n "$SSH_CONNECTION" ]] && [ -f ~/.shell/ssh ] && source ~/.shell/ssh # Scripts _path_append "$HOME/scripts" diff --git a/shell/common/ssh b/shell/common/ssh new file mode 100644 index 0000000..3e478e9 --- /dev/null +++ b/shell/common/ssh @@ -0,0 +1,12 @@ +# SSH-specific shell overrides +# Sourced automatically when $SSH_CONNECTION is set (see shell/common/env) + +# OSC52 clipboard: copies to local clipboard through SSH tunnel +# (supported by kitty, wezterm, most modern terminals) +alias pbcopy='printf "\033]52;c;%s\a" "$(base64 -w0)"' + +# tools that may not exist on remote — safe fallbacks +command -v bat &>/dev/null || unalias cat 2>/dev/null + +# Prevent Hyprland-specific aliases from failing +unalias s 2>/dev/null # lynx_search (may not be installed) From 866c7cc3365746fc0f8ce26020da90c243173de6 Mon Sep 17 00:00:00 2001 From: Jackson Holiday Wheeler Date: Mon, 2 Mar 2026 20:06:40 +0800 Subject: [PATCH 04/15] tmux: add continuum for auto-save/restore across reboots Pairs with resurrect to persist session state (windows, panes, working dirs) every minute. On tmux server start after a reboot, sessions are automatically restored. Co-Authored-By: Claude Sonnet 4.6 --- tmux.conf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tmux.conf b/tmux.conf index 7a9ff3a..13ed9f4 100644 --- a/tmux.conf +++ b/tmux.conf @@ -89,6 +89,7 @@ set -g bell-action none ## Plugins set -g @plugin 'tmux-plugins/tpm' set -g @plugin 'tmux-plugins/tmux-resurrect' +set -g @plugin 'tmux-plugins/tmux-continuum' # Status bar w/ time, date, week number, and session list set -g status-right '%H:%M:%S #[fg=green]%a %Y-%m-%d W%V' @@ -97,6 +98,10 @@ set -g status-right-length 120 # tmux resurrect set -g @resurrect-strategy-nvim 'session' +# tmux continuum - auto-save every 1 min, auto-restore on start +set -g @continuum-restore 'on' +set -g @continuum-save-interval '1' + ## Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf) run -b '~/.tmux/plugins/tpm/tpm' From 4da384e574293506dac9c5e3387af2fb3bd42fad Mon Sep 17 00:00:00 2001 From: Jackson Holiday Wheeler Date: Mon, 2 Mar 2026 20:23:57 +0800 Subject: [PATCH 05/15] fix: force-link bashrc and add Go toolchain PATH on servers setup.sh writes a regular ~/.bashrc before dotbot runs, which dotbot's relink skips silently. force: true replaces the file with a symlink so the dotfiles bashrc (with aliases, starship, atuin, etc.) actually loads. Add /usr/local/go/bin to PATH conditionally so the Go toolchain stays accessible after the dotfiles bashrc supersedes setup.sh's PATH export. Co-Authored-By: Claude Sonnet 4.6 --- install-ssh.conf.yaml | 4 +++- shell/common/env | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/install-ssh.conf.yaml b/install-ssh.conf.yaml index 93bfe6b..c047217 100644 --- a/install-ssh.conf.yaml +++ b/install-ssh.conf.yaml @@ -22,7 +22,9 @@ ~/.shell/ssh: shell/common/ssh ~/.bash_aliases: shell/common/aliases ~/.bash_profile: shell/bash/bash_profile - ~/.bashrc: shell/bash/bashrc + ~/.bashrc: + path: shell/bash/bashrc + force: true ~/.inputrc: shell/bash/inputrc # Prompt diff --git a/shell/common/env b/shell/common/env index 4112b3a..e09d18c 100644 --- a/shell/common/env +++ b/shell/common/env @@ -20,6 +20,8 @@ export LYNX_CFG=$HOME/.config/lynx/lynx.cfg # Go export GOPATH="$HOME/go" _path_append "$GOPATH/bin" +# Go toolchain binary (system-installed, e.g. /usr/local/go on servers) +[ -d /usr/local/go/bin ] && _path_prepend /usr/local/go/bin # Deno export DENO_INSTALL="$HOME/.deno" From 38905ff123320d605f65f14e42b26483491aa256 Mon Sep 17 00:00:00 2001 From: Jackson Holiday Wheeler Date: Wed, 4 Mar 2026 21:26:24 +0800 Subject: [PATCH 06/15] feat: improve bootstrap-remote and polish SSH environment - Fix nvim appimage URL (https, arch-aware download) - Add lazygit install to bootstrap-remote - Expand EDITOR assignment for clarity - Note pbpaste OSC52 limitation - Bump tmux continuum save interval to 15 min --- bin/bootstrap-remote.sh | 19 +++++++++++++++++-- shell/common/env | 6 +++++- shell/common/ssh | 1 + tmux.conf | 4 ++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/bin/bootstrap-remote.sh b/bin/bootstrap-remote.sh index 2234b02..c7f3aad 100755 --- a/bin/bootstrap-remote.sh +++ b/bin/bootstrap-remote.sh @@ -3,7 +3,7 @@ set -euo pipefail DOTFILES_REPO="https://github.com/jhwheeler/dotfiles.git" DOTFILES_DIR="$HOME/projects/dotfiles" -NVIM_FORK="git://github.com/jhwheeler/NormalNvim.git" +NVIM_FORK="https://github.com/jhwheeler/NormalNvim.git" info() { printf "\n\033[0;34m==> %s\033[0m\n" "$*"; } ok() { printf "\033[0;32m✓ %s\033[0m\n" "$*"; } @@ -42,7 +42,8 @@ cd "$DOTFILES_DIR" info "Installing Neovim" mkdir -p "$HOME/.local/bin" if ! command -v nvim &>/dev/null; then - NVIM_URL="https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.appimage" + ARCH=$(uname -m) + NVIM_URL="https://github.com/neovim/neovim/releases/latest/download/nvim-linux-${ARCH}.appimage" curl -L "$NVIM_URL" -o "$HOME/.local/bin/nvim" chmod +x "$HOME/.local/bin/nvim" ok "Neovim installed" @@ -70,6 +71,20 @@ if ! command -v atuin &>/dev/null; then curl --proto '=https' --tlsv1.2 -LsSf https://setup.atuin.sh | sh fi +# ── Lazygit ─────────────────────────────────────────────────────────────────── +info "Installing Lazygit" +if ! command -v lazygit &>/dev/null; then + LG_ARCH=$(uname -m) + [[ "$LG_ARCH" == "aarch64" ]] && LG_ARCH="arm64" + LG_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | grep '"tag_name"' | sed 's/.*"v\([^"]*\)".*/\1/') + curl -Lo /tmp/lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_${LG_VERSION}_Linux_${LG_ARCH}.tar.gz" + tar -xf /tmp/lazygit.tar.gz -C "$HOME/.local/bin" lazygit + rm /tmp/lazygit.tar.gz + ok "Lazygit installed" +else + ok "Lazygit already installed ($(lazygit --version))" +fi + # ── TPM plugins ─────────────────────────────────────────────────────────────── info "Installing tmux plugins" "$HOME/.tmux/plugins/tpm/bin/install_plugins" 2>/dev/null || true diff --git a/shell/common/env b/shell/common/env index e09d18c..5d7cd58 100644 --- a/shell/common/env +++ b/shell/common/env @@ -5,7 +5,11 @@ _path_prepend() { case ":$PATH:" in *":$1:"*) ;; *) export PATH="$1:$PATH" ;; esac; } _path_append() { case ":$PATH:" in *":$1:"*) ;; *) export PATH="$PATH:$1" ;; esac; } -command -v nvim &>/dev/null && export EDITOR='nvim' || export EDITOR='vim' +if command -v nvim &>/dev/null; then + export EDITOR='nvim' +else + export EDITOR='vim' +fi # SSH-specific overrides (clipboard, etc.) [[ -n "$SSH_CONNECTION" ]] && [ -f ~/.shell/ssh ] && source ~/.shell/ssh diff --git a/shell/common/ssh b/shell/common/ssh index 3e478e9..03bd6cd 100644 --- a/shell/common/ssh +++ b/shell/common/ssh @@ -3,6 +3,7 @@ # OSC52 clipboard: copies to local clipboard through SSH tunnel # (supported by kitty, wezterm, most modern terminals) +# pbpaste via OSC52 is not widely supported — skipped intentionally alias pbcopy='printf "\033]52;c;%s\a" "$(base64 -w0)"' # tools that may not exist on remote — safe fallbacks diff --git a/tmux.conf b/tmux.conf index 13ed9f4..a6cbb16 100644 --- a/tmux.conf +++ b/tmux.conf @@ -98,9 +98,9 @@ set -g status-right-length 120 # tmux resurrect set -g @resurrect-strategy-nvim 'session' -# tmux continuum - auto-save every 1 min, auto-restore on start +# tmux continuum - auto-save every 15 min, auto-restore on start set -g @continuum-restore 'on' -set -g @continuum-save-interval '1' +set -g @continuum-save-interval '15' ## Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf) From 7c8a30656bf50a4c970e23ccc24c5f01a7b28e8a Mon Sep 17 00:00:00 2001 From: Jackson Holiday Wheeler Date: Wed, 4 Mar 2026 21:49:55 +0800 Subject: [PATCH 07/15] feat: SSH-aware tmux prefix and theme for nested sessions Use C-a locally (Dracula purple/blue) and C-s over SSH (Catppuccin Mocha peach/yellow) so nested tmux sessions have distinct prefixes and visually distinct status bars. Add SSH label to remote status-right, disable XON/XOFF flow control for C-s, and auto-name SSH sessions as ssh- with stale-client detach on reconnect. --- shell/common/env | 8 ++++++- shell/common/ssh | 3 +++ tmux.conf | 49 +++++++++++++++++++++++++++++++--------- tmux/scripts/sessions.sh | 14 ++++++++++-- 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/shell/common/env b/shell/common/env index 5d7cd58..eb16b31 100644 --- a/shell/common/env +++ b/shell/common/env @@ -83,5 +83,11 @@ export NODE_OPTIONS="--network-family-autoselection-attempt-timeout=500" # Use tmux if available (interactive shells only, skip in Claude Code) if [[ -z "$CLAUDECODE" ]] && [ -z "$TMUX" ] && [[ $- == *i* ]]; then - tmux attach -t default || tmux new -s default + if [[ -n "$SSH_CONNECTION" ]]; then + _tmux_session="ssh-$(hostname -s)" + tmux attach -d -t "$_tmux_session" || tmux new -s "$_tmux_session" + unset _tmux_session + else + tmux attach -t default || tmux new -s default + fi fi diff --git a/shell/common/ssh b/shell/common/ssh index 03bd6cd..87f69a2 100644 --- a/shell/common/ssh +++ b/shell/common/ssh @@ -1,6 +1,9 @@ # SSH-specific shell overrides # Sourced automatically when $SSH_CONNECTION is set (see shell/common/env) +# Disable XON/XOFF flow control so C-s (tmux prefix over SSH) isn't swallowed +stty -ixon + # OSC52 clipboard: copies to local clipboard through SSH tunnel # (supported by kitty, wezterm, most modern terminals) # pbpaste via OSC52 is not widely supported — skipped intentionally diff --git a/tmux.conf b/tmux.conf index a6cbb16..32b6ae1 100644 --- a/tmux.conf +++ b/tmux.conf @@ -3,9 +3,6 @@ set-environment -g PATH "/usr/local/bin:/bin:/usr/bin" # reload config file (change file location to your the tmux.conf you want to use) bind r source-file ~/.tmux.conf -# remap C-b to C-a -set -g prefix C-a - # tmux messages are displayed for 4 seconds set -g display-time 4000 @@ -59,17 +56,44 @@ bind -n 'C-\' if-shell "$is_vim" "send-keys C-\\\\" "select-pane -l" bind C-l send-keys 'C-l' -# status bar colors +# --- Prefix & Theme: SSH-aware --- +# Local: C-a prefix, Dracula (purple/blue) +# SSH: C-s prefix, Catppuccin Mocha (peach/yellow) + +# Prefix +if-shell 'test -n "$SSH_CONNECTION"' \ + 'set -g prefix C-s; unbind C-a; bind C-s send-prefix' \ + 'set -g prefix C-a; unbind C-b; bind C-a send-prefix' + +# Status bar interval set-option -g status-interval 1 -set -g status-style bg='#44475a',fg='#bd93f9' -set -g status-left '#[fg=#90d0f2]Sessions: #(~/.tmux/scripts/sessions.sh)#[fg=#6272a4]│ #[fg=#90d0f2]Windows: ' + +# Theme colors +if-shell 'test -n "$SSH_CONNECTION"' \ + 'set -g status-style bg=#313244,fg=#fab387' \ + 'set -g status-style bg=#44475a,fg=#bd93f9' + +if-shell 'test -n "$SSH_CONNECTION"' \ + 'set -g status-left "#[fg=#f9e2af]Sessions: #(~/.tmux/scripts/sessions.sh)#[fg=#585b70]│ #[fg=#f9e2af]Windows: "' \ + 'set -g status-left "#[fg=#90d0f2]Sessions: #(~/.tmux/scripts/sessions.sh)#[fg=#6272a4]│ #[fg=#90d0f2]Windows: "' + set -g status-left-length 60 -set -g message-style bg='#44475a',fg='#8be9fd' + +if-shell 'test -n "$SSH_CONNECTION"' \ + 'set -g message-style bg=#313244,fg=#f5c2e7' \ + 'set -g message-style bg=#44475a,fg=#8be9fd' # Window list styling -set -g window-status-current-style fg='#bd93f9',bold +if-shell 'test -n "$SSH_CONNECTION"' \ + 'set -g window-status-current-style fg=#fab387,bold' \ + 'set -g window-status-current-style fg=#bd93f9,bold' + set -g window-status-current-format ' #I:#W ' -set -g window-status-style fg='#bfbfbf' + +if-shell 'test -n "$SSH_CONNECTION"' \ + 'set -g window-status-style fg=#a6adc8' \ + 'set -g window-status-style fg=#bfbfbf' + set -g window-status-format ' #I:#W ' # Enable mouse mode @@ -91,8 +115,11 @@ set -g @plugin 'tmux-plugins/tpm' set -g @plugin 'tmux-plugins/tmux-resurrect' set -g @plugin 'tmux-plugins/tmux-continuum' -# Status bar w/ time, date, week number, and session list -set -g status-right '%H:%M:%S #[fg=green]%a %Y-%m-%d W%V' +# Status bar w/ time, date, week number (+ SSH label on remote) +if-shell 'test -n "$SSH_CONNECTION"' \ + 'set -g status-right "#[fg=#fab387,bold]SSH #[fg=default]%H:%M:%S #[fg=green]%a %Y-%m-%d W%V"' \ + 'set -g status-right "%H:%M:%S #[fg=green]%a %Y-%m-%d W%V"' + set -g status-right-length 120 # tmux resurrect diff --git a/tmux/scripts/sessions.sh b/tmux/scripts/sessions.sh index c3f727e..434d616 100755 --- a/tmux/scripts/sessions.sh +++ b/tmux/scripts/sessions.sh @@ -1,9 +1,19 @@ #!/bin/bash current=$(tmux display-message -p '#S') + +# SSH: peach/Catppuccin Mocha; Local: purple/Dracula +if [ -n "$SSH_CONNECTION" ]; then + active_color="#fab387" + inactive_color="#a6adc8" +else + active_color="#bd93f9" + inactive_color="#bfbfbf" +fi + tmux list-sessions -F '#S' | while read -r s; do if [ "$s" = "$current" ]; then - printf "#[fg=#bd93f9,bold]%s " "$s" + printf "#[fg=%s,bold]%s " "$active_color" "$s" else - printf "#[fg=#bfbfbf,nobold]%s " "$s" + printf "#[fg=%s,nobold]%s " "$inactive_color" "$s" fi done From f2c374e7fcfb6252144caef4dff4945b7afbea9a Mon Sep 17 00:00:00 2001 From: Jackson Holiday Wheeler Date: Wed, 4 Mar 2026 21:53:21 +0800 Subject: [PATCH 08/15] feat: auto-clone nvim config on dotbot install Clone NormalNvim fork into ~/.config/nvim if not already present, so remote machines get the full nvim setup after running ./install. --- install.conf.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install.conf.yaml b/install.conf.yaml index 1d0fcd6..de38ff3 100644 --- a/install.conf.yaml +++ b/install.conf.yaml @@ -73,3 +73,5 @@ - [touch ~/.shell/secrets, Creating local secrets file (not tracked by git)] # If you haven't already, run prefix-I with tmux open after doing this to reload tmux and install the plugins - [ if ! test -d "$HOME/.tmux/plugins/tpm"; then git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm; fi ] + # Clone nvim config (NormalNvim fork) — lazy.nvim bootstraps plugins on first launch + - [ if ! test -d "$HOME/.config/nvim"; then git clone https://github.com/jhwheeler/NormalNvim.git ~/.config/nvim; fi ] From 32c59d355925a16a9739bbb43581e6fe9d471f65 Mon Sep 17 00:00:00 2001 From: Jackson Holiday Wheeler Date: Wed, 4 Mar 2026 21:58:33 +0800 Subject: [PATCH 09/15] fix: use prefix + hjkl for pane nav over SSH Outer tmux intercepts C-h/j/k/l before they reach the inner session. Over SSH, bind pane navigation to prefix + h/j/k/l instead so C-s h/j/k/l works in nested tmux. --- tmux.conf | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/tmux.conf b/tmux.conf index 32b6ae1..482eea9 100644 --- a/tmux.conf +++ b/tmux.conf @@ -36,24 +36,31 @@ is_vim="ps -o state= -o comm= -t '#{pane_tty}' \ is_fzf="ps -o state= -o comm= -t '#{pane_tty}' \ | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?fzf$'" -bind -n C-h run "($is_vim && tmux send-keys C-h) || \ - tmux select-pane -L" +# Local: bind -n (no prefix) so C-h/j/k/l works seamlessly +# SSH: prefix + h/j/k/l since outer tmux intercepts C-h/j/k/l +if-shell 'test -n "$SSH_CONNECTION"' \ + 'bind h run "($is_vim && tmux send-keys C-h) || tmux select-pane -L"' \ + 'bind -n C-h run "($is_vim && tmux send-keys C-h) || tmux select-pane -L"' -bind -n C-j run "($is_vim & tmux send-keys C-j) || \ - ($is_fzf && tmux send-keys C-j) || \ - tmux select-pane -D" +if-shell 'test -n "$SSH_CONNECTION"' \ + 'bind j run "($is_vim && tmux send-keys C-j) || ($is_fzf && tmux send-keys C-j) || tmux select-pane -D"' \ + 'bind -n C-j run "($is_vim && tmux send-keys C-j) || ($is_fzf && tmux send-keys C-j) || tmux select-pane -D"' -bind -n C-k run "($is_vim && tmux send-keys C-k) || \ - ($is_fzf && tmux send-keys C-k) || \ - tmux select-pane -U" +if-shell 'test -n "$SSH_CONNECTION"' \ + 'bind k run "($is_vim && tmux send-keys C-k) || ($is_fzf && tmux send-keys C-k) || tmux select-pane -U"' \ + 'bind -n C-k run "($is_vim && tmux send-keys C-k) || ($is_fzf && tmux send-keys C-k) || tmux select-pane -U"' -bind -n C-l run "($is_vim && tmux send-keys C-l) || \ - tmux select-pane -R" +if-shell 'test -n "$SSH_CONNECTION"' \ + 'bind l run "($is_vim && tmux send-keys C-l) || tmux select-pane -R"' \ + 'bind -n C-l run "($is_vim && tmux send-keys C-l) || tmux select-pane -R"' -bind -n 'C-\' if-shell "$is_vim" "send-keys C-\\\\" "select-pane -l" +if-shell 'test -n "$SSH_CONNECTION"' \ + 'bind \\ if-shell "$is_vim" "send-keys C-\\\\" "select-pane -l"' \ + 'bind -n "C-\\" if-shell "$is_vim" "send-keys C-\\\\" "select-pane -l"' -# prefix + C-l to clear console (b/c C-l is remapped above) -bind C-l send-keys 'C-l' +# prefix + C-l to clear console (b/c C-l is remapped above locally) +if-shell 'test -z "$SSH_CONNECTION"' \ + 'bind C-l send-keys "C-l"' # --- Prefix & Theme: SSH-aware --- From 99194fcf538f2c31e8118fb954ae4787002f3595 Mon Sep 17 00:00:00 2001 From: Jackson Holiday Wheeler Date: Wed, 4 Mar 2026 22:05:52 +0800 Subject: [PATCH 10/15] fix: close terminal on grouped session exit instead of mirroring Set detach-on-destroy on per grouped session and use exec so the terminal closes cleanly rather than falling through to the base "default" session. --- shell/common/env | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shell/common/env b/shell/common/env index eb16b31..1d59392 100644 --- a/shell/common/env +++ b/shell/common/env @@ -88,6 +88,11 @@ if [[ -z "$CLAUDECODE" ]] && [ -z "$TMUX" ] && [[ $- == *i* ]]; then tmux attach -d -t "$_tmux_session" || tmux new -s "$_tmux_session" unset _tmux_session else - tmux attach -t default || tmux new -s default + # Create base session if needed, then attach a grouped session for an + # independent view (so multiple terminals aren't mirrored). + # detach-on-destroy on (session-level) ensures closing this terminal + # doesn't fall through to the base session. exec closes the terminal on detach. + tmux has-session -t default 2>/dev/null || tmux new-session -d -s default + exec tmux new-session -t default \; set-option detach-on-destroy on fi fi From e0db52be7c6f496541df769c5132f5b04d4b5948 Mon Sep 17 00:00:00 2001 From: Jackson Holiday Wheeler Date: Wed, 4 Mar 2026 22:07:20 +0800 Subject: [PATCH 11/15] fix: give each terminal its own tmux session to avoid mirroring Replace grouped session approach with simple new-session per terminal. Each gets an auto-named independent session; prefix+s to switch between. --- shell/common/env | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/shell/common/env b/shell/common/env index 1d59392..5517023 100644 --- a/shell/common/env +++ b/shell/common/env @@ -88,11 +88,8 @@ if [[ -z "$CLAUDECODE" ]] && [ -z "$TMUX" ] && [[ $- == *i* ]]; then tmux attach -d -t "$_tmux_session" || tmux new -s "$_tmux_session" unset _tmux_session else - # Create base session if needed, then attach a grouped session for an - # independent view (so multiple terminals aren't mirrored). - # detach-on-destroy on (session-level) ensures closing this terminal - # doesn't fall through to the base session. exec closes the terminal on detach. - tmux has-session -t default 2>/dev/null || tmux new-session -d -s default - exec tmux new-session -t default \; set-option detach-on-destroy on + # Each terminal gets its own session (no mirroring). + # detach-on-destroy off lets you cycle to other sessions if you want. + exec tmux new-session fi fi From f2d4c0c9994434f5ca1059e1bed8edcd148264cb Mon Sep 17 00:00:00 2001 From: Jackson Holiday Wheeler Date: Wed, 4 Mar 2026 22:22:42 +0800 Subject: [PATCH 12/15] feat: auto-install SCM Breeze on dotbot install --- install.conf.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install.conf.yaml b/install.conf.yaml index de38ff3..59c1108 100644 --- a/install.conf.yaml +++ b/install.conf.yaml @@ -75,3 +75,5 @@ - [ if ! test -d "$HOME/.tmux/plugins/tpm"; then git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm; fi ] # Clone nvim config (NormalNvim fork) — lazy.nvim bootstraps plugins on first launch - [ if ! test -d "$HOME/.config/nvim"; then git clone https://github.com/jhwheeler/NormalNvim.git ~/.config/nvim; fi ] + # SCM Breeze (git aliases, numbered shortcuts) + - [ if ! test -d "$HOME/.scm_breeze"; then git clone https://github.com/scmbreeze/scm_breeze.git ~/.scm_breeze && ~/.scm_breeze/install.sh; fi ] From 786db30ed54287ff5c5fd69b3a6bd9774d3f90ba Mon Sep 17 00:00:00 2001 From: jhwheeler Date: Wed, 4 Mar 2026 14:29:17 +0000 Subject: [PATCH 13/15] fix: use direct select-pane for SSH pane navigation The run-based bindings with $is_vim failed silently over SSH because the variable isn't available in the spawned shell, causing tmux to fall back to the default last-window binding on prefix+l. Prefix-based bindings don't need vim detection since the prefix already disambiguates. Co-Authored-By: Claude Opus 4.6 --- tmux.conf | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tmux.conf b/tmux.conf index 482eea9..8763c0e 100644 --- a/tmux.conf +++ b/tmux.conf @@ -36,26 +36,26 @@ is_vim="ps -o state= -o comm= -t '#{pane_tty}' \ is_fzf="ps -o state= -o comm= -t '#{pane_tty}' \ | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?fzf$'" -# Local: bind -n (no prefix) so C-h/j/k/l works seamlessly -# SSH: prefix + h/j/k/l since outer tmux intercepts C-h/j/k/l +# Local: bind -n (no prefix) so C-h/j/k/l works seamlessly with vim detection +# SSH: prefix + h/j/k/l (no vim detection needed — prefix disambiguates) if-shell 'test -n "$SSH_CONNECTION"' \ - 'bind h run "($is_vim && tmux send-keys C-h) || tmux select-pane -L"' \ + 'bind h select-pane -L' \ 'bind -n C-h run "($is_vim && tmux send-keys C-h) || tmux select-pane -L"' if-shell 'test -n "$SSH_CONNECTION"' \ - 'bind j run "($is_vim && tmux send-keys C-j) || ($is_fzf && tmux send-keys C-j) || tmux select-pane -D"' \ + 'bind j select-pane -D' \ 'bind -n C-j run "($is_vim && tmux send-keys C-j) || ($is_fzf && tmux send-keys C-j) || tmux select-pane -D"' if-shell 'test -n "$SSH_CONNECTION"' \ - 'bind k run "($is_vim && tmux send-keys C-k) || ($is_fzf && tmux send-keys C-k) || tmux select-pane -U"' \ + 'bind k select-pane -U' \ 'bind -n C-k run "($is_vim && tmux send-keys C-k) || ($is_fzf && tmux send-keys C-k) || tmux select-pane -U"' if-shell 'test -n "$SSH_CONNECTION"' \ - 'bind l run "($is_vim && tmux send-keys C-l) || tmux select-pane -R"' \ + 'bind l select-pane -R' \ 'bind -n C-l run "($is_vim && tmux send-keys C-l) || tmux select-pane -R"' if-shell 'test -n "$SSH_CONNECTION"' \ - 'bind \\ if-shell "$is_vim" "send-keys C-\\\\" "select-pane -l"' \ + 'bind \\ select-pane -l' \ 'bind -n "C-\\" if-shell "$is_vim" "send-keys C-\\\\" "select-pane -l"' # prefix + C-l to clear console (b/c C-l is remapped above locally) From 406ddff5fdc530e49a17512f84070a1a5b153347 Mon Sep 17 00:00:00 2001 From: jhwheeler Date: Mon, 23 Mar 2026 02:46:30 +0000 Subject: [PATCH 14/15] add fzf file opener, openclaw completions, PATH updates --- shell/bash/bash_profile | 4 ++++ shell/bash/bashrc | 13 +++++++++++++ shell/common/env | 13 +++++++------ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/shell/bash/bash_profile b/shell/bash/bash_profile index c4bfe35..dc5d079 100644 --- a/shell/bash/bash_profile +++ b/shell/bash/bash_profile @@ -6,3 +6,7 @@ # Shared login tasks (atuin, Hyprland auto-start, etc.) [ -f ~/.shell/login ] && . ~/.shell/login + +# bun +export BUN_INSTALL="$HOME/.bun" +export PATH="$BUN_INSTALL/bin:$PATH" diff --git a/shell/bash/bashrc b/shell/bash/bashrc index c78fe97..7ce3d66 100644 --- a/shell/bash/bashrc +++ b/shell/bash/bashrc @@ -61,3 +61,16 @@ fi # Atuin (shell history) [[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh command -v atuin &>/dev/null && eval "$(atuin init bash)" + +if command -v wt >/dev/null 2>&1; then eval "$(command wt config shell init bash)"; fi +export PATH="$HOME/.npm-global/bin:$PATH" + +# OpenClaw Completion +source "/home/rheo/.openclaw/completions/openclaw.bash" +export PATH="$HOME/projects/scripts:$PATH" + +# Open file in $EDITOR via fzf +fe() { + local file + file=$(fzf --preview 'cat {}') && ${EDITOR:-vim} "$file" +} diff --git a/shell/common/env b/shell/common/env index 5517023..6da6a3a 100644 --- a/shell/common/env +++ b/shell/common/env @@ -5,12 +5,6 @@ _path_prepend() { case ":$PATH:" in *":$1:"*) ;; *) export PATH="$1:$PATH" ;; esac; } _path_append() { case ":$PATH:" in *":$1:"*) ;; *) export PATH="$PATH:$1" ;; esac; } -if command -v nvim &>/dev/null; then - export EDITOR='nvim' -else - export EDITOR='vim' -fi - # SSH-specific overrides (clipboard, etc.) [[ -n "$SSH_CONNECTION" ]] && [ -f ~/.shell/ssh ] && source ~/.shell/ssh @@ -44,6 +38,13 @@ _path_append "$HOME/.local/bin" # opencode _path_prepend "$HOME/.opencode/bin" +# Set EDITOR after PATH is fully configured so nvim can be found +if command -v nvim &>/dev/null; then + export EDITOR='nvim' +else + export EDITOR='vim' +fi + # NVM export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" From 1eed23559c302e8c99b879cc16836b206ad745f6 Mon Sep 17 00:00:00 2001 From: jhwheeler Date: Fri, 27 Mar 2026 02:14:14 +0000 Subject: [PATCH 15/15] Fix bash prompt wrapping in git repos --- shell/bash/bashrc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shell/bash/bashrc b/shell/bash/bashrc index 7ce3d66..bd604c6 100644 --- a/shell/bash/bashrc +++ b/shell/bash/bashrc @@ -44,9 +44,11 @@ else pointerC="${txtwht}" normalC="${txtrst}" - # get name of branch and wrap in parens + # Get the current branch name without color codes so PS1 length stays correct. gitBranch() { - git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1)/' + local branch + branch=$(git branch --show-current --no-color 2>/dev/null) || return + [[ -n "$branch" ]] && printf '(%s)' "$branch" } # build the prompt export PS1="${pathC}\w ${gitC}\$(gitBranch) ${pointerC}\$${normalC} "