Compare commits

...
12 Commits
Author SHA1 Message Date
jeremy 7acdb761d6 zsh: detect eza --hyperlink capability, fix 'cannot take a value' on Debian's older eza
Newer eza (clap) treats --hyperlink as taking an optional value, so the
oh-my-zsh plugin's bare --hyperlink eats the next path arg (la / ->
invalid value '/'). The previous fix glued --hyperlink=always to avoid
that. But Debian ships an older eza where --hyperlink is a pure boolean
that rejects a value, so --hyperlink=always errors with 'Flag
--hyperlink cannot take a value'.

Detect the installed eza's behavior via 'eza --help | grep <WHEN>':
- newer eza: use --hyperlink=always (glued, avoids path-eating)
- older eza: use bare --hyperlink (boolean, doesn't eat tokens so the
  original bug doesn't apply)
Apply the chosen form to every eza alias. Both paths keep hyperlinks on
and paths working.
2026-07-29 12:15:06 -04:00
jeremy 18af4de0c0 zsh: add github binary fallbacks for bat/zoxide/delta (fixes RHEL/Alma without EPEL/cargo)
On RHEL-family distros without EPEL enabled and without cargo, the PM
loop fails for bat/zoxide/git-delta and there was no fallback, so they
ended up skipped/FAIL. Now, after the PM/cargo attempts fail, download
the latest static release tarball from GitHub instead, matching the
existing pattern for eza/fastfetch/starship/lazydocker.

- Add _gh_latest_tag(): resolve latest release tag from the
  /releases/latest redirect (no GitHub API, no rate limits)
- Add _gh_install_bin(): build the asset URL from a versioned template,
  download, extract, and cp the named binary into ~/.local/bin
- Handle per-repo tag conventions: bat/zoxide tags are 'v0.x' while
  delta tags are bare '0.x'; keep the raw tag for the download path and
  strip the optional 'v' only for asset-filename substitution
- Add target_musl/target_gnu per-arch (x86_64/aarch64/armv7l)
- bat/zoxide use static musl builds (no glibc deps); delta ships gnu-only
- Verbose 'gh installing <bin> (<tag> from <repo>)' / FAIL lines
- Bump ZSHRC_BOOTSTRAP_VERSION 3 -> 4 so existing boxes re-run the
  bootstrap and pick up the new fallbacks

Verified end-to-end: bat 0.26.1, zoxide 0.10.0, delta 0.19.2 all
download, extract, install, and run from a temp HOME.
2026-07-29 12:11:31 -04:00
jeremy 0281b06154 zsh: make bootstrap verbose + timeout PM availability checks (fix hang on AlmaLinux)
The bootstrap silently suppressed all output, so the user saw the banner
then an apparent hang. Root cause on AlmaLinux: the cold 'dnf list
available' metadata check could stall for minutes on a slow/unreachable
dnf mirror, and nothing was visible or bounded.

- Wrap apt/dnf/pacman availability checks in 'timeout 20' so a slow
  mirror fails through to the next installer instead of hanging
- Remove >/dev/null 2>&1 suppression so real installs (cargo compiles,
  dnf progress) are visible as progress, not an apparent hang
- Emit a one-line status per step:
    ok <tool>      already present
    ...  <tool>    checking package managers
    dnf  <tool>    not available (or metadata check timed out)
    curl <tool>    installing from binary
    cargo <tool>   (may take a few minutes to compile)
    git  <name>    cloning
    FAIL <tool>    could not install; skipping
- Apply the same timeout+verbose pattern to the git-delta block
- Add progress + FAIL lines to the fastfetch/eza/github clones
2026-07-29 11:55:14 -04:00
jeremy 9343715696 zsh: print a user-facing banner when the bootstrap fires
The bootstrap silently installs tools for ~30s+ on a version bump or
first login, which looked like an unexplained hang. Now prints a
banner explaining what is happening, that it may take a minute, and
that subsequent runs are a no-op. The box is built dynamically so the
right edge stays aligned regardless of the version string length.
Also clarifies the trigger comment in zshrc.
2026-07-29 11:35:43 -04:00
jeremy 10caebad29 zsh: add zoxide/bat/delta, starship prompt config, extra history options; rewrite README
Bootstrap (zshrc-bootstrap.zsh):
- Add bat and zoxide to the cross-distro install loop
- Add git-delta install block (package is 'git-delta' on every PM but
  binary is 'delta', mirroring the starship/lazydocker pattern)
- Debian/Ubuntu compat: symlink batcat->bat and fdfind->fd into
  ~/.local/bin so zshrc integrations work under both names
- Bump ZSHRC_BOOTSTRAP_VERSION 2 -> 3 so existing machines re-run the
  bootstrap and auto-install the new tools

zshrc:
- Add setopts HIST_VERIFY, EXTENDED_HISTORY, AUTO_PUSHD,
  PUSHD_IGNORE_DUPS, PUSHD_SILENT
- Self-configuring tool integrations (all behind command -v guards):
  - zoxide: eval $(zoxide init zsh) for frecent 'z'/'zi' dir jumping
  - bat: alias cat->bat, BAT_THEME, colored MANPAGER via bat
  - delta: GIT_PAGER=delta for prettier git diff/log/show
- Extend the interactive auto-update block to also sync starship.toml
  to ~/.config/starship.toml (overwrite-if-changed, validated the
  same way as zshrc); keep the reproducible/self-configuring design
- Inverted hyperlink fix already landed in prior commit; no change
  here

starship.toml (new):
- Two-line prompt: directory + git status + cmd duration on line 1,
  arrow (red on error) on line 2; Nerd Font symbols, SSH host/user,
  Python/Node/Rust/Go/Java/Docker context blocks
- Validated clean with starship 1.21.1 (uses [nodejs], not [node])

README.md:
- Full rewrite: documents the design philosophy, all files, deps,
  integrations, history options, aliases, keybindings, the eza
  hyperlink workaround rationale, auto-update behavior, per-machine
  overrides, env vars, and helper functions
2026-07-29 11:30:49 -04:00
jeremy c4a804e760 zsh: fix eza --hyperlink eating path argument (e.g. 'la /')
The oh-my-zsh eza plugin adds a bare --hyperlink flag as the last tail
option. eza's --hyperlink takes an optional value, so clap swallows the
next token (the path) -> 'la /' fails with 'invalid value /'.

- Drop the 'hyperlink' zstyle so the plugin stops adding the bare flag
- After oh-my-zsh loads, append the glued --hyperlink=always form to
  each eza alias; the glued form cannot eat the next token, so paths
  work and hyperlinks stay on
2026-07-29 11:09:12 -04:00
jeremy eed5879a25 zsh: harden bootstrap against network failures and unsafe paths
- Add --connect-timeout 5 --max-time 30 to all curl calls (starship,
  lazydocker, fastfetch, eza) so an unreachable/slow host aborts instead
  of hanging the new shell during a bootstrap version bump
- Add -f/--fail to curl calls so a 502/HTML error page is not saved as
  a tarball or piped into sh
- Guard fastfetch extract: only cp/chmod when $ffdir is non-empty,
  preventing cp of '/usr/bin/fastfetch' and glob against '/' when
  curl/tar fails
- Only run fastfetch/eza copy steps when both curl and tar succeed;
  clean up temp tarballs in both success and failure paths
- git clone now uses GIT_TERMINAL_PROMPT=0 and http.lowSpeedLimit/
  lowSpeedTime so a stalled clone aborts after ~10s of no progress
  and an unexpected credential prompt cannot block startup
- Drop invalid -y flag from 'dnf list available' availability check
  (it is a global dnf option, not a list subcommand arg) which could
  cause the check to fail and skip the dnf path
2026-07-29 10:53:17 -04:00
jeremy e01bb35043 zsh: add --fail to curl calls, fix tmux config re-download every startup
- Add -f/--fail to all curl calls so HTTP error pages (502/404) are not
  written to temp files and mistaken for valid config/scripts
- Fix tmux check at line 49: was testing the directory path with -f
  (always true when dir exists) causing tmux.conf to re-download on
  every shell start; now checks for the actual conf files in both
  tmux and byobu config dirs
2026-07-29 10:52:52 -04:00
jeremy 52bca5ce38 zsh: add --max-time to curl calls to prevent hanging on slow connections 2026-07-29 10:35:18 -04:00
jeremy b805357f53 netbird-install-update: add --help option with usage, options, and examples 2026-07-29 10:34:46 -04:00
jeremy d2ca6c6787 Add pacman support for Arch Linux systems
- Detect netbird installed via pacman
- Remove netbird packages using pacman -Rns when replacing with binary install
- Update README to document pacman support
2026-07-25 22:49:39 -04:00
jeremy 376fac0665 netbird-install-update: add install/update script with remote deployment and auto-update timers
- Detects Netbird installation method (binary vs package manager)
- Replaces package manager installs with binary for consistency
- Supports single-host and multi-host SSH deployment
- Adds systemd timer for daily auto-updates with persistent scheduling
- Handles sudo password automation via --password flag or hosts file
- Connection-safe updates using background execution to survive SSH disconnects
- Fixes SELinux contexts and binary permissions automatically
- Provides detailed deployment summaries with per-host action tracking
- Includes SSH timeout handling to prevent hanging on unreachable hosts
- Color-coded output with icons for better readability
2026-07-25 22:42:44 -04:00
9 changed files with 1462 additions and 49 deletions
+1
View File
@@ -11,6 +11,7 @@ A collection of utility scripts for Linux desktop and server administration.
| [`docker-stacks-backup/`](docker-stacks-backup/) | Backup & restore Docker Compose stacks — named volumes, bind mounts, pre/post hooks, retention, integrity verification | | [`docker-stacks-backup/`](docker-stacks-backup/) | Backup & restore Docker Compose stacks — named volumes, bind mounts, pre/post hooks, retention, integrity verification |
| [`docker-stacks-update/`](docker-stacks-update/) | Discover running Compose stacks, pull latest images, and restart only those that changed | | [`docker-stacks-update/`](docker-stacks-update/) | Discover running Compose stacks, pull latest images, and restart only those that changed |
| [`input-remapper-switcher/`](input-remapper-switcher/) | Auto-switch [input-remapper](https://github.com/sezanzeb/input-remapper) presets per-application via Hyprland socket events | | [`input-remapper-switcher/`](input-remapper-switcher/) | Auto-switch [input-remapper](https://github.com/sezanzeb/input-remapper) presets per-application via Hyprland socket events |
| [`netbird-install-update/`](netbird-install-update/) | Install, update, and manage Netbird across single or multiple Linux hosts — detects install method, replaces package-manager installs with binary, supports SSH remote deployment, auto-update timers, and sudo password automation |
| [`proxmox-backup/`](proxmox-backup/) | Backup & restore Proxmox VE configuration (local or over SSH) with integrity verification | | [`proxmox-backup/`](proxmox-backup/) | Backup & restore Proxmox VE configuration (local or over SSH) with integrity verification |
| [`proxmox-cloudimg/`](proxmox-cloudimg/) | Download, customize, and create Proxmox VE VM templates from official cloud images | | [`proxmox-cloudimg/`](proxmox-cloudimg/) | Download, customize, and create Proxmox VE VM templates from official cloud images |
| [`zsh/`](zsh/) | Zsh configuration (`.zshrc`), dependency bootstrap installer, and themed tmux config | | [`zsh/`](zsh/) | Zsh configuration (`.zshrc`), dependency bootstrap installer, and themed tmux config |
+445
View File
@@ -0,0 +1,445 @@
# Netbird Install/Update Script
A comprehensive bash script for installing, updating, and managing Netbird across Linux systems. Supports local execution, single-host remote deployment, and multi-host fleet management.
## Features
- **Smart Installation**: Automatically detects and handles different Netbird installation methods
- **Package Manager Replacement**: Converts package manager installations to binary installs for better control
- **Connection-Safe Updates**: Background update process survives SSH disconnections during Netbird service restarts
- **Auto-Update Timer**: Systemd timer for daily automatic updates with persistent scheduling
- **Remote Deployment**: Deploy to single hosts or entire fleets via SSH with timeout handling
- **Detailed Summary**: Multi-host deployments show which hosts succeeded/failed and what action was performed
- **SELinux Compatible**: Automatically fixes SELinux contexts on RHEL/Fedora systems
- **No Artifacts**: Clean execution with no leftover files or logs
## Requirements
- Linux operating system
- Root/sudo access
- curl
- SSH client (for remote deployment)
- Internet connection to download Netbird
## Installation
Make the script executable:
```bash
chmod +x netbird-install-update.sh
```
## Usage
### Local Execution
Install or update Netbird on the local machine:
```bash
./netbird-install-update.sh
```
Install with automatic daily updates:
```bash
./netbird-install-update.sh --timer
```
Install with custom update schedule:
```bash
./netbird-install-update.sh --timer --time "03:00"
```
### Single Host Remote Deployment
Deploy to a remote host via SSH:
```bash
./netbird-install-update.sh --ssh user@hostname
```
Deploy with sudo password (non-interactive):
```bash
./netbird-install-update.sh --ssh user@hostname --password "your_sudo_password"
```
Deploy with auto-update timer enabled:
```bash
./netbird-install-update.sh --ssh user@hostname --timer
```
Deploy with custom update schedule:
```bash
./netbird-install-update.sh --ssh user@hostname --timer --time "03:00"
```
### Multi-Host Fleet Deployment
Deploy to multiple hosts using a hosts file:
```bash
./netbird-install-update.sh --file hosts.txt
```
Deploy with default sudo password for all hosts:
```bash
./netbird-install-update.sh --file hosts.txt --password "your_sudo_password"
```
Deploy with auto-update timer to all hosts:
```bash
./netbird-install-update.sh --file hosts.txt --timer
```
Deploy with custom update schedule to all hosts:
```bash
./netbird-install-update.sh --file hosts.txt --timer --time "03:00"
```
#### Hosts File Format
Create a text file with one hostname or IP address per line. Empty lines and comments (starting with `#`) are ignored.
**Optional: Include sudo password**
You can optionally include the sudo password after the hostname (separated by space). Host-specific passwords override the default password provided via `--password`.
```bash
# Production servers (with host-specific sudo passwords)
prod-web-01.example.com MySudoPassword123
prod-web-02.example.com MySudoPassword456
prod-db-01.example.com MySudoPassword789
# Development servers (will use default password from --password flag)
dev-app-01.example.com
dev-app-02.example.com
# IP addresses also work
192.168.1.100
10.0.0.50 MyPassword
```
**Password Precedence**:
1. Host-specific password in hosts file (highest priority)
2. Default password from `--password` flag
3. Interactive sudo prompt (if no password provided)
**Security Warning**: Storing passwords in plain text is not recommended for production environments. Consider using SSH key authentication with passwordless sudo instead.
**Requirements**:
- If a password is provided (via hosts file or `--password` flag), `sshpass` must be installed on the local machine
- If no password is provided, you'll be prompted to enter the sudo password interactively for each host
See `hosts.example` for a template.
## Command-Line Options
| Option | Description |
|--------|-------------|
| `--ssh <host>` | Deploy to a single remote host via SSH |
| `--file <path>` | Deploy to multiple hosts listed in a file |
| `--hosts-file <path>` | Alias for `--file` |
| `--timer` | Install systemd timer for auto-updates |
| `--time <schedule>` | Set update schedule (default: "daily") |
| `--password, -p <password>` | Default sudo password for remote hosts |
The `--time` option accepts systemd calendar event format. Examples:
- `daily` - Run once per day at midnight (default)
- `03:00` - Run at 3:00 AM daily
- `Mon,Fri 02:30` - Run at 2:30 AM on Monday and Friday
- `hourly` - Run every hour
- `weekly` - Run once per week
The `--password` option provides a default sudo password for remote deployments. This can be overridden per-host in the hosts file.
## How It Works
### Installation Detection
The script detects three installation states:
1. **Not Installed**: Netbird binary not found in PATH
2. **Package Manager Install**: Installed via apt, yum, dnf, zypper, pacman, or rpm-ostree
3. **Binary Install**: Installed via the official curl-based installer
Detection is performed by:
- Checking `/etc/netbird/install.conf` (authoritative source)
- Querying package managers (dpkg, rpm)
- Falling back to binary install detection
### Installation Flow
**Fresh Installation (Not Installed)**:
1. Downloads official Netbird install script
2. Runs binary-only installation (no GUI, no package manager)
3. Ensures proper ownership and permissions
4. Fixes SELinux contexts if applicable
**Package Manager Replacement**:
1. Downloads official Netbird install script
2. Schedules background replacement (3-second delay)
3. Stops Netbird service
4. Removes package manager installation
5. Runs binary installation
6. Ensures proper ownership, permissions, and SELinux contexts
**Binary Update**:
1. Compares current version with latest release
2. If outdated, schedules background update (3-second delay)
3. Runs official update process
4. Ensures proper ownership, permissions, and SELinux contexts
### Connection-Safe Updates
When updating over SSH, the script uses a detached background process to avoid disconnection issues:
```bash
setsid bash -c 'sleep 3 && [update commands]' </dev/null >/var/log/netbird-update.log 2>&1 &
```
The 3-second delay allows the SSH session to complete cleanly before the Netbird service is stopped and restarted. The update continues even if your connection drops.
### Auto-Update Timer
When `--timer` is specified, the script creates a systemd timer that:
- Runs on a configurable schedule (default: daily at midnight with random 0-10 minute delay)
- Uses `Persistent=true` to catch up on missed runs after system boot
- Logs output to `/var/log/netbird-update.log`
- Automatically updates existing timers if already installed
The schedule can be customized with the `--time` option. Examples:
- `--time "daily"` - Run once per day at midnight (default)
- `--time "03:00"` - Run at 3:00 AM daily
- `--time "Mon,Fri 02:30"` - Run at 2:30 AM on Monday and Friday
- `--time "hourly"` - Run every hour
- `--time "weekly"` - Run once per week
The timer consists of two systemd units:
**netbird-update.service**:
```ini
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'curl -fsSL https://pkgs.netbird.io/install.sh | UPDATE_NETBIRD=true sh && chown root:root /usr/bin/netbird 2>/dev/null || true && chmod +x /usr/bin/netbird 2>/dev/null || true && restorecon -v /usr/bin/netbird 2>/dev/null || true'
```
**netbird-update.timer**:
```ini
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
```
## Logging
All background operations log to `/var/log/netbird-update.log`:
- Package manager replacement operations
- In-place updates
- Systemd timer executions
Check the log to monitor update progress or troubleshoot issues:
```bash
sudo tail -f /var/log/netbird-update.log
```
## Troubleshooting
### Netbird Service Fails to Start
If the Netbird service fails with "Permission denied" errors:
```bash
# Check binary permissions
ls -la /usr/bin/netbird
# Fix ownership and permissions
sudo chown root:root /usr/bin/netbird
sudo chmod +x /usr/bin/netbird
# Fix SELinux context (RHEL/Fedora)
sudo restorecon -v /usr/bin/netbird
# Restart service
sudo systemctl restart netbird
```
### Check Update Status
```bash
# Check current version
netbird version
# Check service status
sudo systemctl status netbird
# View update logs
sudo cat /var/log/netbird-update.log
# Check timer status (if installed)
systemctl status netbird-update.timer
```
### Manual Update
If you need to manually trigger an update:
```bash
./netbird-install-update.sh
```
Or use the official Netbird update method:
```bash
curl -fsSL https://pkgs.netbird.io/install.sh | sudo UPDATE_NETBIRD=true sh
```
### Timer Management
```bash
# Check timer status
systemctl status netbird-update.timer
# View timer schedule
systemctl list-timers netbird-update.timer
# Manually trigger timer
sudo systemctl start netbird-update.service
# Disable timer
sudo systemctl disable --now netbird-update.timer
# Remove timer completely
sudo systemctl disable --now netbird-update.timer
sudo rm /etc/systemd/system/netbird-update.service
sudo rm /etc/systemd/system/netbird-update.timer
sudo systemctl daemon-reload
```
## Why Binary Install?
The script converts package manager installations to binary installs because:
1. **Better Control**: Direct binary management allows precise version control
2. **Simpler Updates**: Binary updates don't require package manager configuration
3. **Consistency**: Same installation method across all Linux distributions
4. **Official Support**: Binary install is the officially recommended method by Netbird
## Remote Deployment Details
When deploying remotely:
1. Script copies itself to the remote host via `scp`
2. Executes remotely with `sudo` via `ssh -t` (terminal allocation for sudo password prompt)
3. Passes through additional flags (like `--timer`)
4. Cleans up the remote copy after execution
The remote deployment continues even if the local script is interrupted.
**Important Requirements**:
- **SSH Authentication**: Remote deployment requires SSH key authentication. Password-based SSH authentication is not supported.
- **Sudo Access**: The script will prompt for the sudo password on each remote host. For fully automated deployments, configure passwordless sudo on the remote hosts (e.g., add a sudoers rule like `username ALL=(ALL) NOPASSWD: /bin/bash`).
### SSH Timeout and Error Handling
The script includes a 30-second timeout for SSH connections to prevent hanging on unreachable hosts. If a host is unreachable or the connection times out, the script will:
- Mark the host as failed in the deployment summary
- Continue to the next host in the list
- Report the failure at the end
### Deployment Summary
After multi-host deployment, the script provides a detailed summary showing:
- **Total successful and failed deployments**
- **List of successful hosts** with the action performed:
- `fresh install` - Netbird was not installed and has been installed
- `pkg replacement` - Netbird was installed via package manager and converted to binary install
- `updated` - Netbird was updated to a newer version
- `up to date` - Netbird was already at the latest version
- **List of failed hosts** that could not be reached or had errors
Example output:
```
╔═══════════════════════════════════════════════════════════════╗
║ ✓ Deployment Summary
╠═══════════════════════════════════════════════════════════════╣
║ ✓ Successful: 3
║ ✗ Failed: 1
╚═══════════════════════════════════════════════════════════════╝
Successful Hosts:
✓ host-01.example.com (updated)
✓ host-02.example.com (fresh install)
✓ host-03.example.com (up to date)
Failed Hosts:
✗ host-04.example.com
```
## Exit Codes
- `0`: Success
- `1`: Error (file not found, SSH failure, etc.)
## Compatibility
### Tested Package Managers
- apt (Debian/Ubuntu)
- yum (RHEL/CentOS 7)
- dnf (Fedora/RHEL 8+/CentOS 8+)
- zypper (openSUSE)
- pacman (Arch Linux)
- rpm-ostree (Fedora Silverblue)
### SELinux Support
Automatically handles SELinux contexts on systems with SELinux enabled (RHEL, Fedora, CentOS). The `restorecon` command is safely ignored on systems without SELinux.
## Security Considerations
- Script requires root/sudo access
- Downloads official Netbird installer from `https://pkgs.netbird.io/install.sh`
- All operations are logged to `/var/log/netbird-update.log`
- No credentials are stored or transmitted
- Temporary files are cleaned up automatically
## Limitations
- Linux only (no macOS or Windows support)
- Requires SSH key authentication for remote deployment (password authentication is not supported)
- Background updates cannot be cancelled once started
## License
This script is provided as-is for managing Netbird installations. Netbird itself is subject to its own license terms.
## Contributing
Contributions are welcome! Please ensure:
- Script syntax is valid (`bash -n script.sh`)
- Changes are tested on multiple Linux distributions
- SELinux compatibility is maintained
- No artifacts are left behind after execution
## Support
For Netbird-specific issues, refer to the [official Netbird documentation](https://docs.netbird.io).
For script-specific issues, check the troubleshooting section or review the logs at `/var/log/netbird-update.log`.
+17
View File
@@ -0,0 +1,17 @@
# Example hosts file for netbird-install-update.sh
# Format: hostname [sudo_password]
# One host per line, comments start with #
# Host-specific passwords override --password flag
# Production servers (with host-specific sudo passwords)
prod-web-01.jeremy.skynet MySudoPassword123
prod-web-02.jeremy.skynet MySudoPassword123
prod-db-01.jeremy.skynet MySudoPassword123
# Development servers (will use --password flag or prompt interactively)
dev-app-01.jeremy.skynet
dev-app-02.jeremy.skynet
# You can also use IP addresses
# 192.168.1.100
# 10.0.0.50 MyPassword
+478
View File
@@ -0,0 +1,478 @@
#!/usr/bin/env bash
set -euo pipefail
# Color codes for output formatting
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m' # No Color
# Icons
ICON_SUCCESS="✓"
ICON_ERROR="✗"
ICON_INFO=""
ICON_WARNING="⚠"
ICON_ARROW="→"
ICON_DOWNLOAD="⬇"
ICON_UPLOAD="⬆"
ICON_INSTALL="📦"
ICON_UPDATE="🔄"
ICON_DEPLOY="🚀"
ICON_TIMER="⏰"
ICON_CHECK="🔍"
ICON_CONFIG="⚙"
ICON_NETWORK="🌐"
ICON_VERSION="📋"
ICON_LOG="📝"
ICON_HOST="🖥"
SSH_HOST=""
HOSTS_FILE=""
INSTALL_TIMER=false
TIMER_TIME="daily"
DEFAULT_PASSWORD=""
REMOTE_ARGS=()
SSH_TIMEOUT=30
# Arrays to track deployment results
declare -a SUCCESS_HOSTS=()
declare -a FAILED_HOSTS=()
declare -a HOST_ACTIONS=()
# Helper functions for formatted output
print_header() {
echo -e "\n${BLUE}${BOLD}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}${BOLD}$1${NC}"
echo -e "${BLUE}${BOLD}╚═══════════════════════════════════════════════════════════════╝${NC}\n"
}
print_info() {
echo -e " ${BLUE}${ICON_INFO}${NC} $1"
}
print_success() {
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $1"
}
print_error() {
echo -e " ${RED}${ICON_ERROR}${NC} $1"
}
print_warning() {
echo -e " ${YELLOW}${ICON_WARNING}${NC} $1"
}
print_status() {
echo -e " ${CYAN}${ICON_ARROW}${NC} $1"
}
print_step() {
echo -e " ${DIM}${NC} $1"
}
print_usage() {
cat <<EOF
Usage: netbird-install-update.sh [options]
Install, update, and manage Netbird across single or multiple Linux hosts.
Options:
-h, --help Show this help message and exit
--ssh <host> Deploy to a single host via SSH
--hosts-file, --file <f> Deploy to multiple hosts from a file
--timer Install a systemd auto-update timer
--time <schedule> Timer schedule (systemd OnCalendar format, default: daily)
-p, --password <pass> Sudo password for remote hosts (used with --ssh or --hosts-file)
Hosts file format (one per line):
hostname_or_ip [sudo_password]
Examples:
# Local install/update
sudo ./netbird-install-update.sh
# Deploy to a single host via SSH
./netbird-install-update.sh --ssh user@hostname
# Deploy to multiple hosts from a file
./netbird-install-update.sh --hosts-file hosts.txt
# Deploy with auto-update timer
./netbird-install-update.sh --ssh user@hostname --timer --time "weekly"
# Multi-host deployment with default sudo password
./netbird-install-update.sh --hosts-file hosts.txt --password "mypass"
EOF
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
print_usage
;;
--ssh)
SSH_HOST="$2"
shift 2
;;
--hosts-file|--file)
HOSTS_FILE="$2"
shift 2
;;
--timer)
INSTALL_TIMER=true
REMOTE_ARGS+=("--timer")
shift
;;
--time)
TIMER_TIME="$2"
REMOTE_ARGS+=("--time" "$2")
shift 2
;;
--password|-p)
DEFAULT_PASSWORD="$2"
shift 2
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
deploy_to_host() {
local host="$1"
local sudo_pass="${2:-}"
echo -e "${CYAN}${BOLD}┌─────────────────────────────────────────────────────────────┐${NC}"
echo -e "${CYAN}${BOLD}${NC} ${ICON_DEPLOY} ${YELLOW}${host}${NC}"
echo -e "${CYAN}${BOLD}└─────────────────────────────────────────────────────────────┘${NC}"
REMOTE_SCRIPT="/tmp/.netbird-install-update-$$"
REMOTE_ACTION_FILE="/tmp/.netbird-action-$$"
LOCAL_ACTION_FILE="/tmp/.netbird-deploy-action-$$"
print_step "Copying script..."
if ! scp -o ConnectTimeout=$SSH_TIMEOUT -o BatchMode=yes "$0" "$host:$REMOTE_SCRIPT" 2>&1; then
print_error "Failed to copy script to $host (timeout or connection error)"
return 1
fi
print_step "Executing script..."
# Build SSH command based on whether we have a password
if [ -n "$sudo_pass" ]; then
# Write password to remote temp file using printf to handle special characters
# Use base64 to safely transfer the password
local encoded_pass=$(echo -n "$sudo_pass" | base64)
if ! ssh -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no "$host" "echo '$encoded_pass' | base64 -d > /tmp/.netbird-pass-$$ && chmod 600 /tmp/.netbird-pass-$$" 2>&1; then
print_error "Failed to setup password file on $host"
return 1
fi
# Execute script with sudo, reading password from file
if ! ssh -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no "$host" "sudo -S bash $REMOTE_SCRIPT ${REMOTE_ARGS[*]} < /tmp/.netbird-pass-$$" 2>&1; then
print_error "Failed to execute script on $host"
ssh -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no "$host" "rm -f $REMOTE_SCRIPT $REMOTE_ACTION_FILE /tmp/.netbird-pass-$$" 2>/dev/null || true
return 1
fi
# Clean up password file
ssh -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no "$host" "rm -f /tmp/.netbird-pass-$$" 2>/dev/null || true
else
# Interactive sudo with terminal
if ! ssh -o ConnectTimeout=$SSH_TIMEOUT -o BatchMode=yes -t "$host" "sudo bash $REMOTE_SCRIPT ${REMOTE_ARGS[*]}" 2>&1; then
print_error "Failed to execute script on $host"
ssh -o ConnectTimeout=$SSH_TIMEOUT -o BatchMode=yes "$host" "rm -f $REMOTE_SCRIPT $REMOTE_ACTION_FILE" 2>/dev/null || true
return 1
fi
fi
# Read the action from the remote action file and write to local action file
if action_output=$(ssh -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no "$host" "cat $REMOTE_ACTION_FILE 2>/dev/null" < /dev/null 2>&1); then
echo "$action_output" > "$LOCAL_ACTION_FILE"
else
echo "unknown" > "$LOCAL_ACTION_FILE"
fi
# Clean up remote files
ssh -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no "$host" "rm -f $REMOTE_SCRIPT $REMOTE_ACTION_FILE" < /dev/null 2>/dev/null || true
print_success "Completed: $host"
return 0
}
if [ -n "$HOSTS_FILE" ]; then
if [ ! -f "$HOSTS_FILE" ]; then
print_error "Hosts file not found: $HOSTS_FILE"
exit 1
fi
print_header "${ICON_NETWORK} Multi-Host Deployment"
print_info "Reading hosts from: ${YELLOW}$HOSTS_FILE${NC}"
print_info "SSH timeout: ${YELLOW}${SSH_TIMEOUT}s${NC}"
echo ""
SUCCESS_COUNT=0
FAIL_COUNT=0
ACTION_FILE="/tmp/.netbird-deploy-action-$$"
# Use file descriptor 3 to read hosts file (prevents SSH from consuming stdin)
exec 3< "$HOSTS_FILE"
while IFS= read -r line <&3 || [ -n "$line" ]; do
# Skip empty lines and comments
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
# Trim whitespace
line=$(echo "$line" | xargs)
# Parse hostname and optional password
host=$(echo "$line" | awk '{print $1}')
sudo_pass=$(echo "$line" | awk '{print $2}')
# Use default password if no host-specific password is provided
if [ -z "$sudo_pass" ] && [ -n "$DEFAULT_PASSWORD" ]; then
sudo_pass="$DEFAULT_PASSWORD"
fi
# Run deployment (output streams to terminal)
if deploy_to_host "$host" "$sudo_pass"; then
# Read the action from the temp file
action="unknown"
if [ -f "$ACTION_FILE" ]; then
action=$(cat "$ACTION_FILE")
rm -f "$ACTION_FILE"
fi
SUCCESS_HOSTS+=("$host")
HOST_ACTIONS+=("$action")
((SUCCESS_COUNT++)) || true
else
FAILED_HOSTS+=("$host")
((FAIL_COUNT++)) || true
fi
echo ""
done
exec 3<&-
# Print detailed summary
echo -e "\n${GREEN}${BOLD}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}${BOLD}${NC} ${ICON_SUCCESS} ${GREEN}${BOLD}Deployment Summary${NC}"
echo -e "${GREEN}${BOLD}╠═══════════════════════════════════════════════════════════════╣${NC}"
echo -e "${GREEN}${BOLD}${NC} ${GREEN}${ICON_SUCCESS}${NC} Successful: ${BOLD}$SUCCESS_COUNT${NC}"
echo -e "${GREEN}${BOLD}${NC} ${RED}${ICON_ERROR}${NC} Failed: ${BOLD}$FAIL_COUNT${NC}"
echo -e "${GREEN}${BOLD}╚═══════════════════════════════════════════════════════════════╝${NC}"
# Show successful hosts with actions
if [ ${#SUCCESS_HOSTS[@]} -gt 0 ]; then
echo -e "\n${GREEN}${BOLD}Successful Hosts:${NC}"
for i in "${!SUCCESS_HOSTS[@]}"; do
host="${SUCCESS_HOSTS[$i]}"
action="${HOST_ACTIONS[$i]}"
echo -e " ${GREEN}${ICON_SUCCESS}${NC} ${YELLOW}$host${NC} ${DIM}($action)${NC}"
done
fi
# Show failed hosts
if [ ${#FAILED_HOSTS[@]} -gt 0 ]; then
echo -e "\n${RED}${BOLD}Failed Hosts:${NC}"
for host in "${FAILED_HOSTS[@]}"; do
echo -e " ${RED}${ICON_ERROR}${NC} ${YELLOW}$host${NC}"
done
fi
echo ""
exit 0
fi
if [ -n "$SSH_HOST" ]; then
print_header "${ICON_DEPLOY} Single Host Deployment"
ACTION_FILE="/tmp/.netbird-deploy-action-$$"
if deploy_to_host "$SSH_HOST" "$DEFAULT_PASSWORD"; then
# Read and display the action
if [ -f "$ACTION_FILE" ]; then
action=$(cat "$ACTION_FILE")
rm -f "$ACTION_FILE"
print_info "Action performed: ${YELLOW}$action${NC}"
fi
fi
echo ""
exit 0
fi
if [ "$EUID" -ne 0 ]; then
print_warning "Elevating to root..."
exec sudo "$0" "$@"
fi
WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT
detect_install() {
if ! command -v netbird &>/dev/null; then
echo "none"
return
fi
if [ -f /etc/netbird/install.conf ]; then
local pm
pm=$(grep -oP 'package_manager=\K.*' /etc/netbird/install.conf 2>/dev/null || true)
if [ "$pm" = "bin" ]; then
echo "bin"
return
elif [ -n "$pm" ]; then
echo "pkg:$pm"
return
fi
fi
if dpkg -s netbird &>/dev/null; then
echo "pkg:apt"
return
fi
if rpm -q netbird &>/dev/null; then
if command -v dnf &>/dev/null; then
echo "pkg:dnf"
else
echo "pkg:yum"
fi
return
fi
if pacman -Q netbird &>/dev/null; then
echo "pkg:pacman"
return
fi
echo "bin"
}
install_timer() {
print_header "${ICON_TIMER} Installing Auto-Update Timer"
if systemctl is-active --quiet netbird-update.timer; then
print_warning "Updating existing timer..."
systemctl stop netbird-update.timer
systemctl disable netbird-update.timer
fi
cat > /etc/systemd/system/netbird-update.service <<'UNIT'
[Unit]
Description=Netbird Auto-Update
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'curl -fsSL https://pkgs.netbird.io/install.sh | UPDATE_NETBIRD=true sh && chown root:root /usr/bin/netbird 2>/dev/null || true && chmod +x /usr/bin/netbird 2>/dev/null || true && restorecon -v /usr/bin/netbird 2>/dev/null || true && systemctl restart netbird 2>/dev/null || true'
StandardOutput=append:/var/log/netbird-update.log
StandardError=append:/var/log/netbird-update.log
UNIT
cat > /etc/systemd/system/netbird-update.timer <<UNIT
[Unit]
Description=Run Netbird update daily
[Timer]
OnCalendar=$TIMER_TIME
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
UNIT
systemctl daemon-reload
systemctl enable --now netbird-update.timer
print_success "Timer installed successfully"
print_info "Schedule: ${YELLOW}$TIMER_TIME${NC} (with random 0-10 minute delay)"
print_info "Logs: ${YELLOW}/var/log/netbird-update.log${NC}"
}
print_header "${ICON_INSTALL} NetBird Install/Update"
print_status "${ICON_DOWNLOAD} Downloading Netbird install script..."
curl -fsSL -o "$WORK_DIR/install.sh" https://pkgs.netbird.io/install.sh
chmod +x "$WORK_DIR/install.sh"
print_success "Download complete"
print_status "${ICON_CHECK} Detecting installation state..."
state=$(detect_install)
print_info "Detected: ${YELLOW}$state${NC}"
echo ""
case "$state" in
none)
print_header "${ICON_INSTALL} Fresh Installation"
print_info "Netbird not found. Installing..."
USE_BIN_INSTALL=true SKIP_UI_APP=true "$WORK_DIR/install.sh"
chown root:root /usr/bin/netbird 2>/dev/null || true
restorecon -v /usr/bin/netbird 2>/dev/null || true
systemctl restart netbird 2>/dev/null || true
print_success "Netbird installed successfully"
echo "fresh install" > /tmp/.netbird-action-$$
;;
pkg:*)
print_header "${ICON_UPDATE} Package Manager Replacement"
pm="${state#pkg:}"
print_info "Netbird installed via ${YELLOW}$pm${NC}"
print_info "Replacing with binary install..."
cat > /tmp/.netbird_replace.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
sleep 3
systemctl stop netbird 2>/dev/null || true
case "$1" in
apt) apt-get remove -y netbird netbird-ui 2>/dev/null || true ;;
yum) yum remove -y netbird netbird-ui 2>/dev/null || true ;;
dnf) dnf remove -y netbird netbird-ui 2>/dev/null || true ;;
zypper) zypper remove -y netbird netbird-ui 2>/dev/null || true ;;
rpm-ostree) rpm-ostree uninstall -y netbird netbird-ui 2>/dev/null || true ;;
pacman) pacman -Rns --noconfirm netbird netbird-ui 2>/dev/null || true ;;
esac
USE_BIN_INSTALL=true SKIP_UI_APP=true bash /tmp/.netbird_install.sh
chown root:root /usr/bin/netbird 2>/dev/null || true
chmod +x /usr/bin/netbird 2>/dev/null || true
restorecon -v /usr/bin/netbird 2>/dev/null || true
systemctl restart netbird 2>/dev/null || true
rm -f /tmp/.netbird_replace.sh /tmp/.netbird_install.sh
EOF
chmod +x /tmp/.netbird_replace.sh
cp "$WORK_DIR/install.sh" /tmp/.netbird_install.sh
setsid bash /tmp/.netbird_replace.sh "$pm" </dev/null >/var/log/netbird-update.log 2>&1 &
print_warning "Replacement scheduled in background"
print_info "Connection may drop momentarily"
print_info "Check ${YELLOW}/var/log/netbird-update.log${NC} for progress"
echo "pkg replacement" > /tmp/.netbird-action-$$
;;
bin)
print_header "${ICON_VERSION} Binary Update Check"
if [ ! -f /etc/netbird/install.conf ]; then
mkdir -p /etc/netbird
echo "package_manager=bin" > /etc/netbird/install.conf
fi
current=$(netbird version 2>/dev/null || echo "unknown")
latest=$(curl -fsSL https://pkgs.netbird.io/releases/latest 2>/dev/null | grep -oP '"tag_name":\s*"v?\K[^"]+' || echo "unknown")
print_info "Current version: ${YELLOW}$current${NC}"
print_info "Latest version: ${YELLOW}$latest${NC}"
echo ""
if [ "$current" = "$latest" ] && [ "$current" != "unknown" ]; then
print_success "Netbird is up to date"
echo "up to date" > /tmp/.netbird-action-$$
else
print_info "Updating Netbird: ${YELLOW}$current${NC}${GREEN}$latest${NC}"
cp "$WORK_DIR/install.sh" /tmp/.netbird_update.sh
setsid bash -c 'sleep 3 && bash /tmp/.netbird_update.sh --update && chown root:root /usr/bin/netbird 2>/dev/null || true && chmod +x /usr/bin/netbird 2>/dev/null || true && restorecon -v /usr/bin/netbird 2>/dev/null || true && systemctl restart netbird 2>/dev/null || true && rm -f /tmp/.netbird_update.sh' \
</dev/null >/var/log/netbird-update.log 2>&1 &
print_warning "Update scheduled in background"
print_info "Connection may drop momentarily"
print_info "Check ${YELLOW}/var/log/netbird-update.log${NC} for progress"
echo "updated" > /tmp/.netbird-action-$$
fi
;;
esac
if $INSTALL_TIMER; then
install_timer
fi
echo ""
+16
View File
@@ -0,0 +1,16 @@
# Test SSH remote execution
echo "=== Testing SSH remote execution ==="
echo ""
echo "Usage examples:"
echo " ./netbird-install-update.sh # Run locally"
echo " ./netbird-install-update.sh --timer # Run locally with auto-update timer"
echo " ./netbird-install-update.sh --ssh user@host # Deploy to remote host"
echo " ./netbird-install-update.sh --ssh user@host --timer # Deploy to remote host with timer"
echo ""
echo "The -ssh flag will:"
echo "1. Copy this script to the remote host via scp"
echo "2. Execute it remotely with sudo"
echo "3. Pass through any other flags (like --timer)"
echo "4. Clean up the remote copy after execution"
echo ""
echo "Script is ready for testing!"
+126 -6
View File
@@ -1,12 +1,132 @@
# Zsh Config # Zsh Config
A reproducible, self-configuring Zsh setup managed from a remote git repo. The `zshrc` auto-updates itself and the Starship prompt config on every interactive shell start, and a versioned bootstrap script installs all missing dependencies across distros. Drop `~/.zshrc` in place once and every machine stays in sync automatically.
## Design
- **Reproducible** — one `zshrc` drives an identical environment across every machine and distro.
- **Self-configuring** — tool integrations are guarded by `command -v`, so the shell works degraded-but-fine when a tool is absent, and lights up automatically once the bootstrap installs it.
- **Self-updating** — on each interactive shell start, `zshrc` and `starship.toml` are re-pulled from the repo and applied if changed (downloaded files are validated with `zsh -n` before replacing anything, so a 502 error page or corrupted content can't break the shell).
- **Network-resilient** — all remote calls use `--fail` + `--connect-timeout`/`--max-time`, so an unreachable server silently no-ops instead of hanging the shell.
## Files ## Files
### `zshrc` | File | Description |
Main Zsh configuration file. Sets up Oh My Zsh, plugins (git, sudo, eza, fzf, starship, etc.), history options, aliases, and auto-updates itself from a remote source. Before replacing `~/.zshrc`, the downloaded file is validated with `zsh -n` to ensure it's syntactically valid — this prevents error pages or corrupted content from breaking the shell. Also bootstraps tmux config and runs a fastfetch system info display on interactive shells. |------|-------------|
| [`zshrc`](zshrc) | Main config. Path setup, history options, oh-my-zsh + plugins, eza theme, tool integrations, aliases, and the auto-update logic. Sources `~/.zshrc.local` at the end for per-machine overrides. |
| [`zshrc-bootstrap.zsh`](zshrc-bootstrap.zsh) | One-shot setup script run by `zshrc` when the bootstrap version changes. Installs all dependencies across distros and clones oh-my-zsh + custom plugins. |
| [`starship.toml`](starship.toml) | Starship prompt config — two-line prompt with directory, git status, command duration, Nerd Font symbols, and an error indicator. Synced to `~/.config/starship.toml` by the auto-update logic. |
| [`tmux.conf`](tmux.conf) | Tmux config with GitHub Dark theming, Vim-style pane navigation (`h/j/k/l`), TPM plugin manager, Nerd Font auto-install, and a built-in cheatsheet (`prefix + ?`). Installed to `~/.config/tmux/tmux.conf` on first shell start. |
### `zshrc-bootstrap.zsh` ## Getting Started
One-shot setup script executed by `zshrc`. Installs missing dependencies (starship, lazydocker, fastfetch, eza, fzf, etc.) via the system package manager (brew, apt, dnf, pacman) or cargo, clones Oh My Zsh and custom plugins (zsh-autosuggestions, fast-syntax-highlighting, fzf-tab). Runs only when the bootstrap version changes.
### `tmux.conf` Point `~/.zshrc` at the remote `zshrc`:
Tmux configuration with GitHub Dark theming, Nerd Font auto-install, TPM plugin manager, pane/window keybindings (Vim-style navigation with `h/j/k/l`), status bar showing session info, battery, online status, and a built-in cheatsheet (bound to `prefix + ?`).
```bash
curl -fsSL https://git.jeremymcclure.com/jeremy/scripts/raw/branch/master/zsh/zshrc -o ~/.zshrc && exec zsh
```
On first start, `zshrc` bootstraps missing dependencies (oh-my-zsh, plugins, and all CLI tools below) over the next shell starts. To force a re-bootstrap (e.g., after adding new tool installs to the bootstrap script), the bootstrap version is bumped in `zshrc` and every machine re-runs it automatically — or run manually:
```bash
BOOTSTRAP=true exec zsh
```
## Dependencies
All installed automatically by the bootstrap across brew / apt / dnf / pacman / cargo:
| Tool | Purpose |
|------|---------|
| starship | Prompt (configured via `starship.toml`) |
| fzf | Fuzzy finder — powers `fzf-tab` completion and `zi` |
| eza | `ls` replacement with icons, git status, hyperlinks |
| bat | `cat` replacement + colored man pages |
| zoxide | Frecency directory jumping (`z`, `zi`) |
| git-delta | Pretty `git diff`/`log`/`show` pager |
| fastfetch | System info on shell start |
| lazydocker | TUI for Docker |
| rsync / tmux / git | Core utilities |
Debian/Ubuntu renames handled: `batcat``bat`, `fdfind``fd` (symlinked into `~/.local/bin`).
## Oh-My-Zsh Plugins
`git`, `sudo`, `extract`, `eza`, `history`, `kitty`, `docker`, `docker-compose`, `archlinux`, `encode64`, `universalarchive`, `zsh-autosuggestions`, `fast-syntax-highlighting`, `fzf`, `fzf-tab`, `systemd`, `vscode`, `rsync`, `starship`.
## Tool Integrations
Each is enabled only when the binary is present (`command -v`):
- **zoxide** — `z <substring>` jumps to frecency-ranked dirs; `zi` opens the fzf picker. `cd` still works.
- **bat** — `alias cat='bat'`, `BAT_THEME=Monokai Extended`, and `MANPAGER` set so `man` pages are colored and paginated by bat.
- **delta** — `GIT_PAGER=delta` so `git diff`/`log`/`show` render side-by-side with syntax highlighting. (Not aliased to `diff` — delta is a pager reading diff input on stdin, not a `diff a b` replacement.)
## History & Shell Options
`SHARE_HISTORY`, `HIST_IGNORE_DUPS`, `HIST_IGNORE_ALL_DUPS`, `HIST_REDUCE_BLANKS`, `HIST_FCNTL_LOCK`, `HIST_VERIFY`, `EXTENDED_HISTORY`, `AUTO_CD`, `AUTO_LIST`, `INTERACTIVE_COMMENTS`, `AUTO_PUSHD`, `PUSHD_IGNORE_DUPS`, `PUSHD_SILENT`.
`AUTO_PUSHD` maintains an automatic directory stack, so `cd -<TAB>` and `dirs` let you navigate recent locations without `pushd`/`popd` muscle memory.
## Aliases
| Alias | Action |
|-------|--------|
| `c` | `clear` |
| `q` | `exit` |
| `open-ports` | `ss -tulpn \| grep LISTEN` |
| `nbstat` | Netbird peer status as a TSV table |
| `yeet` | `yay -Rcs` (remove + deps + config on Arch) |
## Keybindings
| Shortcut | Action |
|----------|--------|
| `Ctrl+R` | fzf history search (via `fzf` plugin) |
| `Ctrl+T` | fzf file finder |
| `Alt+C` | fzf cd |
| `Tab` | `fzf-tab` completion menu |
| `Esc` twice | Toggle `sudo` prefix (via `sudo` plugin) |
## Eza Hyperlinks — Implementation Note
`zshrc` does **not** set the oh-my-zsh `eza` plugin's `hyperlink` zstyle (the plugin's bare `--hyperlink` causes issues). Instead, after oh-my-zsh loads, the aliases are rewritten to append the correct hyperlink flag for the installed eza version:
- **Newer eza** (help shows `--hyperlink [<WHEN>]`): the oh-my-zsh plugin's bare `--hyperlink` is treated by clap as taking the *next token* as its optional value, so `la /` fails with `invalid value '/'`. We append the glued `--hyperlink=always` form, which can't eat a following path argument.
- **Older eza** (e.g. Debian's, where `--hyperlink` is a pure boolean): `--hyperlink=always` is rejected with `Flag --hyperlink cannot take a value`. We detect this via `eza --help` and fall back to the bare `--hyperlink`, which is safe here because boolean flags don't consume the next token (the path-eating bug doesn't apply).
Detection runs once at shell start via `eza --help | grep '<WHEN>'`; the right form is applied to every eza alias. Hyperlinks stay on and paths keep working across eza versions.
## Auto-Update Behavior
On every interactive shell start:
1. **zshrc** — re-downloaded, validated with `zsh -n`, and applied via `exec zsh` only if it differs from `~/.zshrc`.
2. **starship.toml** — re-downloaded and moved to `~/.config/starship.toml` if changed (applied live on the next prompt render; no shell reload needed).
3. **tmux.conf** — downloaded once on first shell start if `~/.config/tmux/tmux.conf` and `~/.config/byobu/.tmux.conf` are both absent.
4. **bootstrap** — runs (`zshrc-bootstrap.zsh`) when `ZSHRC_BOOTSTRAP_VERSION` in `zshrc` doesn't match `~/.config/zsh/.bootstrapped`.
All downloads use `--fail` so a 502/HTML error page is never written over a working config, and `--connect-timeout`/`--max-time` so an unreachable server never hangs startup.
## Per-Machine Overrides
`~/.zshrc.local` is sourced at the very end of `zshrc` (created empty if missing). Put machine-specific aliases, env vars, or PATH additions there — they survive repo updates since `zshrc` doesn't manage that file.
## Environment Variables
| Variable | Default | Purpose |
|----------|---------|---------|
| `ZSHRC_GIT` | `.../scripts/raw/branch/master/zsh/` | Base URL for all remote config files |
| `ZSHRC_BOOTSTRAP_VERSION` | `4` | Bump to force a re-bootstrap on all machines |
| `FFENABLED` | (unset → enabled) | Set to `false` to disable the fastfetch system info on shell start |
| `BAT_THEME` | `Monokai Extended` | Override before the bat block to use a different bat theme |
| `GIT_PAGER` | `delta` (when installed) | Override to use a different git pager |
## Helper Functions
```bash
update_zshrc # Manually re-pull and reload the zshrc
fix_btopbg # Disable btop theme_background (workaround for transparency)
install_opencode # Install the opencode CLI
BOOTSTRAP=true exec zsh # Force a re-bootstrap
```
+101
View File
@@ -0,0 +1,101 @@
# Starship prompt configuration - managed by the zshrc repo.
# Local edits to ~/.config/starship.toml are overwritten on the next interactive
# shell start (reproducible/self-configuring design). Uses Nerd Font symbols
# (JetBrainsMono Nerd Font is installed by the tmux config).
command_timeout = 1000
add_newline = true
# Two-line prompt:
# line 1: directory + git status + command duration
# line 2: the input arrow (red on error)
format = """
$directory\
$git_branch\
$git_status\
$cmd_duration\
$line_break\
$character\
"""
[character]
success_symbol = "[➜](bold green)"
error_symbol = "[✗](bold red)"
vimcmd_symbol = "[V](bold green)"
[directory]
truncation_length = 3
truncate_to_repo = true
style = "bold blue"
read_only = " 󰌾"
read_only_style = "red"
repo_root_style = "bold cyan"
[git_branch]
symbol = " "
style = "bold purple"
format = "[$symbol$branch]($style) "
[git_status]
style = "bold red"
conflicted = "=${count}"
ahead = "⇡${count}"
behind = "⇣${count}"
diverged = "⇕↑${ahead_count}↓${behind_count}"
untracked = "?${count}"
stashed = " *${count}"
modified = " !${count}"
staged = " +${count}"
renamed = " »${count}"
deleted = " ✘${count}"
format = '([$all_status$ahead_behind]($style) )'
[cmd_duration]
min_time = 2000
format = "took [$duration](bold yellow) "
[status]
disabled = false
format = '[$symbol]($style)'
symbol = "✗ "
success_symbol = ""
style = "bold red"
[username]
show_always = false
style_user = "bold yellow"
style_root = "bold red"
format = "[$user]($style)@"
[hostname]
ssh_only = true
style = "bold green"
format = "[$hostname]($style) "
[python]
symbol = " "
format = '[${symbol}${pyenv_prefix}(${version})(\($virtualenv\))]($style) '
[nodejs]
symbol = " "
format = "[$symbol($version)]($style) "
[rust]
symbol = " "
format = "[$symbol($version)]($style) "
[golang]
symbol = " "
format = "[$symbol($version)]($style) "
[java]
symbol = " "
format = "[$symbol($version)]($style) "
[docker_context]
symbol = " "
format = "[$symbol$context]($style) "
[shell]
disabled = true
style = "bold cyan"
+76 -12
View File
@@ -6,7 +6,7 @@ export ZSHRC_GIT="https://git.jeremymcclure.com/jeremy/scripts/raw/branch/master
export ZSHRC_URL="$ZSHRC_GIT/zshrc" export ZSHRC_URL="$ZSHRC_GIT/zshrc"
export ZSHRC_BOOTSTRAP_URL="$ZSHRC_GIT/zshrc-bootstrap.zsh" export ZSHRC_BOOTSTRAP_URL="$ZSHRC_GIT/zshrc-bootstrap.zsh"
export ZSHRC_BOOTSTRAP_VERSION="2" export ZSHRC_BOOTSTRAP_VERSION="4"
export ZSHRC_BOOTSTRAP="$ZSH_CONFIG/.bootstrapped" export ZSHRC_BOOTSTRAP="$ZSH_CONFIG/.bootstrapped"
[[ -d "$HOME/Scripts" ]] && path+=("$HOME/Scripts") [[ -d "$HOME/Scripts" ]] && path+=("$HOME/Scripts")
@@ -16,7 +16,7 @@ export ZSHRC_BOOTSTRAP="$ZSH_CONFIG/.bootstrapped"
update_zshrc(){ update_zshrc(){
local tmp=/tmp/.zshrc.$$ local tmp=/tmp/.zshrc.$$
curl -sSL --connect-timeout 5 "$ZSHRC_URL" -o "$tmp" 2>/dev/null || return curl -fsSL --connect-timeout 5 --max-time 10 "$ZSHRC_URL" -o "$tmp" 2>/dev/null || return
if zsh -n "$tmp" 2>/dev/null && ! cmp -s "$tmp" "$HOME/.zshrc" 2>/dev/null; then if zsh -n "$tmp" 2>/dev/null && ! cmp -s "$tmp" "$HOME/.zshrc" 2>/dev/null; then
echo "Updating zshrc..." echo "Updating zshrc..."
mv "$tmp" "$HOME/.zshrc" && echo "Updated. Reloading..." && exec zsh mv "$tmp" "$HOME/.zshrc" && echo "Updated. Reloading..." && exec zsh
@@ -32,10 +32,13 @@ install_opencode(){
curl -fsSL https://opencode.ai/install | bash curl -fsSL https://opencode.ai/install | bash
} }
# Bootstrap: download and run once per bootstrap version # Bootstrap: download and run once per bootstrap version. Installs missing CLI
# tools and oh-my-zsh/plugins across distros; prints a banner so the user knows
# what the activity is. Re-runs only when ZSHRC_BOOTSTRAP_VERSION is bumped or
# BOOTSTRAP=true is set.
if [[ ! -f "$ZSHRC_BOOTSTRAP" || "$(cat "$ZSHRC_BOOTSTRAP" 2>/dev/null)" != "$ZSHRC_BOOTSTRAP_VERSION" || "$BOOTSTRAP" == "true" ]]; then if [[ ! -f "$ZSHRC_BOOTSTRAP" || "$(cat "$ZSHRC_BOOTSTRAP" 2>/dev/null)" != "$ZSHRC_BOOTSTRAP_VERSION" || "$BOOTSTRAP" == "true" ]]; then
tmp=/tmp/.zshrc_bootstrap.$$ tmp=/tmp/.zshrc_bootstrap.$$
curl -sSL --connect-timeout 5 -o "$tmp" "$ZSHRC_BOOTSTRAP_URL" 2>/dev/null curl -fsSL --connect-timeout 5 --max-time 10 -o "$tmp" "$ZSHRC_BOOTSTRAP_URL" 2>/dev/null
if [[ -f "$tmp" ]]; then if [[ -f "$tmp" ]]; then
zsh "$tmp" 2>/dev/null zsh "$tmp" 2>/dev/null
echo "$ZSHRC_BOOTSTRAP_VERSION" > "$ZSHRC_BOOTSTRAP" echo "$ZSHRC_BOOTSTRAP_VERSION" > "$ZSHRC_BOOTSTRAP"
@@ -46,13 +49,13 @@ fi
TMUXCONF="$HOME/.config/tmux" TMUXCONF="$HOME/.config/tmux"
BYOBUCONF="$HOME/.config/byobu" BYOBUCONF="$HOME/.config/byobu"
TMUXREMOTE="$ZSHRC_GIT/tmux.conf" TMUXREMOTE="$ZSHRC_GIT/tmux.conf"
if [ ! -f "$TMUXCONF" ]; then if [ ! -f "$TMUXCONF/tmux.conf" ] && [ ! -f "$BYOBUCONF/.tmux.conf" ]; then
# Create a secure temporary file # Create a secure temporary file
TEMP_TMUXCONF=$(mktemp) TEMP_TMUXCONF=$(mktemp)
# Download to temp file. # Download to temp file.
# The '&&' ensures the 'mv' only runs if curl succeeds (exit code 0). # The '&&' ensures the 'mv' only runs if curl succeeds (exit code 0).
if curl -sSL --connect-timeout 5 -o "$TEMP_TMUXCONF" "$TMUXREMOTE"; then if curl -fsSL --connect-timeout 5 --max-time 10 -o "$TEMP_TMUXCONF" "$TMUXREMOTE"; then
mkdir -p "$TMUXCONF" "$BYOBUCONF" mkdir -p "$TMUXCONF" "$BYOBUCONF"
cp "$TEMP_TMUXCONF" "$TMUXCONF/tmux.conf" cp "$TEMP_TMUXCONF" "$TMUXCONF/tmux.conf"
mv "$TEMP_TMUXCONF" "$BYOBUCONF/.tmux.conf" mv "$TEMP_TMUXCONF" "$BYOBUCONF/.tmux.conf"
@@ -76,6 +79,11 @@ setopt HIST_FCNTL_LOCK
setopt AUTO_CD setopt AUTO_CD
setopt AUTO_LIST setopt AUTO_LIST
setopt INTERACTIVE_COMMENTS setopt INTERACTIVE_COMMENTS
setopt HIST_VERIFY
setopt EXTENDED_HISTORY
setopt AUTO_PUSHD
setopt PUSHD_IGNORE_DUPS
setopt PUSHD_SILENT
ZSH_THEME="" ZSH_THEME=""
ENABLE_CORRECTION="false" ENABLE_CORRECTION="false"
@@ -112,7 +120,10 @@ zstyle ':omz:plugins:eza' 'icons' yes
zstyle ':omz:plugins:eza' 'color-scale' all zstyle ':omz:plugins:eza' 'color-scale' all
zstyle ':omz:plugins:eza' 'color-scale-mode' fixed zstyle ':omz:plugins:eza' 'color-scale-mode' fixed
zstyle ':omz:plugins:eza' 'size-prefix' si zstyle ':omz:plugins:eza' 'size-prefix' si
zstyle ':omz:plugins:eza' 'hyperlink' yes # NOTE: 'hyperlink' is intentionally NOT set here. The oh-my-zsh eza plugin adds
# a bare "--hyperlink" flag which eza (clap) treats as taking the next token as
# its optional value, so `la /` fails with "invalid value '/'". We re-enable
# hyperlinks safely below via the glued `--hyperlink=always` form.
zstyle ':completion:*' menu select zstyle ':completion:*' menu select
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}'
@@ -121,21 +132,74 @@ zstyle ':completion:*' special-dirs true
source $OHMYZSH/oh-my-zsh.sh source $OHMYZSH/oh-my-zsh.sh
# Re-enable eza hyperlinks, picking the flag form the installed eza supports.
# Newer eza: --hyperlink takes an optional value; the oh-my-zsh plugin's bare
# --hyperlink would eat the following path arg (e.g. `la /` -> invalid value '/'),
# so we use the glued --hyperlink=always form.
# Older eza (e.g. Debian's): --hyperlink is a pure boolean and rejects a value,
# so --hyperlink=always errors; the bare --hyperlink is correct and safe there
# (boolean flags don't consume the next token, so the path-eating bug doesn't
# apply).
_hl_flag="--hyperlink"
if command -v eza >/dev/null && eza --help 2>&1 | grep -q -- '--hyperlink.*<WHEN>'; then
_hl_flag="--hyperlink=always"
fi
for _ea in la ldot lD lDD ll ls lsd lsdl lS lT; do
(( $+aliases[$_ea] )) && alias "$_ea"="${aliases[$_ea]} $_hl_flag"
done
unset _ea _hl_flag
# --- Tool integrations (self-configuring: enabled only when the tool exists) ---
# zoxide: frecency-based directory jumping — `z <substring>` / `zi` (fzf picker).
# Replaces nothing; `cd` still works. Auto-installed by the bootstrap.
command -v zoxide >/dev/null && eval "$(zoxide init zsh)" 2>/dev/null
# bat: colored `cat` and colored man pages. bat/batcat handled by bootstrap.
if command -v bat >/dev/null; then
alias cat='bat'
export BAT_THEME="${BAT_THEME:-Monokai Extended}"
export MANPAGER="sh -c 'col -bx | bat -l man -p'"
export MANROFFOPT='-c'
fi
# delta: prettier git diffs as git's pager (git respects GIT_PAGER). Auto-installed
# by the bootstrap. Not aliased to `diff` since delta reads diff input on stdin,
# not two file arguments — GIT_PAGER is the correct integration.
command -v delta >/dev/null && export GIT_PAGER='delta'
alias c='clear' alias c='clear'
alias q="exit" alias q="exit"
alias nbstat=$'netbird status --json | jq -r \'.peers.details[]? | [(.hostname // .fqdn), .netbirdIp, .status] | @tsv\' | column -t -s $\'\\t\'' alias nbstat=$'netbird status --json | jq -r \'.peers.details[]? | [(.hostname // .fqdn), .netbirdIp, .status] | @tsv\' | column -t -s $\'\\t\''
alias open-ports="ss -tulpn | grep LISTEN" alias open-ports="ss -tulpn | grep LISTEN"
alias yeet='yay -Rcs' alias yeet='yay -Rcs'
# Auto-update: check every shell start, silently fail if server unreachable # Auto-update self and managed configs: checked every interactive shell start,
# silently skipped if the server is unreachable. Keeps zshrc + starship.toml in
# sync across machines (reproducible/self-configuring); only reloads the shell
# when the zshrc itself changes (starship applies its config live per render).
if [[ -o interactive ]]; then if [[ -o interactive ]]; then
# zshrc (reload shell if changed)
tmp=/tmp/.zshrc.$$ tmp=/tmp/.zshrc.$$
curl -sSL --connect-timeout 3 "$ZSHRC_URL" -o "$tmp" 2>/dev/null if curl -fsSL --connect-timeout 3 --max-time 8 "$ZSHRC_URL" -o "$tmp" 2>/dev/null \
if [[ -f "$tmp" ]] && zsh -n "$tmp" 2>/dev/null && ! cmp -s "$tmp" "$HOME/.zshrc" 2>/dev/null; then && [[ -f "$tmp" ]] && zsh -n "$tmp" 2>/dev/null \
&& ! cmp -s "$tmp" "$HOME/.zshrc" 2>/dev/null; then
echo "Updating zshrc..." echo "Updating zshrc..."
mv "$tmp" "$HOME/.zshrc" && exec zsh mv "$tmp" "$HOME/.zshrc" && exec zsh
fi fi
rm -f "$tmp" rm -f "$tmp"
# starship prompt config (overwrite if changed; applied live on next render)
if command -v starship >/dev/null; then
stmp=$(mktemp)
if curl -fsSL --connect-timeout 3 --max-time 8 "$ZSHRC_GIT/starship.toml" -o "$stmp" 2>/dev/null \
&& [[ -f "$stmp" ]] && ! cmp -s "$stmp" "$HOME/.config/starship.toml" 2>/dev/null; then
mkdir -p "$HOME/.config"
mv "$stmp" "$HOME/.config/starship.toml"
else
rm -f "$stmp"
fi
fi
fi fi
[[ -f $HOME/.zshrc.local ]] || touch $HOME/.zshrc.local [[ -f $HOME/.zshrc.local ]] || touch $HOME/.zshrc.local
+202 -31
View File
@@ -2,71 +2,238 @@ export ZSH_CONFIG=$HOME/.config/zsh
export OHMYZSH=$ZSH_CONFIG/oh-my-zsh export OHMYZSH=$ZSH_CONFIG/oh-my-zsh
export OHMYZSH_CUSTOM=$OHMYZSH/custom export OHMYZSH_CUSTOM=$OHMYZSH/custom
# Bootstrap banner — built dynamically so the box stays aligned regardless
# of the version string length or terminal width.
_bootstrap_banner() {
local inner=61 # inner width (between the vertical bars)
local pad line
local title="Zsh bootstrap - one-time environment setup (v${ZSHRC_BOOTSTRAP_VERSION})"
local rule=$(printf '═%.0s' {1..$inner})
local mid=$(printf '─%.0s' {1..$inner})
# left-pad the title by 2 spaces so it doesn't hug the left bar
title=" $title"
pad=$(( inner - ${#title} ))
(( pad < 0 )) && pad=0
echo ""
echo "${rule}"
printf '║%s%*s║\n' "$title" "$pad" ""
echo "${mid}"
while IFS= read -r line; do
line=" $line"
pad=$(( inner - ${#line} ))
(( pad < 0 )) && pad=0
printf '║%s%*s║\n' "$line" "$pad" ""
done <<'EOF'
This runs on first login or when the bootstrap version
changes. It installs missing CLI tools and clones oh-my-zsh
+ plugins across distros. Network calls time out after
~30s so an unreachable host skips silently. The first run
may take a minute; subsequent runs are a no-op.
EOF
echo "${rule}"
echo ""
}
_bootstrap_banner
unfunction _bootstrap_banner
# GitHub-release binary fallback. Used for tools not packaged in a distro's
# repos (e.g. bat/zoxide/git-delta on RHEL-family without EPEL, or no cargo).
# Resolves the latest release tag via the /releases/latest redirect (no API,
# no rate limits), builds the asset URL from a template, extracts, and copies
# the named binary into ~/.local/bin. Args:
# $1 owner/repo $2 asset template (%v = version, no leading v)
# $3 binary name $4 arch suffix to substitute into %a in the template
_gh_latest_tag() {
local repo="$1"
curl -fsIL --connect-timeout 5 --max-time 20 "https://github.com/$repo/releases/latest" 2>/dev/null \
| grep -i '^location:' | tail -1 \
| sed -E 's#.*tag/([^[:space:]"/]+).*#\1#'
}
_gh_install_bin() {
local repo="$1" tmpl="$2" bin="$3"
local rawtag ver asset ext tmp found
rawtag=$(_gh_latest_tag "$repo")
if [[ -z "$rawtag" ]]; then
echo " FAIL could not resolve latest tag for $repo"; return 1
fi
ver="${rawtag#v}" # strip optional leading v for asset-filename substitution
asset="${tmpl//\%v/$ver}"
ext="${asset##*.}"
echo " gh installing $bin ($rawtag from $repo)"
tmp=$(mktemp -d)
if curl -fsSL --connect-timeout 5 --max-time 60 -o "$tmp/pkg.$ext" \
"https://github.com/$repo/releases/download/$rawtag/$asset" \
&& { case "$ext" in
gz) tar -xzf "$tmp/pkg.$ext" -C "$tmp" 2>/dev/null ;;
*) return 1 ;;
esac; }; then
found=$(find "$tmp" -type f -name "$bin" ! -name "*.*" | head -1)
if [[ -n "$found" ]]; then
cp "$found" "$HOME/.local/bin/$bin"
chmod +x "$HOME/.local/bin/$bin"
rm -rf "$tmp"; return 0
fi
fi
rm -rf "$tmp"
echo " FAIL could not download/extract $bin from $repo"; return 1
}
touch ~/.hushlogin touch ~/.hushlogin
mkdir -p "$HOME/.local/bin" mkdir -p "$HOME/.local/bin"
mkdir -p "$ZSH_CONFIG" mkdir -p "$ZSH_CONFIG"
path+=("$HOME/.local/bin") path+=("$HOME/.local/bin")
for p in git starship fzf eza rsync lazydocker fastfetch tmux; do # Package availability checks can hang on slow/unreachable mirrors (cold dnf
command -v $p >/dev/null && continue # metadata, apt update, pacman -Sy). Wrap them in a 20s timeout so the bootstrap
# never stalls the shell; a timed-out check just falls through to the next PM
# or to the binary installers below. Verbose output so real builds (cargo) and
# slow installs are visible as progress instead of an apparent hang.
for p in git starship fzf eza rsync lazydocker fastfetch tmux bat zoxide; do
if command -v $p >/dev/null; then
echo " ok $p (already installed)"
continue
fi
echo " ... $p (checking package managers)"
if command -v brew >/dev/null; then if command -v brew >/dev/null; then
echo "Installing $p using brew" && brew install $p && continue echo " brew installing $p" && brew install $p && continue
fi fi
if command -v apt >/dev/null && apt-cache show $p >/dev/null 2>&1; then if command -v apt >/dev/null; then
echo "Installing $p using Apt" && sudo apt install $p -y && continue if timeout 20 apt-cache show $p >/dev/null 2>&1; then
echo " apt installing $p" && sudo apt install -y $p && continue
else
echo " apt $p not available (or metadata check timed out)"
fi
fi fi
if command -v dnf >/dev/null && dnf list available -y $p >/dev/null 2>&1; then if command -v dnf >/dev/null; then
echo "Installing $p using dnf" && sudo dnf install $p -y && continue if timeout 20 dnf list available $p >/dev/null 2>&1; then
echo " dnf installing $p" && sudo dnf install -y $p && continue
else
echo " dnf $p not available (or metadata check timed out)"
fi
fi fi
if command -v pacman >/dev/null && pacman -Si $p >/dev/null 2>&1; then if command -v pacman >/dev/null; then
echo "Installing $p using pacman" && sudo pacman -Sy --noconfirm $p && continue if timeout 20 pacman -Si $p >/dev/null 2>&1; then
echo " pacman installing $p" && sudo pacman -Sy --noconfirm $p && continue
else
echo " pacman $p not available (or metadata check timed out)"
fi
fi fi
if command -v cargo >/dev/null; then if command -v cargo >/dev/null; then
echo "Installing $p using cargo" && cargo install $p --locked && continue echo " cargo installing $p (may take a few minutes to compile)" && cargo install $p --locked && continue
fi fi
echo " FAIL could not install $p by any method; skipping"
done done
if ! command -v starship >/dev/null; then if ! command -v starship >/dev/null; then
echo "Installing starship binary..." && \ echo " curl installing starship binary"
curl -sSL https://starship.rs/install.sh | sh -s -- -b ~/.local/bin -y >/dev/null 2>&1 curl -fsSL --connect-timeout 5 --max-time 30 https://starship.rs/install.sh | sh -s -- -b ~/.local/bin -y
fi fi
if ! command -v lazydocker >/dev/null; then if ! command -v lazydocker >/dev/null; then
echo "Installing lazydocker binary..." && \ echo " curl installing lazydocker binary"
curl -sSL https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | \ curl -fsSL --connect-timeout 5 --max-time 30 https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | \
DIR=$HOME/.local/bin bash >/dev/null 2>&1 DIR=$HOME/.local/bin bash
fi
# Debian/Ubuntu ships bat as 'batcat' and fd as 'fdfind'; symlink to the
# canonical names so the zshrc integrations (which call bat/fd) work everywhere.
if ! command -v bat >/dev/null && command -v batcat >/dev/null; then
ln -sf "$(command -v batcat)" "$HOME/.local/bin/bat"
fi
if ! command -v fd >/dev/null && command -v fdfind >/dev/null; then
ln -sf "$(command -v fdfind)" "$HOME/.local/bin/fd"
fi
# git-delta: the package is named 'git-delta' on every package manager but the
# binary is 'delta' (name mismatch vs the install loop above), so install via the
# same PM cascade using the real package name. Used by zshrc as GIT_PAGER.
if ! command -v delta >/dev/null; then
echo " ... delta (checking package managers, package name is 'git-delta')"
if command -v brew >/dev/null; then
echo " brew installing git-delta" && brew install git-delta
elif command -v apt >/dev/null && timeout 20 apt-cache show git-delta >/dev/null 2>&1; then
echo " apt installing git-delta" && sudo apt install -y git-delta
elif command -v dnf >/dev/null && timeout 20 dnf list available git-delta >/dev/null 2>&1; then
echo " dnf installing git-delta" && sudo dnf install -y git-delta
elif command -v pacman >/dev/null && timeout 20 pacman -Si git-delta >/dev/null 2>&1; then
echo " pacman installing git-delta" && sudo pacman -Sy --noconfirm git-delta
elif command -v cargo >/dev/null; then
echo " cargo installing git-delta (may take a few minutes to compile)" && cargo install git-delta --locked
else
echo " FAIL could not install delta; skipping"
fi
fi fi
arch=$(uname -m) arch=$(uname -m)
target_musl=""; target_gnu=""
case "$arch" in case "$arch" in
x86_64) target_fast="linux-amd64" && target_eza="x86_64-unknown-linux-gnu" ;; x86_64) target_fast="linux-amd64" && target_eza="x86_64-unknown-linux-gnu" \
aarch64|arm64) target_fast="linux-aarch64" && target_eza="aarch64-unknown-linux-gnu" ;; && target_musl="x86_64-unknown-linux-musl" && target_gnu="x86_64-unknown-linux-gnu" ;;
armv7l) target_fast="linux-armv7l" && target_eza="armv7-unknown-linux-gnueabihf" ;; aarch64|arm64) target_fast="linux-aarch64" && target_eza="aarch64-unknown-linux-gnu" \
&& target_musl="aarch64-unknown-linux-musl" && target_gnu="aarch64-unknown-linux-gnu" ;;
armv7l) target_fast="linux-armv7l" && target_eza="armv7-unknown-linux-gnueabihf" \
&& target_musl="armv7-unknown-linux-musleabihf" && target_gnu="armv7-unknown-linux-gnueabihf" ;;
esac esac
if [[ -n $target_fast && -n $target_eza ]]; then if [[ -n $target_fast && -n $target_eza ]]; then
if ! command -v fastfetch >/dev/null; then if ! command -v fastfetch >/dev/null; then
curl -sSL -o /tmp/fastfetch.tar.gz "https://github.com/fastfetch-cli/fastfetch/releases/latest/download/fastfetch-${target_fast}.tar.gz" echo " curl installing fastfetch (github release tarball)"
tar -xzf /tmp/fastfetch.tar.gz -C /tmp if curl -fsSL --connect-timeout 5 --max-time 30 -o /tmp/fastfetch.tar.gz \
ffdir=$(find /tmp -maxdepth 1 -type d -name 'fastfetch*' | head -1) "https://github.com/fastfetch-cli/fastfetch/releases/latest/download/fastfetch-${target_fast}.tar.gz" \
cp "$ffdir/usr/bin/fastfetch" "$HOME/.local/bin/" && tar -xzf /tmp/fastfetch.tar.gz -C /tmp 2>/dev/null; then
chmod +x "$HOME/.local/bin/fastfetch" ffdir=$(find /tmp -maxdepth 1 -type d -name 'fastfetch*' | head -1)
mkdir -p "$HOME/.local/share/fastfetch" if [[ -n "$ffdir" ]]; then
cp -r "$ffdir/usr/share/fastfetch/"* "$HOME/.local/share/fastfetch/" cp "$ffdir/usr/bin/fastfetch" "$HOME/.local/bin/"
rm -rf "$ffdir" /tmp/fastfetch.tar.gz chmod +x "$HOME/.local/bin/fastfetch"
mkdir -p "$HOME/.local/share/fastfetch"
cp -r "$ffdir/usr/share/fastfetch/"* "$HOME/.local/share/fastfetch/"
fi
rm -rf "$ffdir" /tmp/fastfetch.tar.gz
else
rm -f /tmp/fastfetch.tar.gz
echo " FAIL could not download/extract fastfetch; skipping"
fi
fi fi
if ! command -v eza >/dev/null; then if ! command -v eza >/dev/null; then
curl -sSL -o /tmp/eza.tar.gz "https://github.com/eza-community/eza/releases/latest/download/eza_${target_eza}.tar.gz" echo " curl installing eza (github release tarball)"
tar -xzf /tmp/eza.tar.gz -C "$HOME/.local/bin/" if curl -fsSL --connect-timeout 5 --max-time 30 -o /tmp/eza.tar.gz \
chmod +x "$HOME/.local/bin/eza" "https://github.com/eza-community/eza/releases/latest/download/eza_${target_eza}.tar.gz" \
&& tar -xzf /tmp/eza.tar.gz -C "$HOME/.local/bin/" 2>/dev/null; then
chmod +x "$HOME/.local/bin/eza"
else
rm -f /tmp/eza.tar.gz
echo " FAIL could not download/extract eza; skipping"
fi
rm -f /tmp/eza.tar.gz rm -f /tmp/eza.tar.gz
fi fi
fi fi
# GitHub-release binary fallbacks for tools often missing from distro repos
# (e.g. bat/zoxide/git-delta on RHEL/Alma without EPEL, or boxes with no cargo).
# Tried after the PM/cargo loop above already failed for these. Static musl
# builds preferred so there are no glibc dependencies; delta ships gnu-only.
if ! command -v bat >/dev/null && [[ -n "$target_musl" ]]; then
_gh_install_bin "sharkdp/bat" "bat-v%v-$target_musl.tar.gz" "bat"
fi
if ! command -v zoxide >/dev/null && [[ -n "$target_musl" ]]; then
_gh_install_bin "ajeetdsouza/zoxide" "zoxide-%v-$target_musl.tar.gz" "zoxide"
fi
if ! command -v delta >/dev/null && [[ -n "$target_gnu" ]]; then
_gh_install_bin "dandavison/delta" "delta-%v-$target_gnu.tar.gz" "delta"
fi
mkdir -p "$HOME/.config/zsh" mkdir -p "$HOME/.config/zsh"
[ ! -d $OHMYZSH ] && git clone --depth=1 https://github.com/ohmyzsh/ohmyzsh.git $OHMYZSH if [ ! -d "$OHMYZSH" ]; then
echo " git cloning oh-my-zsh"
GIT_TERMINAL_PROMPT=0 git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=10 \
clone --depth=1 https://github.com/ohmyzsh/ohmyzsh.git $OHMYZSH || \
echo " FAIL could not clone oh-my-zsh"
fi
for plug in \ for plug in \
"zsh-autosuggestions|https://github.com/zsh-users/zsh-autosuggestions" \ "zsh-autosuggestions|https://github.com/zsh-users/zsh-autosuggestions" \
@@ -76,7 +243,11 @@ do
name="${plug%%|*}" name="${plug%%|*}"
url="${plug##*|}" url="${plug##*|}"
dir="$OHMYZSH_CUSTOM/plugins/$name" dir="$OHMYZSH_CUSTOM/plugins/$name"
[[ ! -d "$dir" ]] && git clone "$url" "$dir" 2>/dev/null if [[ ! -d "$dir" ]]; then
echo " git cloning $name"
GIT_TERMINAL_PROMPT=0 git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=10 \
clone "$url" "$dir" 2>/dev/null || echo " FAIL could not clone $name"
fi
done done
# Cleanup # Cleanup