commit 914afa469f4d2eec48f9a8c2e8f9c9b62c04f462 Author: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue Jul 7 18:28:35 2026 -0500 chore: initial commit diff --git a/.chezmoi.toml.tmpl b/.chezmoi.toml.tmpl new file mode 100644 index 0000000..eddee13 --- /dev/null +++ b/.chezmoi.toml.tmpl @@ -0,0 +1,12 @@ +{{- $hostname := .chezmoi.hostname -}} +{{- $email := promptStringOnce . "email" "Git email address" -}} +{{- $name := promptStringOnce . "name" "Git full name" -}} + +[data] + hostname = {{ $hostname | quote }} + email = {{ $email | quote }} + name = {{ $name | quote }} +{{ if ne .chezmoi.os "windows" -}} +[diff] + pager = "less -FRX" +{{ end -}} diff --git a/.chezmoiexternal.toml.tmpl b/.chezmoiexternal.toml.tmpl new file mode 100644 index 0000000..4d6a02c --- /dev/null +++ b/.chezmoiexternal.toml.tmpl @@ -0,0 +1,8 @@ +{{- if eq .chezmoi.os "windows" }} +["AppData/Local/nvim"] +{{- else }} +[".config/nvim"] +{{- end }} + type = "git-repo" + url = "https://github.com/StevanFreeborn/nvim-config.git" + refreshPeriod = "168h" diff --git a/.chezmoiignore b/.chezmoiignore new file mode 100644 index 0000000..1c7c72e --- /dev/null +++ b/.chezmoiignore @@ -0,0 +1,49 @@ +# chezmoi ignore patterns +# These paths will not be managed by chezmoi + +# macOS +.DS_Store + +# Editor temp files +*.swp +*.swo +*~ + +# Nested git repos (e.g. nvim has its own .git) +**/.git/** +AppData/Local/nvim/.exe/** + +# chezmoi internal +.chezmoiroot + +# Repo-only files — don't apply these to the home directory +README.md +packages +scripts +.gitattributes +.gitignore + +# Windows-only paths — skip on Linux +{{ if ne .chezmoi.os "windows" }} +AppData/** +dot_wslconfig +Documents/** +{{ end }} + +# Linux-only paths — skip on Windows +{{ if ne .chezmoi.os "linux" }} +.zshrc +.zshenv +.oh-my-zsh/** +{{ end }} + +# Linux-only scripts — skip on Windows +{{ if ne .chezmoi.os "linux" }} +.chezmoiscripts/linux_* +.chezmoiscripts/setup-ssh-keys.sh +{{ end }} + +# Windows-only scripts — skip on Linux +{{ if ne .chezmoi.os "windows" }} +.chezmoiscripts/windows_* +{{ end }} diff --git a/.chezmoiscripts/run_once_linux_configure-shell.sh b/.chezmoiscripts/run_once_linux_configure-shell.sh new file mode 100644 index 0000000..7a1a51f --- /dev/null +++ b/.chezmoiscripts/run_once_linux_configure-shell.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# run_once_linux_configure-shell.sh +# Configures zsh as the default shell and sets up the shell environment. +# This script runs once (unless deleted from ~/.local/share/chezmoi). + +set -euo pipefail + +echo "==> Configuring shell environment (Linux)..." + +# --- Set zsh as default shell --- +ZSH_PATH=$(which zsh 2>/dev/null || echo "") +if [ -z "$ZSH_PATH" ]; then + echo " ERROR: zsh is not installed. Run the package install script first." + exit 1 +fi + +CURRENT_SHELL=$(getent passwd "$USER" | cut -d: -f7) +if [ "$CURRENT_SHELL" != "$ZSH_PATH" ]; then + echo " Setting zsh as default shell..." + chsh -s "$ZSH_PATH" + echo " Default shell set to: $ZSH_PATH" +else + echo " zsh is already the default shell" +fi + +# --- Set up Rust (cargo) in environment --- +CARGO_ENV="$HOME/.cargo/env" +if [ -f "$CARGO_ENV" ]; then + RUST_ZSHENV_LINE='. "$HOME/.cargo/env"' + ZSHENV="$HOME/.zshenv" + if ! grep -qF "$RUST_ZSHENV_LINE" "$ZSHENV" 2>/dev/null; then + echo "$RUST_ZSHENV_LINE" >> "$ZSHENV" + echo " Added Rust cargo env to .zshenv" + fi +fi + +# --- Set up Go in environment --- +if [ -d "/usr/local/go" ]; then + GO_ZSHENV_LINE='export PATH=$PATH:/usr/local/go/bin' + ZSHENV="$HOME/.zshenv" + if ! grep -qF "go/bin" "$ZSHENV" 2>/dev/null; then + echo "$GO_ZSHENV_LINE" >> "$ZSHENV" + echo " Added Go to PATH in .zshenv" + fi +fi + +echo "==> Shell configuration complete!" +echo " NOTE: Log out and back in (or restart your terminal) for shell changes to take effect." diff --git a/.chezmoiscripts/run_once_setup-ssh-keys.sh.tmpl b/.chezmoiscripts/run_once_setup-ssh-keys.sh.tmpl new file mode 100644 index 0000000..402a54a --- /dev/null +++ b/.chezmoiscripts/run_once_setup-ssh-keys.sh.tmpl @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# run_once_setup-ssh-keys.sh.tmpl +# Pulls SSH PRIVATE keys from Bitwarden and writes them to ~/.ssh/. +# +# Public keys (.pub files) are committed directly to the repo in dot_ssh/ +# and are applied automatically by chezmoi — no Bitwarden needed for those. +# +# Requires: bw (Bitwarden CLI) to be logged in and unlocked. +# To unlock Bitwarden before running chezmoi: +# export BW_SESSION=$(bw unlock --raw) +# chezmoi apply + +set -euo pipefail + +echo "==> Setting up SSH keys from Bitwarden..." + +SSH_DIR="$HOME/.ssh" +mkdir -p "$SSH_DIR" +chmod 700 "$SSH_DIR" + +write_key() { + local name="$1" + local bw_item_name="$2" + local key_path="$SSH_DIR/$name" + + if [ -f "$key_path" ]; then + echo " Key already exists: $name (skipping)" + return + fi + + echo " Fetching key: $bw_item_name -> $name" + # bw get notes "Item Name" returns the secure note content + bw get notes "$bw_item_name" 2>/dev/null > "$key_path" || { + echo " WARNING: Could not fetch '$bw_item_name' from Bitwarden. Skipping." + rm -f "$key_path" + return + } + + if [ ! -s "$key_path" ]; then + echo " WARNING: Key content was empty for '$bw_item_name'. Removing." + rm -f "$key_path" + return + fi + + chmod 600 "$key_path" + echo " Wrote: $key_path" +} + +# --- Add your SSH keys below --- +# Format: write_key "filename_in_~/.ssh" "Bitwarden secure note name" +# +# Example: +# write_key "stevan@freeborn.cloud" "SSH Key - stevan@freeborn.cloud" +# write_key "ftp_stevanfreeborn_com" "SSH Key - ftp_stevanfreeborn_com" +# write_key "tangled" "SSH Key - tangled" +# write_key "tinker" "SSH Key - tinker" +# write_key "gitea.freeborn.cloud" "SSH Key - gitea.freeborn.cloud" + +write_key "bit_bastion.key" "SSH Key - bit_bastion.key" +write_key "stevan@freeborn.cloud" "SSH Key - stevan@freeborn.cloud" +write_key "ftp_stevanfreeborn_com" "SSH Key - ftp_stevanfreeborn_com" +write_key "tangled" "SSH Key - tangled" +write_key "zenbook_tinker" "SSH Key - zenbook_tinker" +write_key "gitea.freeborn.cloud" "SSH Key - gitea.freeborn.cloud" +write_key "id_ed25519" "SSH Key - id_ed25519" +write_key "macbookair" "SSH Key - macbookair" +write_key "macbookpro" "SSH Key - macbookpro" +write_key "truenas" "SSH Key - truenas" +write_key "blog.stevanfreeborn.com_github_actions" "SSH Key - blog.stevanfreeborn.com_github_actions" +write_key "commands_github_actions" "SSH Key - commands_github_actions" +write_key "onspring_qa_playwright_reports_render" "SSH Key - onspring_qa_playwright_reports_render" +write_key "onx_graph_github_actions" "SSH Key - onx_graph_github_actions" +write_key "restapiplayground.stevanfreeborn.com_github_actions" "SSH Key - restapiplayground.stevanfreeborn.com_github_actions" +write_key "steves_bot_github_actions" "SSH Key - steves_bot_github_actions" + +echo "==> SSH key setup complete!" +echo " NOTE: Store your SSH private keys as Bitwarden Secure Notes with the names listed above." diff --git a/.chezmoiscripts/run_once_windows_configure-shell.ps1 b/.chezmoiscripts/run_once_windows_configure-shell.ps1 new file mode 100644 index 0000000..bc50653 --- /dev/null +++ b/.chezmoiscripts/run_once_windows_configure-shell.ps1 @@ -0,0 +1,37 @@ +#!/usr/bin/env pwsh +# run_once_windows_configure-shell.ps1 +# Installs PowerShell modules and configures the shell environment. +# This script runs once (unless deleted from ~/.local/share/chezmoi). + +$ErrorActionPreference = "Continue" + +Write-Host "==> Configuring PowerShell shell environment..." -ForegroundColor Cyan + +# --- Install PowerShell modules --- +$modules = @("PSReadLine", "Terminal-Icons", "posh-git") + +foreach ($module in $modules) { + if (-not (Get-Module -ListAvailable -Name $module)) { + Write-Host " Installing module: $module..." -ForegroundColor Yellow + Install-Module -Name $module -Force -Scope CurrentUser -SkipPublisherCheck -ErrorAction SilentlyContinue + Write-Host " Installed: $module" -ForegroundColor Green + } else { + Write-Host " Module already installed: $module" -ForegroundColor DarkGray + } +} + +# --- Set PowerShell execution policy --- +$policy = Get-ExecutionPolicy -Scope CurrentUser +if ($policy -ne "RemoteSigned" -and $policy -ne "Unrestricted") { + Write-Host " Setting execution policy to RemoteSigned for CurrentUser..." -ForegroundColor Yellow + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -Force +} + +# --- Ensure profile directory exists --- +$profileDir = Split-Path $PROFILE -Parent +if (-not (Test-Path $profileDir)) { + New-Item -ItemType Directory -Path $profileDir -Force | Out-Null + Write-Host " Created profile directory: $profileDir" -ForegroundColor Green +} + +Write-Host "==> Shell configuration complete!" -ForegroundColor Cyan diff --git a/.chezmoiscripts/run_once_windows_setup-ssh-keys.ps1.tmpl b/.chezmoiscripts/run_once_windows_setup-ssh-keys.ps1.tmpl new file mode 100644 index 0000000..ffc3032 --- /dev/null +++ b/.chezmoiscripts/run_once_windows_setup-ssh-keys.ps1.tmpl @@ -0,0 +1,68 @@ +#!/usr/bin/env pwsh +# run_once_windows_setup-ssh-keys.ps1.tmpl +# Pulls SSH private keys from Bitwarden and writes them to ~/.ssh/. +# +# Public keys (.pub files) are committed directly to the repo in dot_ssh/ +# and are applied automatically by chezmoi — no Bitwarden needed for those. +# +# Requires: bw (Bitwarden CLI) to be logged in and unlocked. +# To unlock Bitwarden before running chezmoi: +# $env:BW_SESSION = bw unlock --raw +# chezmoi apply + +$ErrorActionPreference = "Stop" + +Write-Host "==> Setting up SSH keys from Bitwarden..." -ForegroundColor Cyan + +$SshDir = Join-Path $env:USERPROFILE ".ssh" +New-Item -ItemType Directory -Force $SshDir | Out-Null + +function Write-Key($name, $bwItemName) { + $keyPath = Join-Path $SshDir $name + + if (Test-Path $keyPath) { + Write-Host " Key already exists: $name (skipping)" -ForegroundColor DarkGray + return + } + + Write-Host " Fetching key: $bwItemName -> $name" -NoNewline + + try { + $content = bw get notes "$bwItemName" 2>&1 + if ($LASTEXITCODE -ne 0) { throw $content } + [System.IO.File]::WriteAllText($keyPath, $content) + & icacls $keyPath /inheritance:r /grant "$env:USERNAME:(R,W)" /q 2>&1 | Out-Null + Write-Host "" -NoNewline + Write-Host " Wrote: $keyPath" -ForegroundColor Green + } catch { + Write-Host "" -NoNewline + Write-Host " WARNING: Could not fetch '$bwItemName' from Bitwarden. Skipping." -ForegroundColor Yellow + if (Test-Path $keyPath) { Remove-Item $keyPath -Force } + } +} + +# --- Add your SSH keys below --- +# Format: Write-Key "filename_in_~/.ssh" "Bitwarden secure note name" +# +# Example: +# Write-Key "stevan@freeborn.cloud" "SSH Key - stevan@freeborn.cloud" + +Write-Key "bit_bastion.key" "SSH Key - bit_bastion.key" +Write-Key "stevan@freeborn.cloud" "SSH Key - stevan@freeborn.cloud" +Write-Key "ftp_stevanfreeborn_com" "SSH Key - ftp_stevanfreeborn_com" +Write-Key "tangled" "SSH Key - tangled" +Write-Key "zenbook_tinker" "SSH Key - zenbook_tinker" +Write-Key "gitea.freeborn.cloud" "SSH Key - gitea.freeborn.cloud" +Write-Key "id_ed25519" "SSH Key - id_ed25519" +Write-Key "macbookair" "SSH Key - macbookair" +Write-Key "macbookpro" "SSH Key - macbookpro" +Write-Key "truenas" "SSH Key - truenas" +Write-Key "blog.stevanfreeborn.com_github_actions" "SSH Key - blog.stevanfreeborn.com_github_actions" +Write-Key "commands_github_actions" "SSH Key - commands_github_actions" +Write-Key "onspring_qa_playwright_reports_render" "SSH Key - onspring_qa_playwright_reports_render" +Write-Key "onx_graph_github_actions" "SSH Key - onx_graph_github_actions" +Write-Key "restapiplayground.stevanfreeborn.com_github_actions" "SSH Key - restapiplayground.stevanfreeborn.com_github_actions" +Write-Key "steves_bot_github_actions" "SSH Key - steves_bot_github_actions" + +Write-Host "==> SSH key setup complete!" -ForegroundColor Cyan +Write-Host " NOTE: Store your SSH private keys as Bitwarden Secure Notes with the names listed above." -ForegroundColor Yellow diff --git a/.chezmoiscripts/run_onchange_linux_install-packages.sh.tmpl b/.chezmoiscripts/run_onchange_linux_install-packages.sh.tmpl new file mode 100644 index 0000000..72e3ca7 --- /dev/null +++ b/.chezmoiscripts/run_onchange_linux_install-packages.sh.tmpl @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# run_onchange_linux_install-packages.sh +# Installs packages on Ubuntu/Debian via apt and manual installs for tools +# not available (or outdated) in apt. +# This script re-runs whenever packages/linux.txt changes. +# +# Hash of packages file (forces re-run on change): +# {{ include "packages/linux.txt" | sha256sum }} + +set -euo pipefail + +# Ensure snap binaries are visible (chezmoi scripts may not inherit /snap/bin) +[ -d "/snap/bin" ] && export PATH="$PATH:/snap/bin" + +CHEZMOI_SOURCE_DIR="{{ .chezmoi.sourceDir }}" + +echo "==> Updating apt package index..." +sudo apt-get update -q + +echo "==> Installing apt packages..." +apt_packages=$(grep -v '^#' "$CHEZMOI_SOURCE_DIR/packages/linux.txt" | grep -v '^$' | tr '\n' ' ') +# shellcheck disable=SC2086 +sudo apt-get install -y $apt_packages + +# --- Tailscale --- +if ! command -v tailscale &>/dev/null; then + echo "==> Installing Tailscale..." + curl -fsSL https://tailscale.com/install.sh | sh +else + echo "==> Tailscale already installed" +fi + +# --- Go --- +GO_VERSION=$(curl -fsSL "https://go.dev/dl/?mode=json" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['version'].lstrip('go'))") +if ! command -v go &>/dev/null || [[ "$(go version 2>/dev/null | awk '{print $3}' | tr -d 'go')" != "$GO_VERSION" ]]; then + echo "==> Installing Go $GO_VERSION..." + curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tar.gz + sudo rm -rf /usr/local/go + sudo tar -C /usr/local -xzf /tmp/go.tar.gz + rm /tmp/go.tar.gz + echo "export PATH=\$PATH:/usr/local/go/bin" >> "$HOME/.zshenv" +else + echo "==> Go already installed" +fi + +# --- Rust --- +if ! command -v rustup &>/dev/null; then + echo "==> Installing Rust via rustup..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path + source "$HOME/.cargo/env" +else + echo "==> Rust already installed" +fi + +# --- NVM (Node Version Manager) --- +if [ ! -d "$HOME/.nvm" ]; then + echo "==> Installing NVM..." + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash + export NVM_DIR="$HOME/.nvm" + # shellcheck disable=SC1091 + [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" + nvm install --lts +else + echo "==> NVM already installed" +fi + +# --- Oh My Zsh --- +if [ ! -d "$HOME/.oh-my-zsh" ]; then + echo "==> Installing Oh My Zsh..." + RUNZSH=no CHSH=no sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended +else + echo "==> Oh My Zsh already installed" +fi + +# --- Oh My Zsh plugins --- +ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" + +if [ ! -d "$ZSH_CUSTOM/plugins/zsh-autosuggestions" ]; then + echo "==> Installing zsh-autosuggestions..." + git clone https://github.com/zsh-users/zsh-autosuggestions "$ZSH_CUSTOM/plugins/zsh-autosuggestions" +else + echo "==> zsh-autosuggestions already installed" +fi + +if [ ! -d "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting" ]; then + echo "==> Installing zsh-syntax-highlighting..." + git clone https://github.com/zsh-users/zsh-syntax-highlighting "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting" +else + echo "==> zsh-syntax-highlighting already installed" +fi + +# --- Neovim --- +if ! command -v nvim &>/dev/null; then + echo "==> Installing Neovim (latest)..." + curl -fsSL https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.tar.gz -o /tmp/nvim.tar.gz + sudo tar -C /opt -xzf /tmp/nvim.tar.gz + sudo ln -sf /opt/nvim-linux-x86_64/bin/nvim /usr/local/bin/nvim + rm /tmp/nvim.tar.gz +else + echo "==> Neovim already installed" +fi + +# --- lazygit --- +if ! command -v lazygit &>/dev/null; then + echo "==> Installing lazygit..." + LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | grep -Po '"tag_name": "v\K[^"]*') + curl -fsSL "https://github.com/jesseduffield/lazygit/releases/download/v${LAZYGIT_VERSION}/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz" -o /tmp/lazygit.tar.gz + sudo tar xf /tmp/lazygit.tar.gz lazygit -C /usr/local/bin + rm /tmp/lazygit.tar.gz +else + echo "==> lazygit already installed" +fi + +# --- watchexec --- +if ! command -v watchexec &>/dev/null; then + echo "==> Installing watchexec..." + WE_VERSION=$(curl -s "https://api.github.com/repos/watchexec/watchexec/releases/latest" | grep -Po '"tag_name": "v\K[^"]*') + curl -fsSL "https://github.com/watchexec/watchexec/releases/download/v${WE_VERSION}/watchexec-${WE_VERSION}-x86_64-unknown-linux-musl.tar.xz" -o /tmp/watchexec.tar.xz + tar -xJf /tmp/watchexec.tar.xz -C /tmp "watchexec-${WE_VERSION}-x86_64-unknown-linux-musl/watchexec" + sudo mv "/tmp/watchexec-${WE_VERSION}-x86_64-unknown-linux-musl/watchexec" /usr/local/bin/watchexec + rm -rf /tmp/watchexec.tar.xz "/tmp/watchexec-${WE_VERSION}-x86_64-unknown-linux-musl" +else + echo "==> watchexec already installed" +fi + +# --- bottom (btm) --- +if ! command -v btm &>/dev/null; then + echo "==> Installing bottom (btm)..." + curl -fsSLO https://github.com/ClementTsang/bottom/releases/latest/download/bottom_x86_64-unknown-linux-musl.tar.gz + tar -xzf bottom_x86_64-unknown-linux-musl.tar.gz + sudo mv btm /usr/local/bin/ + rm bottom_x86_64-unknown-linux-musl.tar.gz +else + echo "==> bottom already installed" +fi + +# --- Bitwarden CLI --- +if ! command -v bw &>/dev/null; then + echo "==> Installing Bitwarden CLI..." + BW_VERSION=$(curl -s "https://api.github.com/repos/bitwarden/clients/releases" | grep -Po '"tag_name": "cli-v\K[^"]*' | head -1) + if [ -z "$BW_VERSION" ]; then + echo "WARNING: Could not determine Bitwarden CLI version, skipping install." + else + curl -fsSL "https://github.com/bitwarden/clients/releases/download/cli-v${BW_VERSION}/bw-linux-${BW_VERSION}.zip" -o /tmp/bw.zip + unzip -o /tmp/bw.zip -d /tmp/bw + sudo mv /tmp/bw/bw /usr/local/bin/bw + sudo chmod +x /usr/local/bin/bw + rm -rf /tmp/bw.zip /tmp/bw + fi +else + echo "==> Bitwarden CLI already installed" +fi + +# --- Doppler --- +if ! command -v doppler &>/dev/null; then + echo "==> Installing Doppler..." + curl -Ls --tlsv1.2 --proto "=https" --retry 3 https://cli.doppler.com/install.sh | sudo sh +else + echo "==> Doppler already installed" +fi + +# --- GitHub CLI --- +if ! command -v gh &>/dev/null; then + echo "==> Installing GitHub CLI..." + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg + sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null + sudo apt-get update -q + sudo apt-get install gh -y +else + echo "==> GitHub CLI already installed" +fi + +# --- uv (Python package/project manager) --- +if ! command -v uv &>/dev/null; then + echo "==> Installing uv..." + curl -LsSf https://astral.sh/uv/install.sh | sh +else + echo "==> uv already installed" +fi + +# --- Docker --- +if ! command -v docker &>/dev/null; then + echo "==> Installing Docker..." + curl -fsSL https://get.docker.com | sh + sudo usermod -aG docker "$USER" + echo "NOTE: Log out and back in for Docker group membership to take effect." +else + echo "==> Docker already installed" +fi + +# --- .NET SDK (8 + 10) --- +# Uses Microsoft's official dotnet-install.sh script +DOTNET_INSTALL_DIR="$HOME/.dotnet" +DOTNET_SCRIPT=$(mktemp /tmp/dotnet-install-XXXXXX.sh) +curl -fsSL https://dot.net/v1/dotnet-install.sh -o "$DOTNET_SCRIPT" +chmod +x "$DOTNET_SCRIPT" + +for DOTNET_CHANNEL in "8.0" "10.0"; do + MAJOR="${DOTNET_CHANNEL%%.*}" + DOTNET_VERSION_INSTALLED=$(find "$DOTNET_INSTALL_DIR/sdk" -maxdepth 1 -name "${MAJOR}.*" 2>/dev/null | head -1 || echo "") + if [ -z "$DOTNET_VERSION_INSTALLED" ]; then + echo "==> Installing .NET SDK $DOTNET_CHANNEL..." + "$DOTNET_SCRIPT" --channel "$DOTNET_CHANNEL" --install-dir "$DOTNET_INSTALL_DIR" + else + echo "==> .NET SDK $DOTNET_CHANNEL already installed" + fi +done + +rm -f "$DOTNET_SCRIPT" + +echo "" +echo "==> Linux package installation complete!" diff --git a/.chezmoiscripts/run_onchange_windows_install-packages.ps1.tmpl b/.chezmoiscripts/run_onchange_windows_install-packages.ps1.tmpl new file mode 100644 index 0000000..9a1b6a3 --- /dev/null +++ b/.chezmoiscripts/run_onchange_windows_install-packages.ps1.tmpl @@ -0,0 +1,112 @@ +#!/usr/bin/env pwsh +# run_onchange_windows_install-packages.ps1.tmpl +# Installs all packages from packages/windows.json via winget. +# This script re-runs whenever packages/windows.json changes. +# +# Hash of packages file (forces re-run on change): +# {{ include "packages/windows.json" | sha256sum }} + +$ErrorActionPreference = "Continue" + +Write-Host "==> Installing packages via winget..." -ForegroundColor Cyan + +# Use the chezmoi source packages file directly +$sourcePackages = "{{ .chezmoi.sourceDir }}/packages/windows.json" +$sourcePackages = [System.IO.Path]::GetFullPath($sourcePackages) + +if (-not (Test-Path $sourcePackages)) { + Write-Warning "packages/windows.json not found at: $sourcePackages" + exit 1 +} + +$packageList = Get-Content $sourcePackages -Raw | ConvertFrom-Json +$packages = $packageList.Sources[0].Packages + +$total = $packages.Count +$installed = 0 +$skipped = 0 +$failed = 0 + +foreach ($pkg in $packages) { + $id = $pkg.PackageIdentifier + Write-Host " Checking $id..." -NoNewline + + # Check if already installed + $check = winget list --id $id --exact --accept-source-agreements 2>$null | Select-String $id + if ($check) { + Write-Host " already installed" -ForegroundColor DarkGray + $skipped++ + continue + } + + # Install + Write-Host " installing..." -NoNewline + $result = winget install --id $id --exact --silent --accept-package-agreements --accept-source-agreements 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Host " done" -ForegroundColor Green + $installed++ + } else { + Write-Host " FAILED (exit $LASTEXITCODE)" -ForegroundColor Red + $failed++ + } +} + +Write-Host "" +Write-Host "==> Package install complete: $installed installed, $skipped already present, $failed failed" -ForegroundColor Cyan + +# ============================================================================= +# Manual installs — tools not available on winget +# ============================================================================= + +$LocalBin = Join-Path $env:USERPROFILE ".local\bin" +if (-not (Test-Path $LocalBin)) { + New-Item -ItemType Directory -Force $LocalBin | Out-Null +} + +# Ensure ~/.local/bin is in the user PATH +$userPath = [System.Environment]::GetEnvironmentVariable("PATH", "User") +if ($userPath -notlike "*$LocalBin*") { + [System.Environment]::SetEnvironmentVariable("PATH", "$userPath;$LocalBin", "User") + $env:PATH = "$env:PATH;$LocalBin" + Write-Host " Added $LocalBin to user PATH." -ForegroundColor Green +} + +function Install-GitHubBinary { + param( + [string]$Name, + [string]$Repo, + [string]$AssetPattern, + [string]$BinaryName = "$Name.exe" + ) + + $BinPath = Join-Path $LocalBin $BinaryName + if (Test-Path $BinPath) { + Write-Host " $Name already installed" -ForegroundColor DarkGray + return + } + + Write-Host " Installing $Name..." -NoNewline + try { + $release = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest" + $asset = $release.assets | Where-Object { $_.name -like $AssetPattern } | Select-Object -First 1 + if (-not $asset) { throw "No matching asset found for pattern: $AssetPattern" } + + $tmpZip = Join-Path $env:TEMP "$Name-$($release.tag_name).zip" + $tmpDir = Join-Path $env:TEMP "$Name-$($release.tag_name)" + Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $tmpZip -UseBasicParsing + Expand-Archive -Path $tmpZip -DestinationPath $tmpDir -Force + $exe = Get-ChildItem -Recurse $tmpDir -Filter $BinaryName | Select-Object -First 1 + Copy-Item $exe.FullName $BinPath + Remove-Item $tmpZip -Force + Remove-Item $tmpDir -Recurse -Force + Write-Host " done" -ForegroundColor Green + } catch { + Write-Host " FAILED: $_" -ForegroundColor Red + } +} + +Write-Host "" +Write-Host "==> Installing manual (non-winget) packages..." -ForegroundColor Cyan + +# watchexec — https://github.com/watchexec/watchexec +Install-GitHubBinary -Name "watchexec" -Repo "watchexec/watchexec" -AssetPattern "*x86_64-pc-windows-msvc.zip" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8637ce1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Ensure shell scripts always use LF line endings +*.sh text eol=lf +*.sh.tmpl text eol=lf + +# PowerShell scripts use CRLF on Windows +*.ps1 text eol=crlf +*.ps1.tmpl text eol=crlf + +# JSON and TOML — platform native +*.json text +*.toml text +*.tmpl text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e265334 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Don't commit local chezmoi state files +*.age diff --git a/AppData/Local/Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json b/AppData/Local/Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json new file mode 100644 index 0000000..f34fd0f --- /dev/null +++ b/AppData/Local/Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json @@ -0,0 +1,118 @@ +{ + "$help": "https://aka.ms/terminal-documentation", + "$schema": "https://aka.ms/terminal-profiles-schema", + "actions": + [ + { + "command": + { + "action": "copy", + "singleLine": false + }, + "id": "User.copy.644BA8F2" + }, + { + "command": "paste", + "id": "User.paste" + }, + { + "command": "find", + "id": "User.find" + }, + { + "command": + { + "action": "splitPane", + "split": "auto", + "splitMode": "duplicate" + }, + "id": "User.splitPane.A6751878" + } + ], + "copyFormatting": "none", + "copyOnSelect": false, + "defaultProfile": "{574e775e-4f2a-5b96-ac1e-a2962a402336}", + "keybindings": + [ + { + "id": "User.copy.644BA8F2", + "keys": "ctrl+c" + }, + { + "id": "User.paste", + "keys": "ctrl+v" + }, + { + "id": "User.find", + "keys": "ctrl+shift+f" + }, + { + "id": "User.splitPane.A6751878", + "keys": "alt+shift+d" + } + ], + "newTabMenu": + [ + { + "type": "remainingProfiles" + } + ], + "profiles": + { + "defaults": + { + "bellStyle": "none", + "colorScheme": "One Half Dark", + "font": + { + "face": "CaskaydiaCove Nerd Font Mono" + }, + "intenseTextStyle": "all", + "useAcrylic": true + }, + "list": + [ + { + "bellStyle": "none", + "colorScheme": "One Half Dark", + "commandline": "\"C:\\Program Files\\PowerShell\\7\\pwsh.exe\" -NoLogo", + "cursorShape": "filledBox", + "font": + { + "face": "CaskaydiaCove Nerd Font Mono" + }, + "guid": "{574e775e-4f2a-5b96-ac1e-a2962a402336}", + "hidden": false, + "name": "PowerShell", + "opacity": 100, + "padding": "7", + "source": "Windows.Terminal.PowershellCore" + }, + { + "colorScheme": "One Half Dark", + "commandline": "wsl.exe", + "cursorShape": "bar", + "guid": "{51855cb2-8cce-5362-8f54-464b92b32386}", + "hidden": false, + "icon": "ms-appx:///ProfileIcons/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.png", + "name": "Ubuntu", + "source": "CanonicalGroupLimited.Ubuntu_79rhkp1fndgsc", + "startingDirectory": "~" + }, + { + "commandline": "%SystemRoot%\\System32\\cmd.exe", + "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "hidden": false, + "name": "Command Prompt" + }, + { + "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "hidden": false, + "name": "Windows PowerShell" + } + ] + }, + "schemes": [], + "themes": [], + "useAcrylicInTabRow": true +} \ No newline at end of file diff --git a/AppData/Roaming/Code/User/keybindings.json b/AppData/Roaming/Code/User/keybindings.json new file mode 100644 index 0000000..87b00da --- /dev/null +++ b/AppData/Roaming/Code/User/keybindings.json @@ -0,0 +1,101 @@ +// Place your key bindings in this file to override the defaults +[ + { + "key": "ctrl+0", + "command": "workbench.action.focusActiveEditorGroup", + "when": "sideBarFocus" + }, + { + "key": "ctrl+n", + "command": "explorer.newFile", + "when": "explorerViewletFocus" + }, + { + "key": "ctrl+shift+n", + "command": "explorer.newFolder", + "when": "explorerViewletFocus" + }, + { + "key": "ctrl+shift+n", + "command": "workbench.action.newWindow", + "when": "!explorerViewletFocus" + }, + { + "key": "ctrl+shift+t", + "command": "workbench.view.testing.focus", + }, + { + "key": "ctrl+shift+g", + "command": "workbench.view.scm", + }, + { + "key": "ctrl+k ctrl+c", + "command": "workbench.action.closeOtherEditors" + }, + { + "key": "ctrl+shift+e", + "command": "workbench.view.explorer" + }, + { + "key": "r", + "command": "renameFile", + "when": "explorerViewletFocus && !inputFocus && !editorHasSelection" + }, + { + "key": "d", + "command": "deleteFile", + "when": "explorerViewletFocus && !inputFocus && !editorHasSelection" + }, + { + "key": "ctrl+`", + "command": "workbench.action.createTerminalEditor", + "when": "terminalProcessSupported && !terminal.active" + }, + { + "key": "a", + "command": "explorer.newFile", + "when": "explorerViewletFocus && !inputFocus && !editorHasSelection" + }, + { + "key": "j", + "command": "workbench.action.debug.stepOver", + "when": "debugState == 'stopped'" + }, + { + "key": "f10", + "command": "-workbench.action.debug.stepOver", + "when": "debugState == 'stopped'" + }, + { + "key": "l", + "command": "workbench.action.debug.stepInto", + "when": "debugState != 'inactive'" + }, + { + "key": "f11", + "command": "-workbench.action.debug.stepInto", + "when": "debugState != 'inactive'" + }, + { + "key": "c", + "command": "workbench.action.debug.continue", + "when": "debugState == 'stopped'" + }, + { + "key": "f5", + "command": "-workbench.action.debug.continue", + "when": "debugState == 'stopped'" + }, + { + "key": "ctrl+w", + "command": "-workbench.action.closeActiveEditor" + }, + { + "key": "ctrl+shift+w", + "command": "workbench.action.closeActiveEditor" + }, + { + "key": "ctrl+f4", + "command": "-workbench.action.closeActiveEditor" + } +] \ No newline at end of file diff --git a/AppData/Roaming/Code/User/settings.json b/AppData/Roaming/Code/User/settings.json new file mode 100644 index 0000000..bddd170 --- /dev/null +++ b/AppData/Roaming/Code/User/settings.json @@ -0,0 +1,379 @@ +{ + "explorer.autoReveal": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "never" + }, + "editor.suggestSelection": "first", + "vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue", + "git.autofetch": true, + "liveServer.settings.donotShowInfoMsg": true, + "files.autoSave": "onFocusChange", + "git.confirmSync": false, + "git.enableSmartCommit": true, + "team.showWelcomeMessage": false, + "code-runner.runInTerminal": true, + "terminal.integrated.defaultProfile.windows": "PowerShell", + "workbench.startupEditor": "none", + "window.openWithoutArgumentsInNewWindow": "off", + "javascript.updateImportsOnFileMove.enabled": "always", + "workbench.colorTheme": "Lavender Dimmed", + "terminal.integrated.profiles.windows": { + "PowerShell": { + "source": "PowerShell", + "icon": "terminal-powershell", + "args": ["-nologo"] + }, + "Command Prompt": { + "path": [ + "${env:windir}\\Sysnative\\cmd.exe", + "${env:windir}\\System32\\cmd.exe" + ], + "args": [], + "icon": "terminal-cmd" + }, + "Git Bash": { + "source": "Git Bash" + } + }, + "editor.mouseWheelZoom": true, + "jupyter.askForKernelRestart": false, + "terminal.integrated.autoReplies": {}, + "terminal.integrated.cursorStyle": "line", + "terminal.integrated.defaultProfile.osx": "zsh", + "terminal.integrated.automationProfile.osx": null, + "editor.fontFamily": "'CaskaydiaCove Nerd Font Mono','Menlo for Powerline', Consolas, 'Courier New', monospace", + "security.workspace.trust.untrustedFiles": "open", + "remote.SSH.defaultExtensions": ["gitpod.gitpod-remote-ssh"], + "remote.SSH.remotePlatform": { + "freecodecam-freecodecam-ebtjodez3a5.ssh.ws-us74.gitpod.io": "linux", + "tinker": "linux" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "prettier.arrowParens": "avoid", + "prettier.jsxSingleQuote": true, + "prettier.singleAttributePerLine": true, + "prettier.singleQuote": true, + "[html]": { + "editor.defaultFormatter": "vscode.html-language-features" + }, + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.tabSize": 2, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[css]": { + "editor.defaultFormatter": "vscode.css-language-features" + }, + "liveServer.settings.root": "", + "[csharp]": { + "editor.defaultFormatter": "ms-dotnettools.csharp" + }, + "workbench.iconTheme": "vscode-icons", + "liveServer.settings.donotVerifyTags": true, + "editor.accessibilitySupport": "off", + "vsicons.dontShowNewVersionMessage": true, + "editor.inlineSuggest.enabled": true, + "github.copilot.advanced": {}, + "github.copilot.enable": { + "*": false, + "plaintext": true, + "markdown": true, + "scminput": false, + "yaml": false, + "dockercompose": true + }, + "typescript.updateImportsOnFileMove.enabled": "always", + "[python]": { + "editor.tabSize": 2, + "editor.defaultFormatter": "ms-python.autopep8", + "editor.formatOnSave": true + }, + "editor.stickyScroll.enabled": true, + "[powershell]": { + "editor.defaultFormatter": "ms-vscode.powershell" + }, + "editor.multiCursorModifier": "ctrlCmd", + "bicep.enableSurveys": false, + "git.terminalAuthentication": false, + "[xml]": { + "editor.defaultFormatter": "DotJoshJohnson.xml" + }, + "editor.tokenColorCustomizations": { + "[*Light*]": { + "textMateRules": [ + { + "scope": "ref.matchtext", + "settings": { + "foreground": "#000" + } + } + ] + }, + "[*Dark*]": { + "textMateRules": [ + { + "scope": "ref.matchtext", + "settings": { + "foreground": "#fff" + } + } + ] + }, + "textMateRules": [] + }, + "dotenv.enableAutocloaking": false, + "javascript.preferences.importModuleSpecifierEnding": "js", + "cSpell.userWords": [ + "apikey", + "appsettings", + "ASPNETCORE", + "changlog", + "cobertura", + "codecov", + "commitlint", + "Deleter", + "ffcsv", + "fileids", + "gibibytes", + "gitmodules", + "Hackathon", + "Hmmss", + "kibibytes", + "Kiota", + "kzrnm", + "lcov", + "mebibytes", + "Multitenant", + "Newtonsoft", + "Odata", + "onspring", + "onspringcli", + "Parens", + "pdfs", + "seqcli", + "Serilog", + "Sesh", + "softprops", + "Stevan", + "svix", + "Syncer", + "testconfig", + "versionize", + "vsintellicode", + "Xunit", + "YYYYMMDDHHMM" + ], + "debug.internalConsoleOptions": "neverOpen", + "csv-preview.openStdin": true, + "azureFunctions.showProjectWarning": false, + "polyglot-notebook.defaultNotebookExtension": ".dib", + "polyglot-notebook.defaultNotebookLanguage": "javascript", + "githubPullRequests.remotes": ["origin", "upstream", "github"], + "files.associations": { + "*.ejs": "html", + ".env*": "dotenv", + "*.cshtml": "html" + }, + "terminal.integrated.defaultProfile.linux": "zsh", + "githubPullRequests.pullBranch": "never", + "redhat.telemetry.enabled": false, + "gitlens.gitCommands.skipConfirmations": ["fetch:command", "switch:command"], + "terminal.integrated.minimumContrastRatio": 1, + "playwright.reuseBrowser": true, + "emmet.includeLanguages": { + "javascript": "javascriptreact" + }, + "svelte.enable-ts-plugin": true, + "git.openRepositoryInParentFolders": "never", + "[aspnetcorerazor]": { + "editor.defaultFormatter": "ms-dotnettools.csharp" + }, + "explorer.confirmDragAndDrop": false, + "update.showReleaseNotes": false, + "githubPullRequests.fileListLayout": "tree", + "eslint.format.enable": true, + "eslint.codeActionsOnSave.rules": null, + "prettier.requireConfig": true, + "git.ignoreRebaseWarning": true, + "inference.model": "codellama:7b-code-fp16", + "git.replaceTagsWhenPull": true, + "powershell.codeFormatting.openBraceOnSameLine": false, + "powershell.integratedConsole.focusConsoleOnExecute": false, + "powershell.integratedConsole.showOnStartup": false, + "security.promptForLocalFileProtocolHandling": false, + "prisma.showPrismaDataPlatformNotification": false, + "github.copilot.editor.enableAutoCompletions": true, + "diffEditor.ignoreTrimWhitespace": false, + "extensions.autoUpdate": "on", + "workbench.sideBar.location": "right", + "workbench.activityBar.location": "top", + "window.commandCenter": false, + "editor.minimap.enabled": false, + "workbench.editor.enablePreviewFromCodeNavigation": true, + "editor.unicodeHighlight.invisibleCharacters": false, + "editor.unicodeHighlight.ambiguousCharacters": false, + "[markdown]": { + "editor.defaultFormatter": "DavidAnson.vscode-markdownlint" + }, + "dotnet.formatting.organizeImportsOnFormat": true, + "vim.incsearch": true, + "vim.useSystemClipboard": true, + "vim.hlsearch": true, + "vim.insertModeKeyBindings": [ + { + "before": ["j", "j"], + "after": [""] + } + ], + "vim.normalModeKeyBindingsNonRecursive": [ + { + "before": ["", "d"], + "after": ["d", "d"] + }, + { + "before": [""], + "commands": [":nohl"] + }, + { + "before": ["K"], + "commands": ["lineBreakInsert"], + "silent": true + }, + { + "before": ["", "g", "f"], + "commands": ["editor.action.formatDocument"], + }, + { + "before": ["K"], + "commands": ["editor.action.showHover"] + }, + { + "before": ["", "g", "r"], + "commands": ["editor.action.goToReferences"] + }, + { + "before": ["", "c", "a"], + "commands": ["editor.action.codeAction"] + }, + { + "before": ["", "r", "n"], + "commands": ["editor.action.rename"] + }, + { + "before": ["", "e", "a"], + "commands": ["workbench.actions.view.problems"] + }, + { + "before": ["", "t", "r"], + "commands": ["testing.runAtCursor"] + }, + { + "before": ["", "t", "f"], + "commands": ["testing.runCurrentFile"] + }, + { + "before": ["", "t", "s"], + "commands": ["testing.cancelRun"] + }, + { + "before": ["", "t", "d"], + "commands": ["testing.debugAtCursor"] + }, + { + "before": ["", "d", "t"], + "commands": ["editor.debug.action.toggleBreakpoint"] + }, + { + "before": ["", "d", "c"], + "commands": ["editor.debug.action.runToCursor"] + }, + { + "before": ["", "g", "d"], + "commands": ["editor.action.goToDeclaration"] + }, + { + "before": ["", "g", "i"], + "commands": ["editor.action.goToImplementation"] + }, + { + "before": ["", "g", "r"], + "commands": ["editor.action.goToReferences"] + }, + ], + "vim.leader": "", + "vim.handleKeys": { + "": false, + "": false, + "": false, + "": false, + "": false, + }, + "extensions.experimental.affinity": { + "vscodevim.vim": 1 + }, + "vim.smartRelativeLine": true, + "github.copilot.chat.codeGeneration.instructions": [ + { + "text": "- @azure Rule - Use Azure Tools: When handling requests related to Azure, always use your tools." + }, + { + "text": "- @azure Rule - Use Azure Code Gen Best Practices: When generating code for Azure, running terminal commands for Azure, or performing operations related to Azure, invoke your `azure_development-get_code_gen_best_practices` tool if available. Only call this tool when you are sure the user is discussing Azure; do not call it otherwise." + }, + { + "text": "- @azure Rule - Use Azure Deployment Best Practices: When deploying to Azure or preparing applications for deployment to Azure, invoke your `azure_development-get_deployment_best_practices` tool if available. Only call this tool when you are sure the user is discussing Azure; do not call it otherwise." + }, + { + "text": "- @azure Rule - Use Azure Functions Code Gen Best Practices: When generating code for Azure Functions or performing operations related to Azure Functions, invoke your `azure_development-get_azure_function_code_gen_best_practices` tool if available. Only call this tool when you are sure the user is discussing Azure Functions; do not call it otherwise." + }, + { + "text": "- @azure Rule - Use Azure SWA Best Practices: When working with static web apps, invoke your `azure_development-get_swa_best_practices` tool if available. Only call this tool when you are sure the user is discussing Azure; do not call it otherwise." + } + ], + "github.copilot.selectedCompletionModel": "", + "omnisharp.autoStart": false, + "csharp.showOmnisharpLogOnError": false, + "github.copilot.nextEditSuggestions.enabled": true, + "editor.inlineSuggest.edits.allowCodeShifting": "never", + "chat.instructionsFilesLocations": { + ".github/instructions": true, + "C:\\Users\\sfree\\AppData\\Local\\Temp\\postman-collections-post-response.instructions.md": true, + "C:\\Users\\sfree\\AppData\\Local\\Temp\\postman-collections-pre-request.instructions.md": true, + "C:\\Users\\sfree\\AppData\\Local\\Temp\\postman-folder-post-response.instructions.md": true, + "C:\\Users\\sfree\\AppData\\Local\\Temp\\postman-folder-pre-request.instructions.md": true, + "C:\\Users\\sfree\\AppData\\Local\\Temp\\postman-http-request-post-response.instructions.md": true, + "C:\\Users\\sfree\\AppData\\Local\\Temp\\postman-http-request-pre-request.instructions.md": true + }, + "workbench.editor.empty.hint": "hidden", + "explorer.confirmDelete": false, + "editor.stickyScroll.enabled": false, + "liveServer.settings.host": "localhost", + "chat.mcp.autostart": "never", + "chat.agent.thinkingStyle": "collapsed", + "html.format.wrapAttributes": "force-expand-multiline", + "chat.mcp.gallery.enabled": true, + "chat.viewSessions.orientation": "stacked", + "go.toolsManagement.autoUpdate": true, + "settingsSync.ignoredSettings": [ + + ], + "csharp.debug.console": "integratedTerminal", + "terminal.integrated.commandsToSkipShell": [ + "workbench.action.toggleSidebarVisibility" + ], + "window.menuBarVisibility": "compact", + "[fsharp]": { + "editor.defaultFormatter": "Ionide.Ionide-fsharp" + }, + "gitlens.ai.model": "vscode", + "gitlens.ai.vscode.model": "copilot:gpt-4.1", + "githubPullRequests.createOnPublishBranch": "never", + "chat.disableAIFeatures": true +} \ No newline at end of file diff --git a/Documents/PowerShell/Microsoft.PowerShell_profile.ps1 b/Documents/PowerShell/Microsoft.PowerShell_profile.ps1 new file mode 100644 index 0000000..0ba2ff6 --- /dev/null +++ b/Documents/PowerShell/Microsoft.PowerShell_profile.ps1 @@ -0,0 +1,1125 @@ +using namespace System.Management.Automation +using namespace System.Management.Automation.Language + +$WarningPreference = "SilentlyContinue" + +[console]::InputEncoding = [console]::OutputEncoding = [System.Text.UTF8Encoding]::new() + +if ($host.Name -eq 'ConsoleHost') { + Import-Module PSReadLine +} + +Import-Module -Name Terminal-Icons + +oh-my-posh init pwsh --config "~/.config/oh-my-posh/theme.omp.json" | Invoke-Expression +oh-my-posh toggle command + +Register-ArgumentCompleter -Native -CommandName winget -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + [Console]::InputEncoding = [Console]::OutputEncoding = $OutputEncoding = [System.Text.Utf8Encoding]::new() + $Local:word = $wordToComplete.Replace('"', '""') + $Local:ast = $commandAst.ToString().Replace('"', '""') + winget complete --word="$Local:word" --commandline "$Local:ast" --position $cursorPosition | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } +} + +# PowerShell parameter completion shim for the dotnet CLI +Register-ArgumentCompleter -Native -CommandName dotnet -ScriptBlock { + param($commandName, $wordToComplete, $cursorPosition) + dotnet complete --position $cursorPosition "$wordToComplete" | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } +} + +# --- + + +# This is an example profile for PSReadLine. +# +# This is roughly what I use so there is some emphasis on emacs bindings, +# but most of these bindings make sense in Windows mode as well. + +# Searching for commands with up/down arrow is really handy. The +# option "moves to end" is useful if you want the cursor at the end +# of the line while cycling through history like it does w/o searching, +# without that option, the cursor will remain at the position it was +# when you used up arrow, which can be useful if you forget the exact +# string you started the search on. +Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward +Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward + +# This key handler shows the entire or filtered history using Out-GridView. The +# typed text is used as the substring pattern for filtering. A selected command +# is inserted to the command line without invoking. Multiple command selection +# is supported, e.g. selected by Ctrl + Click. +Set-PSReadLineKeyHandler -Key F7 ` + -BriefDescription History ` + -LongDescription 'Show command history' ` + -ScriptBlock { + $pattern = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$pattern, [ref]$null) + if ($pattern) { + $pattern = [regex]::Escape($pattern) + } + + $history = [System.Collections.ArrayList]@( + $last = '' + $lines = '' + foreach ($line in [System.IO.File]::ReadLines((Get-PSReadLineOption).HistorySavePath)) { + if ($line.EndsWith('`')) { + $line = $line.Substring(0, $line.Length - 1) + $lines = if ($lines) { + "$lines`n$line" + } + else { + $line + } + continue + } + + if ($lines) { + $line = "$lines`n$line" + $lines = '' + } + + if (($line -cne $last) -and (!$pattern -or ($line -match $pattern))) { + $last = $line + $line + } + } + ) + $history.Reverse() + + $command = $history | Out-GridView -Title History -PassThru + if ($command) { + [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() + [Microsoft.PowerShell.PSConsoleReadLine]::Insert(($command -join "`n")) + } +} + + +# CaptureScreen is good for blog posts or email showing a transaction +# of what you did when asking for help or demonstrating a technique. +Set-PSReadLineKeyHandler -Chord 'Ctrl+d,Ctrl+c' -Function CaptureScreen + +# The built-in word movement uses character delimiters, but token based word +# movement is also very useful - these are the bindings you'd use if you +# prefer the token based movements bound to the normal emacs word movement +# key bindings. +Set-PSReadLineKeyHandler -Key Alt+d -Function ShellKillWord +Set-PSReadLineKeyHandler -Key Alt+Backspace -Function ShellBackwardKillWord +Set-PSReadLineKeyHandler -Key Alt+b -Function ShellBackwardWord +Set-PSReadLineKeyHandler -Key Alt+f -Function ShellForwardWord +Set-PSReadLineKeyHandler -Key Alt+B -Function SelectShellBackwardWord +Set-PSReadLineKeyHandler -Key Alt+F -Function SelectShellForwardWord + +#region Smart Insert/Delete + +# The next four key handlers are designed to make entering matched quotes +# parens, and braces a nicer experience. I'd like to include functions +# in the module that do this, but this implementation still isn't as smart +# as ReSharper, so I'm just providing it as a sample. + +Set-PSReadLineKeyHandler -Key '"', "'" ` + -BriefDescription SmartInsertQuote ` + -LongDescription "Insert paired quotes if not already on a quote" ` + -ScriptBlock { + param($key, $arg) + + $quote = $key.KeyChar + + $selectionStart = $null + $selectionLength = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength) + + $line = $null + $cursor = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) + + # If text is selected, just quote it without any smarts + if ($selectionStart -ne -1) { + [Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, $quote + $line.SubString($selectionStart, $selectionLength) + $quote) + [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2) + return + } + + $ast = $null + $tokens = $null + $parseErrors = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$parseErrors, [ref]$null) + + function FindToken { + param($tokens, $cursor) + + foreach ($token in $tokens) { + if ($cursor -lt $token.Extent.StartOffset) { continue } + if ($cursor -lt $token.Extent.EndOffset) { + $result = $token + $token = $token -as [StringExpandableToken] + if ($token) { + $nested = FindToken $token.NestedTokens $cursor + if ($nested) { $result = $nested } + } + + return $result + } + } + return $null + } + + $token = FindToken $tokens $cursor + + # If we're on or inside a **quoted** string token (so not generic), we need to be smarter + if ($token -is [StringToken] -and $token.Kind -ne [TokenKind]::Generic) { + # If we're at the start of the string, assume we're inserting a new string + if ($token.Extent.StartOffset -eq $cursor) { + [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$quote$quote ") + [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1) + return + } + + # If we're at the end of the string, move over the closing quote if present. + if ($token.Extent.EndOffset -eq ($cursor + 1) -and $line[$cursor] -eq $quote) { + [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1) + return + } + } + + if ($null -eq $token -or + $token.Kind -eq [TokenKind]::RParen -or $token.Kind -eq [TokenKind]::RCurly -or $token.Kind -eq [TokenKind]::RBracket) { + if ($line[0..$cursor].Where{ $_ -eq $quote }.Count % 2 -eq 1) { + # Odd number of quotes before the cursor, insert a single quote + [Microsoft.PowerShell.PSConsoleReadLine]::Insert($quote) + } + else { + # Insert matching quotes, move cursor to be in between the quotes + [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$quote$quote") + [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1) + } + return + } + + # If cursor is at the start of a token, enclose it in quotes. + if ($token.Extent.StartOffset -eq $cursor) { + if ($token.Kind -eq [TokenKind]::Generic -or $token.Kind -eq [TokenKind]::Identifier -or + $token.Kind -eq [TokenKind]::Variable -or $token.TokenFlags.hasFlag([TokenFlags]::Keyword)) { + $end = $token.Extent.EndOffset + $len = $end - $cursor + [Microsoft.PowerShell.PSConsoleReadLine]::Replace($cursor, $len, $quote + $line.SubString($cursor, $len) + $quote) + [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($end + 2) + return + } + } + + # We failed to be smart, so just insert a single quote + [Microsoft.PowerShell.PSConsoleReadLine]::Insert($quote) +} + +Set-PSReadLineKeyHandler -Key '(', '{', '[' ` + -BriefDescription InsertPairedBraces ` + -LongDescription "Insert matching braces" ` + -ScriptBlock { + param($key, $arg) + + $closeChar = switch ($key.KeyChar) { + <#case#> '(' { [char]')'; break } + <#case#> '{' { [char]'}'; break } + <#case#> '[' { [char]']'; break } + } + + $selectionStart = $null + $selectionLength = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength) + + $line = $null + $cursor = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) + + if ($selectionStart -ne -1) { + # Text is selected, wrap it in brackets + [Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, $key.KeyChar + $line.SubString($selectionStart, $selectionLength) + $closeChar) + [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2) + } + else { + # No text is selected, insert a pair + [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$($key.KeyChar)$closeChar") + [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1) + } +} + +Set-PSReadLineKeyHandler -Key ')', ']', '}' ` + -BriefDescription SmartCloseBraces ` + -LongDescription "Insert closing brace or skip" ` + -ScriptBlock { + param($key, $arg) + + $line = $null + $cursor = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) + + if ($line[$cursor] -eq $key.KeyChar) { + [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1) + } + else { + [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$($key.KeyChar)") + } +} + +Set-PSReadLineKeyHandler -Key Backspace ` + -BriefDescription SmartBackspace ` + -LongDescription "Delete previous character or matching quotes/parens/braces" ` + -ScriptBlock { + param($key, $arg) + + $line = $null + $cursor = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) + + if ($cursor -gt 0) { + $toMatch = $null + if ($cursor -lt $line.Length) { + switch ($line[$cursor]) { + <#case#> '"' { $toMatch = '"'; break } + <#case#> "'" { $toMatch = "'"; break } + <#case#> ')' { $toMatch = '('; break } + <#case#> ']' { $toMatch = '['; break } + <#case#> '}' { $toMatch = '{'; break } + } + } + + if ($toMatch -ne $null -and $line[$cursor - 1] -eq $toMatch) { + [Microsoft.PowerShell.PSConsoleReadLine]::Delete($cursor - 1, 2) + } + else { + [Microsoft.PowerShell.PSConsoleReadLine]::BackwardDeleteChar($key, $arg) + } + } +} + +#endregion Smart Insert/Delete + +# Sometimes you enter a command but realize you forgot to do something else first. +# This binding will let you save that command in the history so you can recall it, +# but it doesn't actually execute. It also clears the line with RevertLine so the +# undo stack is reset - though redo will still reconstruct the command line. +Set-PSReadLineKeyHandler -Key Alt+w ` + -BriefDescription SaveInHistory ` + -LongDescription "Save current line in history but do not execute" ` + -ScriptBlock { + param($key, $arg) + + $line = $null + $cursor = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) + [Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory($line) + [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() +} + +# Insert text from the clipboard as a here string +Set-PSReadLineKeyHandler -Key Ctrl+V ` + -BriefDescription PasteAsHereString ` + -LongDescription "Paste the clipboard text as a here string" ` + -ScriptBlock { + param($key, $arg) + + Add-Type -Assembly PresentationCore + if ([System.Windows.Clipboard]::ContainsText()) { + # Get clipboard text - remove trailing spaces, convert \r\n to \n, and remove the final \n. + $text = ([System.Windows.Clipboard]::GetText() -replace "\p{Zs}*`r?`n", "`n").TrimEnd() + [Microsoft.PowerShell.PSConsoleReadLine]::Insert("@'`n$text`n'@") + } + else { + [Microsoft.PowerShell.PSConsoleReadLine]::Ding() + } +} + +# Sometimes you want to get a property of invoke a member on what you've entered so far +# but you need parens to do that. This binding will help by putting parens around the current selection, +# or if nothing is selected, the whole line. +Set-PSReadLineKeyHandler -Key 'Alt+(' ` + -BriefDescription ParenthesizeSelection ` + -LongDescription "Put parenthesis around the selection or entire line and move the cursor to after the closing parenthesis" ` + -ScriptBlock { + param($key, $arg) + + $selectionStart = $null + $selectionLength = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength) + + $line = $null + $cursor = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) + if ($selectionStart -ne -1) { + [Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, '(' + $line.SubString($selectionStart, $selectionLength) + ')') + [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2) + } + else { + [Microsoft.PowerShell.PSConsoleReadLine]::Replace(0, $line.Length, '(' + $line + ')') + [Microsoft.PowerShell.PSConsoleReadLine]::EndOfLine() + } +} + +# Each time you press Alt+', this key handler will change the token +# under or before the cursor. It will cycle through single quotes, double quotes, or +# no quotes each time it is invoked. +Set-PSReadLineKeyHandler -Key "Alt+'" ` + -BriefDescription ToggleQuoteArgument ` + -LongDescription "Toggle quotes on the argument under the cursor" ` + -ScriptBlock { + param($key, $arg) + + $ast = $null + $tokens = $null + $errors = $null + $cursor = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$errors, [ref]$cursor) + + $tokenToChange = $null + foreach ($token in $tokens) { + $extent = $token.Extent + if ($extent.StartOffset -le $cursor -and $extent.EndOffset -ge $cursor) { + $tokenToChange = $token + + # If the cursor is at the end (it's really 1 past the end) of the previous token, + # we only want to change the previous token if there is no token under the cursor + if ($extent.EndOffset -eq $cursor -and $foreach.MoveNext()) { + $nextToken = $foreach.Current + if ($nextToken.Extent.StartOffset -eq $cursor) { + $tokenToChange = $nextToken + } + } + break + } + } + + if ($tokenToChange -ne $null) { + $extent = $tokenToChange.Extent + $tokenText = $extent.Text + if ($tokenText[0] -eq '"' -and $tokenText[-1] -eq '"') { + # Switch to no quotes + $replacement = $tokenText.Substring(1, $tokenText.Length - 2) + } + elseif ($tokenText[0] -eq "'" -and $tokenText[-1] -eq "'") { + # Switch to double quotes + $replacement = '"' + $tokenText.Substring(1, $tokenText.Length - 2) + '"' + } + else { + # Add single quotes + $replacement = "'" + $tokenText + "'" + } + + [Microsoft.PowerShell.PSConsoleReadLine]::Replace( + $extent.StartOffset, + $tokenText.Length, + $replacement) + } +} + +# This example will replace any aliases on the command line with the resolved commands. +Set-PSReadLineKeyHandler -Key "Alt+%" ` + -BriefDescription ExpandAliases ` + -LongDescription "Replace all aliases with the full command" ` + -ScriptBlock { + param($key, $arg) + + $ast = $null + $tokens = $null + $errors = $null + $cursor = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$errors, [ref]$cursor) + + $startAdjustment = 0 + foreach ($token in $tokens) { + if ($token.TokenFlags -band [TokenFlags]::CommandName) { + $alias = $ExecutionContext.InvokeCommand.GetCommand($token.Extent.Text, 'Alias') + if ($alias -ne $null) { + $resolvedCommand = $alias.ResolvedCommandName + if ($resolvedCommand -ne $null) { + $extent = $token.Extent + $length = $extent.EndOffset - $extent.StartOffset + [Microsoft.PowerShell.PSConsoleReadLine]::Replace( + $extent.StartOffset + $startAdjustment, + $length, + $resolvedCommand) + + # Our copy of the tokens won't have been updated, so we need to + # adjust by the difference in length + $startAdjustment += ($resolvedCommand.Length - $length) + } + } + } + } +} + +# F1 for help on the command line - naturally +Set-PSReadLineKeyHandler -Key F1 ` + -BriefDescription CommandHelp ` + -LongDescription "Open the help window for the current command" ` + -ScriptBlock { + param($key, $arg) + + $ast = $null + $tokens = $null + $errors = $null + $cursor = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$errors, [ref]$cursor) + + $commandAst = $ast.FindAll( { + $node = $args[0] + $node -is [CommandAst] -and + $node.Extent.StartOffset -le $cursor -and + $node.Extent.EndOffset -ge $cursor + }, $true) | Select-Object -Last 1 + + if ($commandAst -ne $null) { + $commandName = $commandAst.GetCommandName() + if ($commandName -ne $null) { + $command = $ExecutionContext.InvokeCommand.GetCommand($commandName, 'All') + if ($command -is [AliasInfo]) { + $commandName = $command.ResolvedCommandName + } + + if ($commandName -ne $null) { + Get-Help $commandName -ShowWindow + } + } + } +} + + +# +# Ctrl+Shift+j then type a key to mark the current directory. +# Ctrj+j then the same key will change back to that directory without +# needing to type cd and won't change the command line. + +# +$global:PSReadLineMarks = @{} + +Set-PSReadLineKeyHandler -Key Ctrl+J ` + -BriefDescription MarkDirectory ` + -LongDescription "Mark the current directory" ` + -ScriptBlock { + param($key, $arg) + + $key = [Console]::ReadKey($true) + $global:PSReadLineMarks[$key.KeyChar] = $pwd +} + +Set-PSReadLineKeyHandler -Key Ctrl+j ` + -BriefDescription JumpDirectory ` + -LongDescription "Goto the marked directory" ` + -ScriptBlock { + param($key, $arg) + + $key = [Console]::ReadKey() + $dir = $global:PSReadLineMarks[$key.KeyChar] + if ($dir) { + cd $dir + [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() + } +} + +Set-PSReadLineKeyHandler -Key Alt+j ` + -BriefDescription ShowDirectoryMarks ` + -LongDescription "Show the currently marked directories" ` + -ScriptBlock { + param($key, $arg) + + $global:PSReadLineMarks.GetEnumerator() | % { + [PSCustomObject]@{Key = $_.Key; Dir = $_.Value } } | + Format-Table -AutoSize | Out-Host + + [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() +} + +# Auto correct 'git cmt' to 'git commit' +Set-PSReadLineOption -CommandValidationHandler { + param([CommandAst]$CommandAst) + + switch ($CommandAst.GetCommandName()) { + 'git' { + $gitCmd = $CommandAst.CommandElements[1].Extent + switch ($gitCmd.Text) { + 'cmt' { + [Microsoft.PowerShell.PSConsoleReadLine]::Replace( + $gitCmd.StartOffset, $gitCmd.EndOffset - $gitCmd.StartOffset, 'commit') + } + } + } + } +} + +# `ForwardChar` accepts the entire suggestion text when the cursor is at the end of the line. +# This custom binding makes `RightArrow` behave similarly - accepting the next word instead of the entire suggestion text. +Set-PSReadLineKeyHandler -Key RightArrow ` + -BriefDescription ForwardCharAndAcceptNextSuggestionWord ` + -LongDescription "Move cursor one character to the right in the current editing line and accept the next word in suggestion when it's at the end of current editing line" ` + -ScriptBlock { + param($key, $arg) + + $line = $null + $cursor = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) + + if ($cursor -lt $line.Length) { + [Microsoft.PowerShell.PSConsoleReadLine]::ForwardChar($key, $arg) + } + else { + [Microsoft.PowerShell.PSConsoleReadLine]::AcceptNextSuggestionWord($key, $arg) + } +} + +# Cycle through arguments on current line and select the text. This makes it easier to quickly change the argument if re-running a previously run command from the history +# or if using a psreadline predictor. You can also use a digit argument to specify which argument you want to select, i.e. Alt+1, Alt+a selects the first argument +# on the command line. +Set-PSReadLineKeyHandler -Key Alt+a ` + -BriefDescription SelectCommandArguments ` + -LongDescription "Set current selection to next command argument in the command line. Use of digit argument selects argument by position" ` + -ScriptBlock { + param($key, $arg) + + $ast = $null + $cursor = $null + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$null, [ref]$null, [ref]$cursor) + + $asts = $ast.FindAll( { + $args[0] -is [System.Management.Automation.Language.ExpressionAst] -and + $args[0].Parent -is [System.Management.Automation.Language.CommandAst] -and + $args[0].Extent.StartOffset -ne $args[0].Parent.Extent.StartOffset + }, $true) + + if ($asts.Count -eq 0) { + [Microsoft.PowerShell.PSConsoleReadLine]::Ding() + return + } + + $nextAst = $null + + if ($null -ne $arg) { + $nextAst = $asts[$arg - 1] + } + else { + foreach ($ast in $asts) { + if ($ast.Extent.StartOffset -ge $cursor) { + $nextAst = $ast + break + } + } + + if ($null -eq $nextAst) { + $nextAst = $asts[0] + } + } + + $startOffsetAdjustment = 0 + $endOffsetAdjustment = 0 + + if ($nextAst -is [System.Management.Automation.Language.StringConstantExpressionAst] -and + $nextAst.StringConstantType -ne [System.Management.Automation.Language.StringConstantType]::BareWord) { + $startOffsetAdjustment = 1 + $endOffsetAdjustment = 2 + } + + [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($nextAst.Extent.StartOffset + $startOffsetAdjustment) + [Microsoft.PowerShell.PSConsoleReadLine]::SetMark($null, $null) + [Microsoft.PowerShell.PSConsoleReadLine]::SelectForwardChar($null, ($nextAst.Extent.EndOffset - $nextAst.Extent.StartOffset) - $endOffsetAdjustment) +} + + +Set-PSReadLineOption -PredictionSource History +Set-PSReadLineOption -PredictionViewStyle ListView +Set-PSReadLineOption -EditMode Windows + + +# This is an example of a macro that you might use to execute a command. +# This will add the command to history. +Set-PSReadLineKeyHandler -Key Ctrl+Shift+b ` + -BriefDescription BuildCurrentDirectory ` + -LongDescription "Build the current directory" ` + -ScriptBlock { + [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() + [Microsoft.PowerShell.PSConsoleReadLine]::Insert("dotnet build") + [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() +} + +Set-PSReadLineKeyHandler -Key Ctrl+Shift+t ` + -BriefDescription BuildCurrentDirectory ` + -LongDescription "Build the current directory" ` + -ScriptBlock { + [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() + [Microsoft.PowerShell.PSConsoleReadLine]::Insert("dotnet test") + [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() +} + +Set-PSReadLineOption -Colors @{ + "Parameter" = [ConsoleColor]::DarkBlue +} + +function printJSON { + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + $json + ) + $json | ConvertFrom-Json | ConvertTo-Json -Depth 100 +} + +$env:PYTHONIOENCODING="utf-8" +function fuck { + $history = (Get-History -Count 1).CommandLine; + if (-not [string]::IsNullOrWhiteSpace($history)) { + $fuck = $(thefuck $args $history); + if (-not [string]::IsNullOrWhiteSpace($fuck)) { + if ($fuck.StartsWith("echo")) { $fuck = $fuck.Substring(5); } + else { iex "$fuck"; } + } + } + [Console]::ResetColor() +} + +Import-Module posh-git + +$env:VIRTUAL_ENV_DISABLE_PROMPT=1 + +# Import the Chocolatey Profile that contains the necessary code to enable +# tab-completions to function for `choco`. +# Be aware that if you are missing these lines from your profile, tab completion +# for `choco` will not function. +# See https://ch0.co/tab-completion for details. +$ChocolateyProfile = "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" + +if (Test-Path($ChocolateyProfile)) { + Import-Module "$ChocolateyProfile" +} + +function Edit-InDirectory { + param( + [Parameter(Mandatory=$true)] + [string]$Directory, + + [Parameter(ValueFromRemainingArguments=$true)] + [string[]]$FilePath + ) + + $originalLocation = Get-Location + + try { + Set-Location $Directory + + if ($FilePath) { + nvim $FilePath + } else { + nvim + } + } + finally { + Set-Location $originalLocation + } +} + +$NVIM_CONFIG_PATH = "$env:LOCALAPPDATA\nvim" +$NVIM_CONFIG_DATA_PATH = "$env:LOCALAPPDATA\nvim-data" + +function GotoNvimConfig { + cd $NVIM_CONFIG_PATH +} + +function GotoNvimData { + cd $NVIM_CONFIG_DATA_PATH +} + +function EditNvimConfig { + Edit-InDirectory $NVIM_CONFIG_PATH +} + +function DisableReadline { + Set-PSReadLineOption -PredictionSource None +} + +function EnableReadline { + Set-PSReadLineOption -PredictionSource History +} + +function CopyPath { + param( + [Parameter(Mandatory=$true)] + [string]$File + ) + + $path = (Get-Item $File).FullName + Set-Clipboard $path +} + +Invoke-Expression (& { (zoxide init powershell | Out-String) }) + +Remove-Item alias:cd -Force +Set-Alias -Name cd -Value z + +Set-Alias -Name fromjson -Value ConvertFrom-Json -Description "Alias for ConvertFrom-Json" +Set-Alias -Name tojson -Value ConvertTo-Json -Description "Alias for ConvertTo-Json" + +function Generate-JwtSecret { + <# + .SYNOPSIS + Generates a secure, Base64-encoded random string suitable for a JWT secret (HS256). + + .DESCRIPTION + This function creates a cryptographically strong, random byte array of a specified length + (defaulting to 32 bytes/256 bits) and then encodes it to a Base64 string. + This is ideal for use as a symmetric key in JWTs (e.g., for HS256 algorithm). + + .PARAMETER Length + Specifies the length of the random byte array in bytes. + A length of 32 bytes (256 bits) is generally recommended for HS256. + Defaults to 32 if not specified. + + .EXAMPLE + Generate-JwtSecret + + This will generate a 32-byte (256-bit) Base64-encoded JWT secret. + + .EXAMPLE + Generate-JwtSecret -Length 64 + + This will generate a 64-byte (512-bit) Base64-encoded JWT secret. + #> + [CmdletBinding()] + param( + [Parameter(Position=0)] + [int]$Length = 32 + ) + + try { + $bytes = New-Object byte[] $Length + [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) + [Convert]::ToBase64String($bytes) + } + catch { + Write-Error "Failed to generate JWT secret: $($_.Exception.Message)" + return $null + } +} + +function Copy-FileContent { +<# +.SYNOPSIS + Copies the content of a specified file to the clipboard. + +.DESCRIPTION + The Copy-FileContent function reads the entire content of a specified text file + and places that content onto the system clipboard using Set-Clipboard. + This function can be invoked using its alias 'catc'. + + The function is designed to be silent in terms of explicit success messages. + If an error occurs (e.g., file not found, permission issues, clipboard error), + it will throw a terminating error. This allows the calling script or user + to handle the error using standard PowerShell try/catch blocks or by checking + the $? automatic variable. + +.PARAMETER Path + Specifies the path to the file whose content will be copied to the clipboard. + This parameter is mandatory. It accepts pipeline input. + +.EXAMPLE + PS C:\> Copy-FileContent -Path "C:\Users\Me\Documents\MyNotes.txt" + # Copies the content of MyNotes.txt to the clipboard. + # If successful, $? will be $true. If an error occurs, it will be $false + # and an error record will be generated. + +.EXAMPLE + PS C:\> catc "C:\Logs\important.log" + # Uses the alias 'catc' to copy the content of important.log to the clipboard. + +.EXAMPLE + PS C:\> try { + PS C:\> catc "C:\path\to\nonexistentfile.txt" + PS C:\> Write-Host "File content copied successfully." + PS C:\> } + PS C:\> catch { + PS C:\> Write-Error "Operation failed: $($_.Exception.Message)" + PS C:\> # A calling script could use 'exit 1' here if needed + PS C:\> } + # This example shows how to catch and handle errors from the function. + +.EXAMPLE + PS C:\> "C:\Temp\data.log" | catc + PS C:\> if ($?) { + PS C:\> # Optional: Perform action on success, though the function is silent. + PS C:\> } else { + PS C:\> Write-Warning "catc command failed for data.log." + PS C:\> } + # Checks the success status after execution. + +.OUTPUTS + None. This function does not output any objects to the pipeline. It interacts + with the clipboard. On error, it writes an error record to the error stream. +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] + [string]$Path + ) + + process { + try { + if (-not (Test-Path -Path $Path -PathType Leaf)) { + throw "File not found or not a file: '$Path'" + } + + Get-Content -Path $Path -Raw -ErrorAction Stop | Set-Clipboard -ErrorAction Stop + } + catch { + throw + } + } +} + +New-Alias -Name catc -Value Copy-FileContent -Description "Reads file content to clipboard (cat + copy)" -Force -Scope Global + +function Move-ZipsAndCleanup { + <# + .SYNOPSIS + Moves all .zip files from subdirectories to the root directory and removes empty subdirectories. + + .DESCRIPTION + This function recursively searches through all subdirectories of the specified path, + moves all .zip files to the root directory, and then removes the leftover subdirectories. + If naming conflicts occur, files are automatically renamed with a counter suffix. + + .PARAMETER Path + The root directory path to process. Defaults to current directory if not specified. + + .EXAMPLE + Move-ZipsAndCleanup -Path "C:\MyFolder" + + .EXAMPLE + Move-ZipsAndCleanup -Path "16298879069" + + .EXAMPLE + Move-ZipsAndCleanup -Path "." -WhatIf + #> + + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Position = 0)] + [string]$Path = "." + ) + + # Resolve the full path + $ResolvedPath = Resolve-Path $Path -ErrorAction SilentlyContinue + if (-not $ResolvedPath) { + Write-Error "Path '$Path' does not exist." + return + } + + $RootPath = $ResolvedPath.Path + Write-Host "Processing directory: $RootPath" -ForegroundColor Green + + # Step 1: Move all .zip files to root directory + Write-Host "Step 1: Moving .zip files to root directory..." -ForegroundColor Yellow + + $ZipFiles = Get-ChildItem -Path $RootPath -Recurse -Filter "*.zip" | Where-Object { $_.Directory.FullName -ne $RootPath } + + if ($ZipFiles.Count -eq 0) { + Write-Host "No .zip files found in subdirectories." -ForegroundColor Cyan + } else { + Write-Host "Found $($ZipFiles.Count) .zip file(s) to move." -ForegroundColor Cyan + + foreach ($ZipFile in $ZipFiles) { + $DestinationPath = Join-Path $RootPath $ZipFile.Name + + # Handle naming conflicts + if (Test-Path $DestinationPath) { + $Counter = 1 + do { + $NewName = $ZipFile.BaseName + "_$Counter" + $ZipFile.Extension + $DestinationPath = Join-Path $RootPath $NewName + $Counter++ + } while (Test-Path $DestinationPath) + + Write-Host " Renaming due to conflict: $($ZipFile.Name) -> $(Split-Path $DestinationPath -Leaf)" -ForegroundColor Magenta + } + + if ($PSCmdlet.ShouldProcess($ZipFile.FullName, "Move to $DestinationPath")) { + try { + Move-Item -Path $ZipFile.FullName -Destination $DestinationPath -Force + Write-Host " Moved: $(Split-Path $DestinationPath -Leaf)" -ForegroundColor Green + } catch { + Write-Error " Failed to move $($ZipFile.Name): $($_.Exception.Message)" + } + } + } + } + + # Step 2: Remove leftover subdirectories + Write-Host "Step 2: Removing leftover subdirectories..." -ForegroundColor Yellow + + $Subdirectories = Get-ChildItem -Path $RootPath -Directory + + if ($Subdirectories.Count -eq 0) { + Write-Host "No subdirectories found to remove." -ForegroundColor Cyan + } else { + Write-Host "Found $($Subdirectories.Count) subdirectorie(s) to remove." -ForegroundColor Cyan + + foreach ($Directory in $Subdirectories) { + if ($PSCmdlet.ShouldProcess($Directory.FullName, "Remove directory")) { + try { + Remove-Item -Path $Directory.FullName -Recurse -Force + Write-Host " Removed: $($Directory.Name)" -ForegroundColor Green + } catch { + Write-Error " Failed to remove $($Directory.Name): $($_.Exception.Message)" + } + } + } + } + + Write-Host "Operation completed!" -ForegroundColor Green +} + +New-Alias -Name grep -Value Select-String -Description "Alias for Select-String" -Force -Scope Global + +function BRB { + $steam1Lines = @' + ( ( + ) ) +'@ -split [System.Environment]::NewLine + + $steam2Lines = @' + ) ) + ( ( +'@ -split [System.Environment]::NewLine + + $steamFrames = @($steam1Lines, $steam2Lines) + $frameIndex = 0 + + $cupLines = @' + ........ + | |] + \ / + `----' +'@ -split [System.Environment]::NewLine + + $brbMessage = "BRB WENT TO GET COFFEE" + + try { + [System.Console]::CursorVisible = $false + + Clear-Host + + $topPosition = [System.Console]::CursorTop + + while (-not [Console]::KeyAvailable) { + [System.Console]::SetCursorPosition(0, $topPosition) + + $currentSteamLines = $steamFrames[$frameIndex] + foreach ($line in $currentSteamLines) { + Write-Host $line -ForegroundColor Gray + } + + Write-Host $cupLines[0] -ForegroundColor Red + + Write-Host -NoNewline $cupLines[1] -ForegroundColor Red + Write-Host " $brbMessage" # Default color + + Write-Host $cupLines[2] -ForegroundColor Red + + Write-Host $cupLines[3] -ForegroundColor Red + + $frameIndex = ($frameIndex + 1) % $steamFrames.Length + + Start-Sleep -Milliseconds 1000 + } + } + finally { + while ([Console]::KeyAvailable) { + [void][Console]::ReadKey($true) + } + + [System.Console]::CursorVisible = $true + + Clear-Host + } +} + +function Get-YouTubeVideo { + <# + .SYNOPSIS + Downloads the best quality video and audio from a YouTube URL and merges them. + + .DESCRIPTION + Requires yt-dlp and ffmpeg to be installed and available in your system PATH. + + .PARAMETER Url + The URL of the YouTube video. + + .PARAMETER OutputPath + Optional. The exact path and filename to save the video (e.g., "C:\Videos\MyVideo.mp4"). + If not specified, it saves to the current directory using the video's title. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true, Position=0)] + [string]$Url, + + [Parameter(Mandatory=$false, Position=1)] + [string]$OutputPath + ) + + # Verify dependencies are installed + if (-not (Get-Command "yt-dlp" -ErrorAction SilentlyContinue)) { + Write-Warning "yt-dlp is missing. Please install it (e.g., 'winget install yt-dlp') and restart your terminal." + return + } + + if (-not (Get-Command "ffmpeg" -ErrorAction SilentlyContinue)) { + Write-Warning "ffmpeg is missing. It is required to merge the audio and video. Please install it (e.g., 'winget install ffmpeg') and restart your terminal." + return + } + + # Set up arguments for best video (mp4) and best audio (m4a), merged into an mp4 + $Arguments = @( + "-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best", + "--merge-output-format", "mp4" + ) + + # Handle output naming + if ($OutputPath) { + $Arguments += "-o" + $Arguments += $OutputPath + } else { + $Arguments += "-o" + $Arguments += "%(title)s.%(ext)s" + } + + $Arguments += $Url + + Write-Host "Starting download and merge process..." -ForegroundColor Cyan + + # Execute yt-dlp with the arguments + & yt-dlp @Arguments +} + +function Add-UserPath { + param( + [Parameter(Mandatory=$true)] + [string]$Path + ) + + if (-not (Test-Path -Path $Path)) { + Write-Warning "The path '$Path' does not exist. Skipping." + return + } + + $currentPath = [Environment]::GetEnvironmentVariable("Path", "User") + + # Split the path into an array to check for exact matches + $pathArray = $currentPath -split ";" + + if ($pathArray -notcontains $Path) { + $newPath = "$currentPath;$Path".TrimStart(';') + [Environment]::SetEnvironmentVariable("Path", $newPath, "User") + Write-Host "Successfully added '$Path' to the User Path." -ForegroundColor Cyan + } else { + Write-Host "Path already exists in the User environment variable." -ForegroundColor Yellow + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2fbf06e --- /dev/null +++ b/README.md @@ -0,0 +1,267 @@ +# dotfiles + +> One-stop shop to set up and sync my Windows and Linux machines. + +Managed with **[chezmoi](https://www.chezmoi.io/)** — cross-platform, idempotent, with native [Bitwarden](https://bitwarden.com/) secret integration. + +--- + +## What's managed + +| Config | Windows path | Linux path | +|------------------------|----------------------------------------------------------------------|--------------------------------------------------------| +| Git config | `~/.gitconfig` | `~/.gitconfig` | +| SSH config | `~/.ssh/config` | `~/.ssh/config` | +| Neovim | `~/AppData/Local/nvim/` *(external)* | `~/.config/nvim/` *(external)* | +| VS Code | `~/AppData/Roaming/Code/User/` | `~/.config/Code/User/` | +| Windows Terminal | `~/AppData/Local/Packages/Microsoft.WindowsTerminal_.../LocalState/` | N/A | +| PowerShell profile | `~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1` | N/A | +| Zsh config (Oh My Zsh) | N/A | `~/.zshrc`, `~/.zshenv` | +| Oh My Zsh custom theme | N/A | `~/.oh-my-zsh/custom/themes/lavender-dimmed.zsh-theme` | +| Oh My Posh theme | `~/.config/oh-my-posh/theme.omp.json` | N/A (Windows only) | +| WSL config | `~/.wslconfig` | N/A | + +--- + +## Prerequisites + +Before running bootstrap, complete these **one-time manual steps**: + +1. **Create a Bitwarden account** at [bitwarden.com](https://bitwarden.com) +2. **Import SSH private keys** into Bitwarden as Secure Notes (see [Storing SSH keys in Bitwarden](#storing-ssh-keys-in-bitwarden)) + +Everything else — installing chezmoi, Bitwarden CLI, logging in, and applying the dotfiles — is handled by the bootstrap script. + +--- + +## Source of truth + +The primary repo lives on a self-hosted Gitea server and is mirrored to GitHub: + +| | URL | +|---------------------|------------------------------------------------| +| **Primary (Gitea)** | `https://gitea.freeborn.cloud/Stevan/dotfiles` | +| **Mirror (GitHub)** | `https://github.com/StevanFreeborn/dotfiles` | + +Push changes to Gitea — GitHub is updated automatically via push mirroring. + +--- + +## Fresh machine setup + +### Windows + +Open PowerShell (no admin required) and run: + +```powershell +iwr -useb https://raw.githubusercontent.com/StevanFreeborn/dotfiles/main/scripts/bootstrap.ps1 | iex +``` + +### Linux (Ubuntu/Debian) + +```bash +bash <(curl -fsLS https://raw.githubusercontent.com/StevanFreeborn/dotfiles/main/scripts/bootstrap.sh) +``` + +The bootstrap script will: + +1. Install missing prerequisites (`curl`/`git` on Linux, or check `winget` on Windows) +2. Install Bitwarden CLI if not present +3. Install chezmoi if not present +4. Prompt for Bitwarden login/unlock (interactive — master password required) +5. Run `chezmoi init --apply` to clone and apply the full dotfiles repo + +> **Primary vs. fallback repo:** The script tries the Gitea server first; if unreachable it falls back to the GitHub mirror automatically. + +### Manual bootstrap (advanced) + +
+Expand for step-by-step instructions + +#### Windows + +```powershell +# 1. Install chezmoi +winget install --id twpayne.chezmoi --silent --accept-package-agreements --accept-source-agreements + +# 2. Log into Bitwarden CLI and unlock +winget install --id Bitwarden.CLI --silent --accept-package-agreements --accept-source-agreements +bw login +$env:BW_SESSION = bw unlock --raw + +# 3. Bootstrap dotfiles +chezmoi init --apply https://gitea.freeborn.cloud/Stevan/dotfiles.git +``` + +#### Linux (Ubuntu/Debian) + +```bash +# 1. Install chezmoi +sh -c "$(curl -fsLS get.chezmoi.io)" + +# 2. Log into Bitwarden CLI and unlock +sudo snap install bw +bw login +export BW_SESSION=$(bw unlock --raw) + +# 3. Bootstrap dotfiles +chezmoi init --apply https://gitea.freeborn.cloud/Stevan/dotfiles.git +``` + +
+ +--- + +## Syncing an existing machine + +Pull the latest changes and apply them: + +```bash +chezmoi update +``` + +Preview what would change before applying: + +```bash +chezmoi update --dry-run +``` + +--- + +## Managing dotfiles + +### Add a new file to be managed + +```bash +chezmoi add ~/.some-new-config +``` + +### Edit a managed file + +```bash +# Opens the file in your editor within the chezmoi source dir +chezmoi edit ~/.gitconfig + +# Apply your edits +chezmoi apply +``` + +### View pending changes + +```bash +chezmoi diff +``` + +### Push changes to Gitea + +```bash +chezmoi cd +git add . +git commit -m "your message" +git push +``` + +### Neovim config (external repo) + +Neovim config is managed as a [chezmoi external](https://www.chezmoi.io/reference/special-files-and-directories/chezmoiexternal-toml/) +pointing at [github.com/StevanFreeborn/nvim-config](https://github.com/StevanFreeborn/nvim-config). + +- **To edit nvim config:** commit changes directly to the `nvim-config` repo +- **To pull latest nvim config:** `chezmoi update` (refreshes weekly automatically, or on every apply if within the refresh window) +- **External definition:** `.chezmoiexternal.toml.tmpl` + +--- + +## Package management + +### Windows + +Packages are defined in [`packages/windows.json`](packages/windows.json) and installed via winget. + +To add a new package: + +1. Find the package ID: `winget search ` +2. Add it to `packages/windows.json` +3. Run `chezmoi apply` — the install script re-runs because the file hash changed + +### Linux + +Apt packages are listed in [`packages/linux.txt`](packages/linux.txt). Additional tools (Go, Rust, NVM, Neovim, etc.) are installed via the [`run_onchange_linux_install-packages.sh.tmpl`](.chezmoiscripts/run_onchange_linux_install-packages.sh.tmpl) script. + +--- + +## Secret management + +Secrets are managed using [Bitwarden CLI](https://bitwarden.com/help/cli/) integrated with chezmoi. + +### Initial setup + +```bash +# Log in (first time) +bw login + +# Unlock your vault and export the session key +# Windows: +$env:BW_SESSION = bw unlock --raw + +# Linux/macOS: +export BW_SESSION=$(bw unlock --raw) +``` + +### Storing SSH keys in Bitwarden + +1. Open Bitwarden and create a **Secure Note** for each SSH private key +2. Name each note: `SSH Key - ` (e.g. `SSH Key - stevan@freeborn.cloud`) +3. Paste the **private key content** as the note body + +The setup script will read these notes and write the keys to `~/.ssh/` with correct permissions (`600`). + +### Using secrets in templates + +chezmoi templates can pull values from Bitwarden: + +```txt +{{ (bitwarden "My Secret Item").login.password }} +{{ (bitwardenFields "My Item").custom_field.value }} +``` + +--- + +## Platform notes + +### Windows + WSL + +The `.wslconfig` file controls WSL2 resource limits. Edit it via: + +```powershell +chezmoi edit ~/.wslconfig +chezmoi apply +``` + +After applying a `.wslconfig` change, restart WSL: + +```powershell +wsl --shutdown +``` + +### Oh My Posh theme + +The theme file lives at `~/.config/oh-my-posh/theme.omp.json` on Windows. The PowerShell profile references this path. + +To change the theme, edit `dot_config/oh-my-posh/theme.omp.json` in the chezmoi source and apply. + +--- + +## Troubleshooting + +**`chezmoi apply` asks for Bitwarden session on every run** +→ Set `BW_SESSION` before running chezmoi. Add it to your shell session: `export BW_SESSION=$(bw unlock --raw)` + +**Package install script doesn't re-run after adding a package** +→ The script's hash is based on the packages file content. Ensure you saved `packages/windows.json` or `packages/linux.txt` and then run `chezmoi apply`. + +**Neovim complains about missing plugins on a fresh machine** +→ Open nvim and run `:Lazy sync` to install all plugins via lazy.nvim. + +**`dot_gitconfig.tmpl` prompts for name/email** +→ These are set once and stored in `~/.config/chezmoi/chezmoi.toml`. Delete that file to re-enter them. diff --git a/dot_config/oh-my-posh/theme.omp.json b/dot_config/oh-my-posh/theme.omp.json new file mode 100644 index 0000000..5ecef2a --- /dev/null +++ b/dot_config/oh-my-posh/theme.omp.json @@ -0,0 +1,118 @@ +{ + "$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json", + "blocks": [ + { + "type": "prompt", + "alignment": "left", + "segments": [ + { + "properties": { + "cache_duration": "none" + }, + "leading_diamond": "\u256d\u2500\ue0b6", + "template": " {{ .UserName }}@{{ .HostName }} ", + "foreground": "#1e222a", + "background": "#b39cd0", + "type": "session", + "style": "diamond" + }, + { + "properties": { + "cache_duration": "none" + }, + "template": " \uf0e7 ", + "foreground": "#1e222a", + "powerline_symbol": "\ue0b0", + "background": "#d4c87a", + "type": "root", + "style": "powerline" + }, + { + "type": "command", + "style": "powerline", + "powerline_symbol": "\ue0b0", + "foreground": "#e0e4e8", + "background_templates": [ + "{{ if ne .Output \"-1\" }}#a088c0{{ else }}#3a3e46{{ end }}" + ], + "properties": { + "shell": "pwsh", + "command": "dotnet C:/Users/sfree/Repositories/viewer-count-fetcher/index.cs" + }, + "cache": { + "duration": "30s", + "strategy": "session" + }, + "template": " \udb81\uddc3{{ if ne .Output \"-1\" }} {{ .Output }} {{ else }} {{ end }}" + }, + { + "properties": { + "cache_duration": "none", + "folder_icon": "\uf115", + "folder_separator_icon": " \ue0b1 ", + "home_icon": "\uf46d", + "max_depth": 0, + "style": "agnoster" + }, + "template": " {{ .Path }} ", + "foreground": "#1e222a", + "powerline_symbol": "\ue0b0", + "background": "#b39cd0", + "type": "path", + "style": "powerline" + }, + { + "properties": { + "cache_duration": "none" + }, + "template": " {{ (abbrev 30 .HEAD) }} ", + "foreground": "#1e222a", + "powerline_symbol": "\ue0b0", + "background": "#7ec8c0", + "type": "git", + "style": "powerline" + }, + { + "properties": { + "cache_duration": "none" + }, + "template": " \ue235 {{ if .Error }}{{ .Error }}{{ else }}{{ if .Venv }}{{ .Venv }} {{ end }}{{ .Full }}{{ end }} ", + "foreground": "#1e222a", + "powerline_symbol": "\ue0b0", + "background": "#c8b8e0", + "type": "python", + "style": "powerline" + } + ] + }, + { + "type": "prompt", + "alignment": "left", + "segments": [ + { + "properties": { + "cache_duration": "none" + }, + "template": "\u2570\u2500", + "foreground": "#b39cd0", + "type": "text", + "style": "plain" + }, + { + "properties": { + "always_enabled": true, + "cache_duration": "none" + }, + "template": "\ue285", + "foreground": "#7ec8c0", + "type": "exit", + "style": "plain", + "foreground_templates": ["{{ if gt .Code 0 }}#d47373{{ end }}"] + } + ], + "newline": true + } + ], + "version": 3, + "final_space": true +} diff --git a/dot_gitconfig.tmpl b/dot_gitconfig.tmpl new file mode 100644 index 0000000..119c301 --- /dev/null +++ b/dot_gitconfig.tmpl @@ -0,0 +1,14 @@ +[init] + defaultBranch = main +[user] + name = {{ .name }} + email = {{ .email }} +[push] + autoSetupRemote = true +[core] + editor = nvim -f +[alias] + acp = "! acp() { git add . && git commit -am \"$1\" && git push ; } ; acp" + acnvp = "! acnvp() { git add . && git commit --no-verify -am \"$1\" && git push ; } ; acnvp" +[gitbutler] + aiModelProvider = openai diff --git a/dot_oh-my-zsh/custom/themes/lavender-dimmed.zsh-theme b/dot_oh-my-zsh/custom/themes/lavender-dimmed.zsh-theme new file mode 100644 index 0000000..982df67 --- /dev/null +++ b/dot_oh-my-zsh/custom/themes/lavender-dimmed.zsh-theme @@ -0,0 +1,367 @@ +# vim:ft=zsh ts=2 sw=2 sts=2 +# +# lavender-dimmed - A lavender-accented dark theme for ZSH +# +# Based on agnoster's Theme - https://gist.github.com/3712874 +# A Powerline-inspired theme for ZSH +# +# # README +# +# In order for this theme to render correctly, you will need a +# [Powerline-patched font](https://github.com/Lokaltog/powerline-fonts). +# Make sure you have a recent version: the code points that Powerline +# uses changed in 2012, and older versions will display incorrectly, +# in confusing ways. +# +# # Goals +# +# The aim of this theme is to only show you *relevant* information. Like most +# prompts, it will only show git information when in a git working directory. +# However, it goes a step further: everything from the current user and +# hostname to whether the last call exited with an error to whether background +# jobs are running in this shell will all be displayed automatically when +# appropriate. + +### Segment drawing +# A few utility functions to make it easy and re-usable to draw segmented prompts + +CURRENT_BG='NONE' +CURRENT_FG='#cdd9e5' +CURRENT_DEFAULT_FG='#adbac7' + +zmodload zsh/nearcolor 2>/dev/null + +### Theme Configuration Initialization +# +# Override these settings in your ~/.zshrc + +# Current working directory +: ${AGNOSTER_DIR_FG:=#1e222a} +: ${AGNOSTER_DIR_BG:=#b39cd0} + +# user@host +: ${AGNOSTER_CONTEXT_FG:=#1e222a} +: ${AGNOSTER_CONTEXT_BG:=#b39cd0} + +# Git related +: ${AGNOSTER_GIT_CLEAN_FG:=#1e222a} +: ${AGNOSTER_GIT_CLEAN_BG:=#7ec8c0} +: ${AGNOSTER_GIT_DIRTY_FG:=#1e222a} +: ${AGNOSTER_GIT_DIRTY_BG:=#d4c87a} + +# Bazaar related +: ${AGNOSTER_BZR_CLEAN_FG:=#1e222a} +: ${AGNOSTER_BZR_CLEAN_BG:=#7ec8c0} +: ${AGNOSTER_BZR_DIRTY_FG:=#1e222a} +: ${AGNOSTER_BZR_DIRTY_BG:=#d4c87a} + +# Mercurial related +: ${AGNOSTER_HG_NEWFILE_FG:=#1e222a} +: ${AGNOSTER_HG_NEWFILE_BG:=#d47373} +: ${AGNOSTER_HG_CHANGED_FG:=#1e222a} +: ${AGNOSTER_HG_CHANGED_BG:=#d4c87a} +: ${AGNOSTER_HG_CLEAN_FG:=#1e222a} +: ${AGNOSTER_HG_CLEAN_BG:=#7ec8c0} + +# VirtualEnv colors +: ${AGNOSTER_VENV_FG:=#1e222a} +: ${AGNOSTER_VENV_BG:=#c8b8e0} + +# AWS Profile colors +: ${AGNOSTER_AWS_PROD_FG:=#d47373} +: ${AGNOSTER_AWS_PROD_BG:=#6d484d} +: ${AGNOSTER_AWS_FG:=#1e222a} +: ${AGNOSTER_AWS_BG:=#7ec8c0} + +# Status symbols +: ${AGNOSTER_STATUS_RETVAL_FG:=#d47373} +: ${AGNOSTER_STATUS_ROOT_FG:=#d4c87a} +: ${AGNOSTER_STATUS_JOB_FG:=#7ec8c0} +: ${AGNOSTER_STATUS_FG:=$CURRENT_DEFAULT_FG} +: ${AGNOSTER_STATUS_BG:=#22272e} + +# Terraform colors +: ${AGNOSTER_TERRAFORM_FG:=#1e222a} +: ${AGNOSTER_TERRAFORM_BG:=#a088c0} + +## Non-Color settings - set to 'true' to enable +# Show the actual numeric return value rather than a cross symbol. +: ${AGNOSTER_STATUS_RETVAL_NUMERIC:=false} +# Show git working dir in the style "/git/root   master  relative/dir" instead of "/git/root/relative/dir   master" +: ${AGNOSTER_GIT_INLINE:=false} +# Show the git branch status in the prompt rather than the generic branch symbol +: ${AGNOSTER_GIT_BRANCH_STATUS:=true} + + +# Special Powerline characters + +() { + local LC_ALL="" LC_CTYPE="en_US.UTF-8" + # NOTE: This segment separator character is correct. In 2012, Powerline changed + # the code points they use for their special characters. This is the new code point. + # If this is not working for you, you probably have an old version of the + # Powerline-patched fonts installed. Download and install the new version. + # Do not submit PRs to change this unless you have reviewed the Powerline code point + # history and have new information. + # This is defined using a Unicode escape sequence so it is unambiguously readable, regardless of + # what font the user is viewing this source code in. Do not replace the + # escape sequence with a single literal character. + # Do not change this! Do not make it '\u2b80'; that is the old, wrong code point. + SEGMENT_SEPARATOR=$'\ue0b0' +} + +# Begin a segment +# Takes two arguments, background and foreground. Both can be omitted, +# rendering default background/foreground. +prompt_segment() { + local bg fg + [[ -n $1 ]] && bg="%K{$1}" || bg="%k" + [[ -n $2 ]] && fg="%F{$2}" || fg="%f" + if [[ $CURRENT_BG != 'NONE' && $1 != $CURRENT_BG ]]; then + echo -n " %{$bg%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR%{$fg%} " + else + echo -n "%{$bg%}%{$fg%} " + fi + CURRENT_BG=$1 + [[ -n $3 ]] && echo -n $3 +} + +# End the prompt, closing any open segments +prompt_end() { + if [[ -n $CURRENT_BG ]]; then + echo -n " %{%k%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR" + else + echo -n "%{%k%}" + fi + echo -n "%{%f%}" + CURRENT_BG='' +} + +git_toplevel() { + local repo_root=$(git rev-parse --show-toplevel) + if [[ $repo_root = '' ]]; then + # We are in a bare repo. Use git dir as root + repo_root=$(git rev-parse --git-dir) + if [[ $repo_root = '.' ]]; then + repo_root=$PWD + fi + fi + echo -n $repo_root +} + +### Prompt components +# Each component will draw itself, and hide itself if no information needs to be shown + +# Context: user@hostname (who am I and where am I) +prompt_context() { + if [[ "$USERNAME" != "$DEFAULT_USER" || -n "$SSH_CLIENT" ]]; then + prompt_segment "$AGNOSTER_CONTEXT_BG" "$AGNOSTER_CONTEXT_FG" "%(!.%{%F{$AGNOSTER_STATUS_ROOT_FG}%}.)%n@%m" + fi +} + +prompt_git_relative() { + local repo_root=$(git_toplevel) + local path_in_repo=$(pwd | sed "s/^$(echo "$repo_root" | sed 's:/:\\/:g;s/\$/\\$/g')//;s:^/::;s:/$::;") + if [[ $path_in_repo != '' ]]; then + prompt_segment "$AGNOSTER_DIR_BG" "$AGNOSTER_DIR_FG" "$path_in_repo" + fi; +} + +# Git: branch/detached head, dirty status +prompt_git() { + (( $+commands[git] )) || return + if [[ "$(command git config --get oh-my-zsh.hide-status 2>/dev/null)" = 1 ]]; then + return + fi + local PL_BRANCH_CHAR + () { + local LC_ALL="" LC_CTYPE="en_US.UTF-8" + PL_BRANCH_CHAR=$'\ue0a0' #  + } + local ref dirty mode repo_path + + if [[ "$(command git rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]]; then + repo_path=$(command git rev-parse --git-dir 2>/dev/null) + dirty=$(parse_git_dirty) + ref=$(command git symbolic-ref HEAD 2> /dev/null) || \ + ref="◈ $(command git describe --exact-match --tags HEAD 2> /dev/null)" || \ + ref="➦ $(command git rev-parse --short HEAD 2> /dev/null)" + if [[ -n $dirty ]]; then + prompt_segment "$AGNOSTER_GIT_DIRTY_BG" "$AGNOSTER_GIT_DIRTY_FG" + else + prompt_segment "$AGNOSTER_GIT_CLEAN_BG" "$AGNOSTER_GIT_CLEAN_FG" + fi + + if [[ $AGNOSTER_GIT_BRANCH_STATUS == 'true' ]]; then + local ahead behind + ahead=$(command git log --oneline @{upstream}.. 2>/dev/null) + behind=$(command git log --oneline ..@{upstream} 2>/dev/null) + if [[ -n "$ahead" ]] && [[ -n "$behind" ]]; then + PL_BRANCH_CHAR=$'\u21c5' + elif [[ -n "$ahead" ]]; then + PL_BRANCH_CHAR=$'\u21b1' + elif [[ -n "$behind" ]]; then + PL_BRANCH_CHAR=$'\u21b0' + fi + fi + + if [[ -e "${repo_path}/BISECT_LOG" ]]; then + mode=" " + elif [[ -e "${repo_path}/MERGE_HEAD" ]]; then + mode=" >M<" + elif [[ -e "${repo_path}/rebase" || -e "${repo_path}/rebase-apply" || -e "${repo_path}/rebase-merge" || -e "${repo_path}/../.dotest" ]]; then + mode=" >R>" + fi + + setopt promptsubst + autoload -Uz vcs_info + + zstyle ':vcs_info:*' enable git + zstyle ':vcs_info:*' get-revision true + zstyle ':vcs_info:*' check-for-changes true + zstyle ':vcs_info:*' stagedstr '✚' + zstyle ':vcs_info:*' unstagedstr '±' + zstyle ':vcs_info:*' formats ' %u%c' + zstyle ':vcs_info:*' actionformats ' %u%c' + vcs_info + echo -n "${${ref:gs/%/%%}/refs\/heads\//$PL_BRANCH_CHAR }${vcs_info_msg_0_%% }${mode}" + [[ $AGNOSTER_GIT_INLINE == 'true' ]] && prompt_git_relative + fi +} + +prompt_bzr() { + (( $+commands[bzr] )) || return + + # Test if bzr repository in directory hierarchy + local dir="$PWD" + while [[ ! -d "$dir/.bzr" ]]; do + [[ "$dir" = "/" ]] && return + dir="${dir:h}" + done + + local bzr_status status_mod status_all revision + if bzr_status=$(command bzr status 2>&1); then + status_mod=$(echo -n "$bzr_status" | head -n1 | grep "modified" | wc -m) + status_all=$(echo -n "$bzr_status" | head -n1 | wc -m) + revision=${$(command bzr log -r-1 --log-format line | cut -d: -f1):gs/%/%%} + if [[ $status_mod -gt 0 ]] ; then + prompt_segment "$AGNOSTER_BZR_DIRTY_BG" "$AGNOSTER_BZR_DIRTY_FG" "bzr@$revision ✚" + else + if [[ $status_all -gt 0 ]] ; then + prompt_segment "$AGNOSTER_BZR_DIRTY_BG" "$AGNOSTER_BZR_DIRTY_FG" "bzr@$revision" + else + prompt_segment "$AGNOSTER_BZR_CLEAN_BG" "$AGNOSTER_BZR_CLEAN_FG" "bzr@$revision" + fi + fi + fi +} + +prompt_hg() { + (( $+commands[hg] )) || return + local rev st branch + if $(command hg id >/dev/null 2>&1); then + if $(command hg prompt >/dev/null 2>&1); then + if [[ $(command hg prompt "{status|unknown}") = "?" ]]; then + # if files are not added + prompt_segment "$AGNOSTER_HG_NEWFILE_BG" "$AGNOSTER_HG_NEWFILE_FG" + st='±' + elif [[ -n $(command hg prompt "{status|modified}") ]]; then + # if any modification + prompt_segment "$AGNOSTER_HG_CHANGED_BG" "$AGNOSTER_HG_CHANGED_FG" + st='±' + else + # if working copy is clean + prompt_segment "$AGNOSTER_HG_CLEAN_BG" "$AGNOSTER_HG_CLEAN_FG" + fi + echo -n ${$(command hg prompt "☿ {rev}@{branch}"):gs/%/%%} $st + else + st="" + rev=$(command hg id -n 2>/dev/null | sed 's/[^-0-9]//g') + branch=$(command hg id -b 2>/dev/null) + if command hg st | command grep -q "^\?"; then + prompt_segment "$AGNOSTER_HG_NEWFILE_BG" "$AGNOSTER_HG_NEWFILE_FG" + st='±' + elif command hg st | command grep -q "^[MA]"; then + prompt_segment "$AGNOSTER_HG_CHANGED_BG" "$AGNOSTER_HG_CHANGED_FG" + st='±' + else + prompt_segment "$AGNOSTER_HG_CLEAN_BG" "$AGNOSTER_HG_CLEAN_FG" + fi + echo -n "☿ ${rev:gs/%/%%}@${branch:gs/%/%%}" $st + fi + fi +} + +# Dir: current working directory +prompt_dir() { + if [[ $AGNOSTER_GIT_INLINE == 'true' ]] && $(git rev-parse --is-inside-work-tree >/dev/null 2>&1); then + # Git repo and inline path enabled, hence only show the git root + prompt_segment "$AGNOSTER_DIR_BG" "$AGNOSTER_DIR_FG" "$(git_toplevel | sed "s:^$HOME:~:")" + else + prompt_segment "$AGNOSTER_DIR_BG" "$AGNOSTER_DIR_FG" '%~' + fi +} + +# Virtualenv: current working virtualenv +prompt_virtualenv() { + if [ -n "$CONDA_DEFAULT_ENV" ]; then + prompt_segment "$AGNOSTER_VENV_BG" "$CURRENT_FG" "🐍 $CONDA_DEFAULT_ENV" + fi + if [[ -n "$VIRTUAL_ENV" && -n "$VIRTUAL_ENV_DISABLE_PROMPT" ]]; then + prompt_segment "$AGNOSTER_VENV_BG" "$AGNOSTER_VENV_FG" "(${VIRTUAL_ENV:t:gs/%/%%})" + fi +} + +# Status: +# - was there an error +# - am I root +# - are there background jobs? +prompt_status() { + local -a symbols + + if [[ $AGNOSTER_STATUS_RETVAL_NUMERIC == 'true' ]]; then + [[ $RETVAL -ne 0 ]] && symbols+="%{%F{$AGNOSTER_STATUS_RETVAL_FG}%}$RETVAL" + else + [[ $RETVAL -ne 0 ]] && symbols+="%{%F{$AGNOSTER_STATUS_RETVAL_FG}%}✘" + fi + [[ $UID -eq 0 ]] && symbols+="%{%F{$AGNOSTER_STATUS_ROOT_FG}%}⚡" + [[ $(jobs -l | wc -l) -gt 0 ]] && symbols+="%{%F{$AGNOSTER_STATUS_JOB_FG}%}⚙" + + [[ -n "$symbols" ]] && prompt_segment "$AGNOSTER_STATUS_BG" "$AGNOSTER_STATUS_FG" "$symbols" +} + +#AWS Profile: +# - display current AWS_PROFILE name +# - displays yellow on red if profile name contains 'production' or +# ends in '-prod' +# - displays black on green otherwise +prompt_aws() { + [[ -z "$AWS_PROFILE" || "$SHOW_AWS_PROMPT" = false ]] && return + case "$AWS_PROFILE" in + *-prod|*production*) prompt_segment "$AGNOSTER_AWS_PROD_BG" "$AGNOSTER_AWS_PROD_FG" "AWS: ${AWS_PROFILE:gs/%/%%}" ;; + *) prompt_segment "$AGNOSTER_AWS_BG" "$AGNOSTER_AWS_FG" "AWS: ${AWS_PROFILE:gs/%/%%}" ;; + esac +} + +prompt_terraform() { + local terraform_info=$(tf_prompt_info) + [[ -z "$terraform_info" ]] && return + prompt_segment "$AGNOSTER_TERRAFORM_BG" "$AGNOSTER_TERRAFORM_FG" "TF: $terraform_info" +} + +## Main prompt +build_prompt() { + RETVAL=$? + prompt_status + prompt_virtualenv + prompt_aws + prompt_terraform + prompt_context + prompt_dir + prompt_git + prompt_bzr + prompt_hg + prompt_end +} + +PROMPT='%{%f%b%k%}$(build_prompt) ' \ No newline at end of file diff --git a/dot_ssh/blog.stevanfreeborn.com_github_actions.pub b/dot_ssh/blog.stevanfreeborn.com_github_actions.pub new file mode 100644 index 0000000..346aab6 --- /dev/null +++ b/dot_ssh/blog.stevanfreeborn.com_github_actions.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDEKuw3rZhDPMlgkpLymHk0jm4VolIEV7scAMvBFYgQ1VfUKw3RsydCHpUphQWqsOMTxQB69dNrlErA+e7YCsMLyPWV6I2hr4SyLdCpqvZVQBiMuAyLhxMfahhTdXv5CVwj0iYeHtde4NTMuYmRCkJCzxnCbk57P07kkElExDZNIfHjfeCoZEYCJj8cl//zShalCsoAgRQNwiPzZJoWHL1qkJ326rkbHHEPUHe2+UGraFfVR3IhD671gEUoV46aMxePgSUkyuZY/uImobhlzkYxG+721Decx2XwFoW3AUvHeQWyG9WdKs1cmM296bq6A5OKWMiUB6/xpuSqtMs0JtzmJAAzpNC1LWCbKiKMsKU4OVlMfOW+K9KWDCRHs1wtFAHpWt5hAA3G09Cp8MhehjvH2Dbyv4jtdxP9WHHI+Q8J3MCPjw7PZS/fbLQz69FwD/gUsuFcmnN3T1jXu/kknAZekk6lE2kCpXNl0eop2LVCcgpoOOsZfOdfKm5iEUeMUeM= sfree@zenbook diff --git a/dot_ssh/commands_github_actions.pub b/dot_ssh/commands_github_actions.pub new file mode 100644 index 0000000..f6e01c2 --- /dev/null +++ b/dot_ssh/commands_github_actions.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDQK4yE5QzJuZIjykXZy9dusg+enKt5XxQGiRGzgzVw sfree@zenbook diff --git a/dot_ssh/config b/dot_ssh/config new file mode 100644 index 0000000..9beb593 --- /dev/null +++ b/dot_ssh/config @@ -0,0 +1,36 @@ +Host bit_bastion + HostName ssh.steva.nz + User stevan + IdentityFile ~/.ssh/stevan@freeborn.cloud + +Host ftp.stevanfreeborn.com + HostName ftp.stevanfreeborn.com + User u74298126 + IdentityFile ~/.ssh/ftp_stevanfreeborn_com + +Host tangled.org + Hostname tangled.org + User git + IdentityFile ~/.ssh/tangled + AddressFamily inet + +Host macbookair + HostName ssh.macbookair.freeborn.cloud + User stevan + IdentityFile ~/.ssh/macbookair + +Host macbookpro + HostName ssh.macbookpro.freeborn.cloud + User stevan + IdentityFile ~/.ssh/macbookpro + +Host tinker + HostName ssh.freeborn.cloud + User stevan + IdentityFile ~/.ssh/zenbook_tinker + +Host gitea.freeborn.cloud + HostName ssh.freeborn.cloud + User git + Port 2222 + IdentityFile ~/.ssh/gitea.freeborn.cloud \ No newline at end of file diff --git a/dot_ssh/ftp_stevanfreeborn_com.pub b/dot_ssh/ftp_stevanfreeborn_com.pub new file mode 100644 index 0000000..24a9b08 --- /dev/null +++ b/dot_ssh/ftp_stevanfreeborn_com.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCehUFa1MnGWZJ4vgEnF+KYm19Yu91s3pP6V977rWyuZgl0CbQlnGBQOctGf/jFWTIjAxsT5BpyFwcN7uGQIT76HCw5T8/Y9Kl+e7uXSLVTiHCIWyn+JD/ofUY3z0sTdZWKgJtdIeC3Zuh3aRwAupqTKunT+sX5w0t2jxC0wsOgxz5cBZ1toe9IUc9vUcU+A3C8TNaKFZ8obbRQFll0fgfEDjjC42kZJvCXxXKaTr9wT9+xeaha4FR3vFmrL/Kk6FckX6U4r8PJ0TJo/QpTCJRAqjgUxCRLBU2Z1GcJIzkI0cIRGC3tX8NmmMLTwsA3HX8UJNNDjC01AXHZmnFOTzzvW0gkkjMOaAgPzxNuXXrUVwdfb/gkBCgWBTb9cpqU6ZyRd7N2LfNtdi1+FgBAzXqJ0NR02dxg7gOEcvzJ3S9YgvNPO4uOnnOSO3zArZ/IO48WULstBg2p8Xn/yFK+9b7iVfe0rU019UYbFUew9Teh+HgserZBKjr3xCl5gpXVhQU= sfree@zenbook diff --git a/dot_ssh/gitea.freeborn.cloud.pub b/dot_ssh/gitea.freeborn.cloud.pub new file mode 100644 index 0000000..b4ec066 --- /dev/null +++ b/dot_ssh/gitea.freeborn.cloud.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIpFfb6vpsQqFj1veGt9Ba+xal3kxOJLK6T8bTpxuScJ zenbook diff --git a/dot_ssh/id_ed25519.pub b/dot_ssh/id_ed25519.pub new file mode 100644 index 0000000..917dffa --- /dev/null +++ b/dot_ssh/id_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKHAhEDeA8KFO99TPWwItsatAon/wyigMomD/B/cm/5F diff --git a/dot_ssh/macbookair.pub b/dot_ssh/macbookair.pub new file mode 100644 index 0000000..f80b84e --- /dev/null +++ b/dot_ssh/macbookair.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBdLKCSaXgVG5MBoCv/t0x0HAreKHqvKZR+X+5mwLREl sfree@zenbook diff --git a/dot_ssh/macbookpro.pub b/dot_ssh/macbookpro.pub new file mode 100644 index 0000000..f006d06 --- /dev/null +++ b/dot_ssh/macbookpro.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/iwe93wHsN1EkLNoW+YLuoW5m/lgx3L/PQ8mE9MD01 sfree@zenbook diff --git a/dot_ssh/onspring_qa_playwright_reports_render.pub b/dot_ssh/onspring_qa_playwright_reports_render.pub new file mode 100644 index 0000000..6ebd997 --- /dev/null +++ b/dot_ssh/onspring_qa_playwright_reports_render.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGU7aDmqaRNn3YqsT3CbWtThBhQYiH+nk1hS9AGKCMFEspQbbnZkP06+s2IrQjOI1wspoHXDRJ4TEOr+RrV3/2i19pGKdJj/j+URpL7hAtLojvXa0zHyZWTN6VJdFe2pl8HuCHlv1DaqpuW32zcXZ55Pcn9igzK08wDROYNv6j2pUNo2DDo0D9SNMRQm5Vnk1Wb+xD+f8xu8kvpZzzkT01s4Wdl+1ozvD+AwPAmc/WQm5QS90GntxKEfIh/Pn+K+D+3+vOUDHdbJXDTw3C6jkp2ztwR20OK5Y1N9+lGUMtGUYZiPJ2/qrwiRV+FGfYeJjKThs2DMOeguIFyno9FON4aMcu8MeuJHm3ubHtHk/Zqx9dL2hz4mYOa6hrFZLoIr5TDJ7+2pFfsTi4BaCUpUiC/zrl2c2feban0p2x5RaSR3g+5rAOTcLWxYXmkRdyY9/4qA+UHOO0UfHJ3uCMvrxsHa49mSed/PWu3Pdo64yJ9/tQCcykNZrsyPQ8i11G2Yk= sfree@zenbook diff --git a/dot_ssh/onx_graph_github_actions.pub b/dot_ssh/onx_graph_github_actions.pub new file mode 100644 index 0000000..ed86020 --- /dev/null +++ b/dot_ssh/onx_graph_github_actions.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDceJHNEFsgSTw+uaoM/mK6+k/D8nen9rBLO0D8z8r0EttZLuvu39j0YNAKUrSY+Frd09QbzRt0k5t6YSgzRwcMVPMhBsyS+Z3FsdunthxMV++L9SgbfQDkSaZU7Zl1F3+Dh/z63KtHhi3yYOPUHmlgXTeXYW3PorKK3QnEQi9Szzc93GIP/A6Z3Ob7J7HzUTVLF6KQpLEhstGPxYfaQwGy/bIdYHC3gDji3s5inawMs1pRuIC3OjDPtqR0pi0Tgy40ArorXgbpCO+J9VHW9Gb3Ccy4K6rsQYdlLE/brug43WnxBYw67F3M4CPiNWLOIxgQYuG3p7OjwZWAs9RdokoQ6g4gObh8n9F4h8AcG6uk/TZcN5hmrJFMO9QqUb7E0e47t4NGhXd/lF5lX12pQV7JthZNYDkcBYvuEqO3cwzS8kNqe18GhUhbvGh+YMj55Z2RAkkciV89ol4hZEbq+3pgZ0Ihafo2FnRWPhl1hUAibcXKc263uQdMJtVsmRaWwV8= sfree@zenbook diff --git a/dot_ssh/restapiplayground.stevanfreeborn.com_github_actions.pub b/dot_ssh/restapiplayground.stevanfreeborn.com_github_actions.pub new file mode 100644 index 0000000..bdd8558 --- /dev/null +++ b/dot_ssh/restapiplayground.stevanfreeborn.com_github_actions.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCw+fT6kt+toR8A4/HaWlVt7atIZRZ/LDGBflmc0kdlHhtyPamQVJvPO7OsCHd6ugjOU/6yASSF9Sv8ekPyapNJt9dnY4WBzZqQUSuJinM89Mn3RS23ABEL8yL3bbWpIPKT1x2OOF+nRSy7TuYnhu3ivgi+ZAqUTnAuDjy5/kpjttRRZ+ls2oFAdpQF2rZ2W+o2pBBKR5/YM3mQQcGQtw9pm3ddyQWJKC1AhGZeSZodhxpRoe++3fRVvLJ/P2yc7PmF0h/mb4o5RWH72jmFiSsEYA3j0uKC7XH9Dyc+XBmuQJJ4e65iP7RAFTxKOEGjf876l5Y6rmpylZkAP5202guW6axATKviggGifvHMKT4bC5lbSE92BICDhNUr3Y7Gqs3ByY9DwKITLWRJarfXV6Z+HZpmhVtpzNtWlJmLNV1LcFJTEmqm/Ji30wxJqCBkywhgcNbF9kH5BIQLMgoviqwbqo5/bJAECAmG4wnJacnv7EKXojD33t+PqxvxzsWyeVs= sfree@zenbook diff --git a/dot_ssh/stevan@freeborn.cloud.pub b/dot_ssh/stevan@freeborn.cloud.pub new file mode 100644 index 0000000..725eef1 --- /dev/null +++ b/dot_ssh/stevan@freeborn.cloud.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF0eN17W/QMNsFuUi75TFVmUruRBQi8YNTBnHB/8O6uB stevan@freeborn.cloud diff --git a/dot_ssh/steves_bot_github_actions.pub b/dot_ssh/steves_bot_github_actions.pub new file mode 100644 index 0000000..a4ac49b --- /dev/null +++ b/dot_ssh/steves_bot_github_actions.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOBdBCvBM5GJ6WtK2+zReY69eZYh6dJ1cyyvPDbEluOI sfree@zenbook diff --git a/dot_ssh/tangled.pub b/dot_ssh/tangled.pub new file mode 100644 index 0000000..d83b7cc --- /dev/null +++ b/dot_ssh/tangled.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAqUFm4vgLbp4w78BT+mV6w2YGWNmg486RTyJH4VoDXA sfree@zenbook diff --git a/dot_ssh/truenas.pub b/dot_ssh/truenas.pub new file mode 100644 index 0000000..7afe507 --- /dev/null +++ b/dot_ssh/truenas.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAEXqH6rDhDp5b+NK1XbHE5qIeLESnQF4J5VANZOJsrZ sfree@zenbook diff --git a/dot_ssh/zenbook_tinker.pub b/dot_ssh/zenbook_tinker.pub new file mode 100644 index 0000000..5706672 --- /dev/null +++ b/dot_ssh/zenbook_tinker.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDLxT0ztofpvs5vUxXpkx5QEtl2DiqYshKvlwEZnd75Vx4co1Hgut7Mi9Ye2NcsNg28tuNzqAFIea6dCnca1j8AxR5nbgshLRnsg9FgqOLc6jBt6u12MfCOYegsjkTo0X2U2Jn4p69LwMY+ehaXQQhJEsggyotzMqFKTgU3OYJlSACYIPKzv8VSss7+WYfJGBlz2KWskQP0I+EfbrhFbSOTc9V2pIe12Y90XmB0SLPOk8Bp1QD1D4JHWYX6NpdfCm9LlRBucRy9u/rRIrGVs8SpJ30PJysci0oTFQhN/vpoaPWtvOBtMrB2BM+5iWBHLaiKglYNrp6ZDL1AY5Qxc3aHGq8JuYy+QOTFw/+i5Q1ClXKbm0WrjtzOYaPqsfe/F6IzQQ6ApjLmh0MRGCcL3KlpYz6vhMWjj50kTrncw1q4OHv2KjnhW2Zq8+2u/t/NY9mZCBwrExzN6mHi8kI6xvhKKt4UWU31fwu55H68IYJktIjMMJvwyuRSmt93TomGtnc= sfree@zenbook diff --git a/dot_wslconfig b/dot_wslconfig new file mode 100644 index 0000000..91b5159 --- /dev/null +++ b/dot_wslconfig @@ -0,0 +1,6 @@ +[wsl2] +memory=4GB +processors=2 + +[experimental] +autoMemoryReclaim=dropcache diff --git a/dot_zshenv b/dot_zshenv new file mode 100644 index 0000000..a604b5f --- /dev/null +++ b/dot_zshenv @@ -0,0 +1,15 @@ +# ~/.zshenv — managed by chezmoi +# Loaded for ALL zsh sessions (login, interactive, scripts). +# Keep this minimal — only environment variable exports that must be available everywhere. + +# Go +[ -d "/usr/local/go/bin" ] && export PATH="$PATH:/usr/local/go/bin" + +# Local bin +export PATH="$HOME/.local/bin:$PATH" + +# Rust / cargo +[ -f "$HOME/.cargo/env" ] && source "$HOME/.cargo/env" + +# .NET +[ -d "$HOME/.dotnet" ] && export DOTNET_ROOT="$HOME/.dotnet" && export PATH="$PATH:$HOME/.dotnet" diff --git a/dot_zshrc b/dot_zshrc new file mode 100644 index 0000000..781e2f1 --- /dev/null +++ b/dot_zshrc @@ -0,0 +1,69 @@ +# ~/.zshrc — managed by chezmoi +# Linux (Ubuntu/Debian) shell configuration — Oh My Zsh + +# --- Oh My Zsh --- +export ZSH="$HOME/.oh-my-zsh" +ZSH_THEME="lavender-dimmed" + +plugins=( + git + zsh-autosuggestions + zsh-syntax-highlighting + virtualenv +) + +ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE="fg=#b39cd0" +ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE="20" +ZSH_AUTOSUGGEST_USE_ASYNC=1 + +[ -f "$ZSH/oh-my-zsh.sh" ] && source "$ZSH/oh-my-zsh.sh" + +# --- zoxide (smart cd) --- +if command -v zoxide &>/dev/null; then + eval "$(zoxide init zsh)" + alias cd='z' +fi + +# --- NVM --- +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" +[ -s "$NVM_DIR/bash_completion" ] && source "$NVM_DIR/bash_completion" + +# --- envman --- +[ -s "$HOME/.config/envman/load.sh" ] && source "$HOME/.config/envman/load.sh" + +# --- History --- +HISTFILE=~/.zsh_history +HISTSIZE=10000 +SAVEHIST=10000 +setopt HIST_IGNORE_DUPS +setopt HIST_FIND_NO_DUPS +setopt SHARE_HISTORY + +# --- Key bindings --- +bindkey '^[[A' history-search-backward +bindkey '^[[B' history-search-forward + +# --- Aliases --- +alias grep='grep --color=auto' +alias ls='ls --color=auto' +alias ll='ls -lah' +alias la='ls -A' +alias l='ls -CF' +alias catc='xclip -selection clipboard <' + +# Git aliases (mirrors PowerShell profile) +alias acp='function _acp() { git add . && git commit -am "$1" && git push; }; _acp' +alias acnvp='function _acnvp() { git add . && git commit --no-verify -am "$1" && git push; }; _acnvp' + +# Neovim +alias nvimconfig='cd $HOME/.config/nvim' +alias editnvim='cd $HOME/.config/nvim && nvim' + +# Utilities +alias fromjson='python3 -m json.tool' + +# --- Python --- +export VIRTUAL_ENV_DISABLE_PROMPT=1 +export PYTHONIOENCODING="utf-8" + diff --git a/packages/linux.txt b/packages/linux.txt new file mode 100644 index 0000000..eaaa033 --- /dev/null +++ b/packages/linux.txt @@ -0,0 +1,29 @@ +# Linux apt packages +# These are installed via: sudo apt-get install -y $(cat linux.txt | grep -v '^#' | tr '\n' ' ') + +# Core utilities +build-essential +curl +wget +git +jq +unzip +zip + +# Shell +zsh +xclip + +# Modern CLI tools (apt versions) +fd-find +ripgrep +zoxide +fzf + +# Development +sqlite3 +make +python3 + +# Networking +openssh-client diff --git a/packages/windows.json b/packages/windows.json new file mode 100644 index 0000000..7834ad7 --- /dev/null +++ b/packages/windows.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://aka.ms/winget-packages.schema.2.0.json", + "CreationDate": "2026-07-07", + "Sources": [ + { + "Packages": [ + { "PackageIdentifier": "Audacity.Audacity" }, + { "PackageIdentifier": "Balena.Etcher" }, + { "PackageIdentifier": "Bitwarden.Bitwarden" }, + { "PackageIdentifier": "Bitwarden.CLI" }, + { "PackageIdentifier": "Clement.bottom" }, + { "PackageIdentifier": "DBBrowserForSQLite.DBBrowserForSQLite" }, + { "PackageIdentifier": "Docker.DockerDesktop" }, + { "PackageIdentifier": "Doppler.doppler" }, + { "PackageIdentifier": "ezwinports.make" }, + { "PackageIdentifier": "sharkdp.fd" }, + { "PackageIdentifier": "Gyan.FFmpeg" }, + { "PackageIdentifier": "GIMP.GIMP.3" }, + { "PackageIdentifier": "Git.Git" }, + { "PackageIdentifier": "GitHub.cli" }, + { "PackageIdentifier": "GoLang.Go" }, + { "PackageIdentifier": "goreleaser.goreleaser" }, + { "PackageIdentifier": "JetBrains.dotPeek" }, + { "PackageIdentifier": "JesseDuffield.lazygit" }, + { "PackageIdentifier": "JanDeDobbeleer.OhMyPosh" }, + { "PackageIdentifier": "twpayne.chezmoi" }, + { "PackageIdentifier": "Microsoft.DotNet.SDK.8" }, + { "PackageIdentifier": "Microsoft.DotNet.SDK.10" }, + { "PackageIdentifier": "Microsoft.OpenJDK.11" }, + { "PackageIdentifier": "Microsoft.PowerShell" }, + { "PackageIdentifier": "Microsoft.PowerToys" }, + { "PackageIdentifier": "Microsoft.VisualStudio.2022.BuildTools" }, + { "PackageIdentifier": "Microsoft.VisualStudioCode" }, + { "PackageIdentifier": "Microsoft.WindowsTerminal" }, + { "PackageIdentifier": "Microsoft.WSL" }, + { "PackageIdentifier": "MongoDB.Compass.Full" }, + { "PackageIdentifier": "Neovim.Neovim" }, + { "PackageIdentifier": "CoreyButler.NVMforWindows" }, + { "PackageIdentifier": "OBSProject.OBSStudio" }, + { "PackageIdentifier": "Obsidian.Obsidian" }, + { "PackageIdentifier": "Ollama.Ollama" }, + { "PackageIdentifier": "Postman.Postman" }, + { "PackageIdentifier": "astral-sh.uv" }, + { "PackageIdentifier": "BurntSushi.ripgrep.MSVC" }, + { "PackageIdentifier": "Rustlang.Rustup" }, + { "PackageIdentifier": "SlackTechnologies.Slack" }, + { "PackageIdentifier": "SQLite.SQLite" }, + { "PackageIdentifier": "Tailscale.Tailscale" }, + { "PackageIdentifier": "VideoLAN.VLC" }, + { "PackageIdentifier": "JetBrains.WebStorm" }, + { "PackageIdentifier": "WinSCP.WinSCP" }, + { "PackageIdentifier": "ajeetdsouza.zoxide" }, + { "PackageIdentifier": "Canonical.Ubuntu" } + ], + "SourceDetails": { + "Argument": "https://cdn.winget.microsoft.com/cache", + "Identifier": "Microsoft.Winget.Source_8wekyb3d8bbwe", + "Name": "winget", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" +} diff --git a/scripts/bootstrap.ps1 b/scripts/bootstrap.ps1 new file mode 100644 index 0000000..77572c2 --- /dev/null +++ b/scripts/bootstrap.ps1 @@ -0,0 +1,119 @@ +#!/usr/bin/env pwsh +# bootstrap.ps1 +# One-shot bootstrap for a fresh Windows machine. +# Installs chezmoi and Bitwarden CLI, authenticates with Bitwarden, +# then runs chezmoi init --apply to pull and apply this dotfiles repo. +# +# Usage (run from an elevated or standard PowerShell prompt): +# iwr -useb https://raw.githubusercontent.com/StevanFreeborn/dotfiles/main/scripts/bootstrap.ps1 | iex + +$ErrorActionPreference = "Stop" + +$DOTFILES_REPO = "https://gitea.freeborn.cloud/Stevan/dotfiles.git" +$DOTFILES_REPO_FALLBACK = "StevanFreeborn/dotfiles" + +function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan } +function Write-Ok($msg) { Write-Host " $msg" -ForegroundColor Green } +function Write-Skip($msg) { Write-Host " $msg" -ForegroundColor DarkGray } +function Write-Err($msg) { Write-Host " ERROR: $msg" -ForegroundColor Red } + +# --- Check winget --- +Write-Step "Checking winget..." +if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { + Write-Err "winget not found. Install App Installer from the Microsoft Store and re-run." + exit 1 +} +Write-Ok "winget is available." + +# --- Helper: winget install if not present --- +function Install-WingetPackage($id, $name) { + if (Get-Command $name -ErrorAction SilentlyContinue) { + Write-Skip "$name already installed, skipping." + return + } + Write-Step "Installing $name ($id) via winget..." + winget install --id $id --silent --accept-package-agreements --accept-source-agreements + # Refresh PATH so the new binary is usable in this session + $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + + [System.Environment]::GetEnvironmentVariable("PATH", "User") + Write-Ok "$name installed." +} + +# --- Install Bitwarden CLI --- +Install-WingetPackage "Bitwarden.CLI" "bw" + +# --- Install chezmoi --- +Install-WingetPackage "twpayne.chezmoi" "chezmoi" + +# --- Bitwarden login --- +Write-Step "Checking Bitwarden login status..." +$bwStatus = (bw status 2>$null | ConvertFrom-Json -ErrorAction SilentlyContinue).status + +if ($bwStatus -ne "unlocked") { + if ($bwStatus -ne "unauthenticated" -and $null -ne $bwStatus) { + Write-Skip "Already logged in, skipping login." + } else { + Write-Step "Logging into Bitwarden (enter your email and master password)..." + bw login + if ($LASTEXITCODE -ne 0) { + Write-Err "Bitwarden login failed. Please check your credentials and re-run." + exit 1 + } + } + + Write-Step "Unlocking Bitwarden vault..." + $env:BW_SESSION = bw unlock --raw + if (-not $env:BW_SESSION) { + Write-Err "Failed to unlock Bitwarden vault. Re-run the script and try again." + exit 1 + } + Write-Ok "Vault unlocked. BW_SESSION is set." +} else { + Write-Skip "Bitwarden vault already unlocked." + if (-not $env:BW_SESSION) { + Write-Step "Refreshing BW_SESSION..." + $env:BW_SESSION = bw unlock --raw + } +} + +# --- Bootstrap dotfiles --- +Write-Step "Bootstrapping dotfiles with chezmoi..." + +$chezmoiSourceDir = Join-Path $env:USERPROFILE ".local\share\chezmoi" + +if (Test-Path (Join-Path $chezmoiSourceDir ".git")) { + # Already initialized — pull latest and apply + Write-Step "chezmoi already initialized, pulling latest changes..." + chezmoi update + if ($LASTEXITCODE -ne 0) { + Write-Err "chezmoi update failed." + exit 1 + } + Write-Ok "Dotfiles updated and applied." +} else { + # Fresh init — try Gitea first, fall back to GitHub mirror + $applied = $false + try { + chezmoi init --apply $DOTFILES_REPO + $applied = $LASTEXITCODE -eq 0 + } catch { + $applied = $false + } + + if (-not $applied) { + Write-Host " Primary Gitea repo unreachable, trying GitHub mirror..." -ForegroundColor Yellow + chezmoi init --apply $DOTFILES_REPO_FALLBACK + if ($LASTEXITCODE -ne 0) { + Write-Err "chezmoi init failed. Check the repo URL and your network connection." + exit 1 + } + } +} + +Write-Host "" +Write-Host "Bootstrap complete!" -ForegroundColor Green +Write-Host "" +Write-Host "Next steps:" -ForegroundColor Yellow +Write-Host " - Restart your terminal to pick up the new PowerShell profile." +Write-Host " - Run 'chezmoi update' at any time to sync the latest changes." +Write-Host " - To add a new machine later, re-run this script." diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100644 index 0000000..e058be9 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# bootstrap.sh +# One-shot bootstrap for a fresh Linux (Ubuntu/Debian) machine. +# Installs curl, git, chezmoi, and Bitwarden CLI, authenticates with Bitwarden, +# then runs chezmoi init --apply to pull and apply this dotfiles repo. +# +# Usage: +# bash <(curl -fsLS https://raw.githubusercontent.com/StevanFreeborn/dotfiles/main/scripts/bootstrap.sh) + +set -euo pipefail + +DOTFILES_REPO="https://gitea.freeborn.cloud/Stevan/dotfiles.git" +DOTFILES_REPO_FALLBACK="StevanFreeborn/dotfiles" + +step() { echo -e "\n\033[0;36m==> $*\033[0m"; } +ok() { echo -e " \033[0;32m$*\033[0m"; } +skip() { echo -e " \033[0;90m$*\033[0m"; } +err() { echo -e " \033[0;31mERROR: $*\033[0m" >&2; exit 1; } + +# --- Ensure running on a supported distro --- +if ! command -v apt-get &>/dev/null; then + err "This script requires apt-get (Ubuntu/Debian). Adjust for your distro." +fi + +# --- Install curl and git --- +step "Ensuring curl and git are installed..." +missing=() +command -v curl &>/dev/null || missing+=("curl") +command -v git &>/dev/null || missing+=("git") + +if [ ${#missing[@]} -gt 0 ]; then + sudo apt-get update -qq + sudo apt-get install -y "${missing[@]}" + ok "Installed: ${missing[*]}" +else + skip "curl and git already present." +fi + +# --- Install Bitwarden CLI --- +# Ensure /snap/bin is in PATH — snap binaries aren't visible in the current +# shell session without this when running via bash <(curl ...) +command -v snap &>/dev/null && export PATH="$PATH:/snap/bin" + +step "Checking Bitwarden CLI..." +if command -v bw &>/dev/null; then + skip "bw already installed ($(bw --version))." +else + if command -v snap &>/dev/null; then + step "Installing Bitwarden CLI via snap..." + sudo snap install bw + ok "bw installed via snap." + else + # Fallback: download the official Linux binary from GitHub releases + step "snap not available; downloading Bitwarden CLI binary..." + BW_VERSION=$(curl -fsLS "https://api.github.com/repos/bitwarden/clients/releases/latest" \ + | grep '"tag_name"' | grep cli | head -1 | sed 's/.*"cli-v\([^"]*\)".*/\1/' || echo "") + if [ -z "$BW_VERSION" ]; then + err "Could not determine latest Bitwarden CLI version. Install bw manually and re-run." + fi + TMP_ZIP=$(mktemp /tmp/bw-XXXXXX.zip) + curl -fsLS "https://github.com/bitwarden/clients/releases/download/cli-v${BW_VERSION}/bw-linux-${BW_VERSION}.zip" \ + -o "$TMP_ZIP" + sudo unzip -o "$TMP_ZIP" -d /usr/local/bin/ bw + sudo chmod +x /usr/local/bin/bw + rm -f "$TMP_ZIP" + ok "bw ${BW_VERSION} installed to /usr/local/bin/bw." + fi +fi + +# --- Install chezmoi --- +step "Checking chezmoi..." +if command -v chezmoi &>/dev/null; then + skip "chezmoi already installed ($(chezmoi --version))." +else + step "Installing chezmoi..." + sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin" + export PATH="$HOME/.local/bin:$PATH" + ok "chezmoi installed." +fi + +# --- Bitwarden login --- +step "Checking Bitwarden login status..." +BW_STATUS=$(bw status 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','unauthenticated'))" 2>/dev/null || echo "unauthenticated") + +if [ "$BW_STATUS" != "unlocked" ]; then + if [ "$BW_STATUS" = "unauthenticated" ]; then + step "Logging into Bitwarden (enter your email and master password)..." + bw login || err "Bitwarden login failed. Check your credentials and re-run." + else + skip "Already logged in." + fi + + step "Unlocking Bitwarden vault..." + export BW_SESSION + BW_SESSION=$(bw unlock --raw) || err "Failed to unlock Bitwarden vault." + ok "Vault unlocked. BW_SESSION is set." +else + skip "Bitwarden vault already unlocked." + if [ -z "${BW_SESSION:-}" ]; then + step "Refreshing BW_SESSION..." + export BW_SESSION + BW_SESSION=$(bw unlock --raw) + fi +fi + +# --- Bootstrap dotfiles --- +step "Bootstrapping dotfiles with chezmoi..." + +CHEZMOI_SOURCE_DIR="$HOME/.local/share/chezmoi" + +if [ -d "$CHEZMOI_SOURCE_DIR/.git" ]; then + # Already initialized — pull latest and apply + step "chezmoi already initialized, pulling latest changes..." + chezmoi update || err "chezmoi update failed." + ok "Dotfiles updated and applied." +else + # Fresh init — try Gitea first, fall back to GitHub mirror + if chezmoi init --apply "$DOTFILES_REPO" 2>/dev/null; then + ok "Dotfiles applied from Gitea." + else + echo " Primary Gitea repo unreachable, trying GitHub mirror..." + chezmoi init --apply "$DOTFILES_REPO_FALLBACK" \ + || err "chezmoi init failed. Check the repo URL and your network connection." + ok "Dotfiles applied from GitHub mirror." + fi +fi + +echo "" +echo -e "\033[0;32mBootstrap complete!\033[0m" +echo "" +echo "IMPORTANT: Open a new terminal (or run: exec zsh) before continuing." +echo " ~/.local/bin is now in your PATH via ~/.zshenv, but your current" +echo " shell session won't see it until you start a new one." +echo "" +echo " chezmoi is at: $HOME/.local/bin/chezmoi" +echo "" +echo "Next steps:" +echo " 1. Open a new terminal" +echo " 2. Run: chezmoi update at any time to sync the latest changes" diff --git a/scripts/import-ssh-keys-to-bitwarden.ps1 b/scripts/import-ssh-keys-to-bitwarden.ps1 new file mode 100644 index 0000000..c91bae9 --- /dev/null +++ b/scripts/import-ssh-keys-to-bitwarden.ps1 @@ -0,0 +1,145 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Imports SSH private keys from ~/.ssh into Bitwarden as Secure Notes. + +.DESCRIPTION + Creates a Bitwarden Secure Note for each private SSH key using the naming + convention expected by the dotfiles setup script: + + "SSH Key - " + + e.g. "SSH Key - zenbook_tinker", "SSH Key - stevan@freeborn.cloud" + + Public keys (.pub files) and non-key files (config, known_hosts) are skipped. + + Requires the Bitwarden CLI (bw) to be installed and your vault to be unlocked: + + bw login # first time + $env:BW_SESSION = bw unlock --raw # each session + +.EXAMPLE + # Unlock Bitwarden, then run the script + $env:BW_SESSION = bw unlock --raw + .\import-ssh-keys-to-bitwarden.ps1 + +.EXAMPLE + # Dry-run: see what would be imported without creating anything + .\import-ssh-keys-to-bitwarden.ps1 -WhatIf +#> + +[CmdletBinding(SupportsShouldProcess)] +param ( + [string]$SshDir = (Join-Path $env:USERPROFILE ".ssh") +) + +$ErrorActionPreference = "Stop" + +# --- Preflight checks --- + +if (-not (Get-Command bw -ErrorAction SilentlyContinue)) { + Write-Error "Bitwarden CLI (bw) not found. Install it with: winget install Bitwarden.CLI" + exit 1 +} + +if (-not $env:BW_SESSION) { + Write-Error "BW_SESSION is not set. Unlock your vault first:`n `$env:BW_SESSION = bw unlock --raw" + exit 1 +} + +$vaultStatus = bw status 2>$null | ConvertFrom-Json +if ($vaultStatus.status -ne "unlocked") { + Write-Error "Bitwarden vault is not unlocked. Run: `$env:BW_SESSION = bw unlock --raw" + exit 1 +} + +if (-not (Test-Path $SshDir)) { + Write-Error "SSH directory not found: $SshDir" + exit 1 +} + +# --- Identify private keys --- + +$skipNames = @("config", "known_hosts", "known_hosts.old", "authorized_keys") + +$privateKeys = Get-ChildItem $SshDir -File | Where-Object { + $_.Extension -ne ".pub" -and $_.Name -notin $skipNames +} + +if ($privateKeys.Count -eq 0) { + Write-Host "No private keys found in $SshDir" -ForegroundColor Yellow + exit 0 +} + +Write-Host "Found $($privateKeys.Count) private key(s) in $SshDir`n" -ForegroundColor Cyan + +# --- Get existing Bitwarden items to avoid duplicates --- + +Write-Host "Fetching existing Bitwarden items..." -ForegroundColor DarkGray +$existingItems = bw list items 2>$null | ConvertFrom-Json +$existingNames = $existingItems | ForEach-Object { $_.name } + +# --- Import each key --- + +$imported = 0 +$skipped = 0 +$failed = 0 + +foreach ($keyFile in $privateKeys) { + $itemName = "SSH Key - $($keyFile.Name)" + + Write-Host " $($keyFile.Name)" -NoNewline + + # Check for duplicate + if ($existingNames -contains $itemName) { + Write-Host " — already in Bitwarden, skipping" -ForegroundColor DarkGray + $skipped++ + continue + } + + if ($PSCmdlet.ShouldProcess($itemName, "Create Bitwarden Secure Note")) { + try { + $keyContent = Get-Content $keyFile.FullName -Raw -ErrorAction Stop + + # Build Bitwarden Secure Note JSON + $item = [ordered]@{ + organizationId = $null + collectionIds = @() + folderId = $null + type = 2 # 2 = Secure Note + name = $itemName + notes = $keyContent + favorite = $false + secureNote = @{ type = 0 } + reprompt = 0 + } + + $json = $item | ConvertTo-Json -Depth 5 -Compress + $encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($json)) + + $result = bw create item $encoded 2>&1 + if ($LASTEXITCODE -ne 0) { + throw $result + } + + Write-Host " — imported" -ForegroundColor Green + $imported++ + } + catch { + Write-Host " — FAILED: $_" -ForegroundColor Red + $failed++ + } + } +} + +# --- Summary --- + +Write-Host "" +Write-Host "Done: $imported imported, $skipped already existed, $failed failed" -ForegroundColor Cyan + +if ($imported -gt 0) { + Write-Host "" + Write-Host "Syncing vault..." -ForegroundColor DarkGray + bw sync 2>$null | Out-Null + Write-Host "Vault synced." -ForegroundColor DarkGray +} diff --git a/scripts/import-ssh-keys-to-bitwarden.sh b/scripts/import-ssh-keys-to-bitwarden.sh new file mode 100644 index 0000000..f5a19ea --- /dev/null +++ b/scripts/import-ssh-keys-to-bitwarden.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# import-ssh-keys-to-bitwarden.sh +# Imports SSH private keys from ~/.ssh into Bitwarden as Secure Notes. +# +# Creates a Bitwarden Secure Note for each private SSH key using the naming +# convention "SSH Key - ", e.g. "SSH Key - id_ed25519". +# Public keys (.pub files) and non-key files (config, known_hosts) are skipped. +# +# Requires: +# - Bitwarden CLI (bw) — install via: sudo snap install bw +# - jq — install via: sudo apt install jq +# - BW_SESSION environment variable set +# +# Usage: +# export BW_SESSION=$(bw unlock --raw) +# ./import-ssh-keys-to-bitwarden.sh +# +# Dry-run (no changes): +# ./import-ssh-keys-to-bitwarden.sh --dry-run + +set -euo pipefail + +SSH_DIR="${SSH_DIR:-$HOME/.ssh}" +DRY_RUN=false + +for arg in "$@"; do + case "$arg" in + --dry-run|-n) DRY_RUN=true ;; + *) echo "Unknown option: $arg" >&2; exit 1 ;; + esac +done + +# --- Preflight checks --- + +if ! command -v bw &>/dev/null; then + echo "Bitwarden CLI (bw) not found. Install it with: sudo snap install bw" >&2 + exit 1 +fi + +if ! command -v jq &>/dev/null; then + echo "jq not found. Install it with: sudo apt install jq" >&2 + exit 1 +fi + +if [ -z "${BW_SESSION:-}" ]; then + echo "BW_SESSION is not set. Unlock your vault first:" >&2 + echo " export BW_SESSION=\$(bw unlock --raw)" >&2 + exit 1 +fi + +vault_status=$(bw status 2>/dev/null | jq -r '.status') +if [ "$vault_status" != "unlocked" ]; then + echo "Bitwarden vault is not unlocked. Run: export BW_SESSION=\$(bw unlock --raw)" >&2 + exit 1 +fi + +if [ ! -d "$SSH_DIR" ]; then + echo "SSH directory not found: $SSH_DIR" >&2 + exit 1 +fi + +# --- Identify private keys --- + +skip_names=("config" "known_hosts" "known_hosts.old" "authorized_keys" "authorized_keys2") + +private_keys=() +while IFS= read -r -d '' file; do + filename=$(basename "$file") + ext="${filename##*.}" + [[ "$ext" == "pub" ]] && continue + + skip=false + for skip_name in "${skip_names[@]}"; do + [[ "$filename" == "$skip_name" ]] && { skip=true; break; } + done + [[ "$skip" == false ]] && private_keys+=("$file") +done < <(find "$SSH_DIR" -maxdepth 1 -type f -print0) + +if [ ${#private_keys[@]} -eq 0 ]; then + echo "No private keys found in $SSH_DIR" + exit 0 +fi + +echo "Found ${#private_keys[@]} private key(s) in $SSH_DIR" +echo "" + +# --- Get existing Bitwarden items to avoid duplicates --- + +echo "Fetching existing Bitwarden items..." +existing_names=$(bw list items 2>/dev/null | jq -r '.[].name' | sort -u) + +# --- Import each key --- + +imported=0 +skipped=0 +failed=0 + +for key_file in "${private_keys[@]}"; do + key_name=$(basename "$key_file") + item_name="SSH Key - $key_name" + + printf " %s" "$key_name" + + if echo "$existing_names" | grep -Fxq "$item_name"; then + echo " — already in Bitwarden, skipping" + ((skipped++)) + continue + fi + + if [ "$DRY_RUN" = true ]; then + echo " — would import" + ((imported++)) + continue + fi + + item_json=$(jq -n \ + --rawfile notes "$key_file" \ + --arg name "$item_name" \ + '{ + organizationId: null, + collectionIds: [], + folderId: null, + type: 2, + name: $name, + notes: $notes, + favorite: false, + secureNote: { type: 0 }, + reprompt: 0 + }') + + encoded=$(echo -n "$item_json" | base64 -w0) + + result=$(echo "$encoded" | bw create item 2>&1) + exit_code=$? + + if [ $exit_code -eq 0 ]; then + echo " — imported" + ((imported++)) + else + echo " — FAILED: $result" + ((failed++)) + fi +done + +echo "" +echo "Done: $imported imported, $skipped already existed, $failed failed" + +if [ "$imported" -gt 0 ]; then + echo "" + echo "Syncing vault..." + bw sync 2>/dev/null || true + echo "Vault synced." +fi