From ac3d03f3871f5627eb1ea8dfbd42eec37ffdebe9 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Tue, 6 Jan 2026 08:09:44 -0500 Subject: [PATCH 01/11] add cargo --- dotfiles/.bashrc | 1 + 1 file changed, 1 insertion(+) diff --git a/dotfiles/.bashrc b/dotfiles/.bashrc index 003b236..146257b 100644 --- a/dotfiles/.bashrc +++ b/dotfiles/.bashrc @@ -172,3 +172,4 @@ bind 'set bell-style none' [ -f ~/dot_config/.bashrc_local ] && source ~/dot_config/.bashrc_local export PATH=$PATH:~/lua-language-server/bin +. "$HOME/.cargo/env" From 3a402039b7acc48f5ed1cbe047841af6e57f2c67 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Wed, 7 Jan 2026 09:39:19 -0500 Subject: [PATCH 02/11] updates --- dotfiles/.bashrc | 2 +- dotfiles/.psqlrc | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/dotfiles/.bashrc b/dotfiles/.bashrc index 1e1dafd..1b1993e 100644 --- a/dotfiles/.bashrc +++ b/dotfiles/.bashrc @@ -197,4 +197,4 @@ bind 'set bell-style none' [ -f ~/dot_config/.bashrc_local ] && source ~/dot_config/.bashrc_local export PATH=$PATH:~/lua-language-server/bin -. "$HOME/.cargo/env" +[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env" diff --git a/dotfiles/.psqlrc b/dotfiles/.psqlrc index 31e531c..b93708c 100644 --- a/dotfiles/.psqlrc +++ b/dotfiles/.psqlrc @@ -1,9 +1,16 @@ -- Switch pagers with :x and :xx commands \set x '\\setenv PAGER ''less -S''' \set xx '\\setenv PAGER \'pspg -bX --no-mouse\'' -\timing on +-- \timing on \set QUIET 1 \pset linestyle unicode -- \pset border 2 \pset null ∅ \unset QUIET +-- work with vd +-- \setenv PAGER 'vd -' +-- \pset format csv +-- \pset footer off +-- \setenv PSQL_PAGER_ALWAYS 1 +-- \pset paget on + From f5dc7c53d99964d383c92bd273664f6f1530bcaf Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sat, 17 Jan 2026 23:07:51 -0500 Subject: [PATCH 03/11] remove .bashrc_local from tracking and add template - Add .gitignore to exclude .bashrc_local - Create .bashrc_local_example template with placeholder passwords - Remove .bashrc_local from git tracking Co-Authored-By: Claude Sonnet 4.5 --- .gitignore | 2 ++ dotfiles/.bashrc | 23 ++++++++++++++++-- dotfiles/.bashrc_local | 9 ------- dotfiles/.bashrc_local_example | 44 ++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 .gitignore delete mode 100644 dotfiles/.bashrc_local create mode 100644 dotfiles/.bashrc_local_example diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b9d6ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Ignore the actual .bashrc_local with real passwords +dotfiles/.bashrc_local diff --git a/dotfiles/.bashrc b/dotfiles/.bashrc index 1b1993e..6697cfd 100644 --- a/dotfiles/.bashrc +++ b/dotfiles/.bashrc @@ -112,7 +112,26 @@ alias xmsp='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | alias xms='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MSW -i % ' # alias xmspa='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MSC -i % | vd -f csv -' # alias xmspa='selected_file=$(lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd)" | fzf) && [ -n "$selected_file" ] && $MSC -i "$selected_file" | vd -f csv -' -alias xmspa='selected_file=$(lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd)" | fzf) && [ -n "$selected_file" ] && $MSC -i "$selected_file" | sed "2d" | vd -d "|" - > /dev/null 2>&1' +xmspa() { + local file + + file=$( + lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | + grep -o "/swap/.*" | + sed 's|^/swap/||' | + tr "%" "/" | + sed -E 's/\.sw[op]$//' | + grep "$(pwd | sed 's|^//|/|')" | + fzf + ) || return + + [[ -z "$file" ]] && return + + eval "$MS" \ + -i "$file" | + sed "2d" | + vd -d "|" - > /dev/null 2>&1 +} alias nv='~/nvim-linux64/bin/nvim' alias gs='git status -s' alias ga='git status --untracked-files=all -s | fzf -m | awk "{print \$2}" | xargs git add ' @@ -195,6 +214,6 @@ fi bind 'set bell-style none' -[ -f ~/dot_config/.bashrc_local ] && source ~/dot_config/.bashrc_local +[ -f ~/setup_env/dotfiles/.bashrc_local ] && source ~/setup_env/dotfiles/.bashrc_local export PATH=$PATH:~/lua-language-server/bin [ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env" diff --git a/dotfiles/.bashrc_local b/dotfiles/.bashrc_local deleted file mode 100644 index 58d870c..0000000 --- a/dotfiles/.bashrc_local +++ /dev/null @@ -1,9 +0,0 @@ -#export IPTOKEN= -#export PG="psql -U ptrowbridge -d ubm -p 5432 -h usmidsap01" -#export MS="sqlcmd.exe -S mid-sql02 -i" -#export JAVA_HOME=/opt/jdk-19.0.1 -#export PATH=$PATH:$JAVA_HOME/bin -#export PATH=$PATH:/opt/gradle/gradle-7.6/bin -#export RUNNER_PATH=/opt/runner/ -#export DB2PW= -#export PGPW= diff --git a/dotfiles/.bashrc_local_example b/dotfiles/.bashrc_local_example new file mode 100644 index 0000000..f395274 --- /dev/null +++ b/dotfiles/.bashrc_local_example @@ -0,0 +1,44 @@ +# .bashrc_local - Machine-specific environment variables +# Copy this file to .bashrc_local and fill in your actual values + +# Token for IP services (if needed) +#export IPTOKEN= + +# PostgreSQL connection string +export PG="psql -U username -d database -p 5432 -h hostname" + +# SQL Server connection strings +export MS="sqlcmd -U username -C -S servername" +export MSC="sqlcmd -U username -S servername -C -s \| -W" + +# Java and Gradle paths +export JAVA_HOME=/opt/jdk-20.0.1 +export PATH=$PATH:$JAVA_HOME/bin +export PATH=$PATH:/opt/gradle/gradle-8.1/bin +export PATH=$PATH:/opt/mssql-tools18/bin + +# Runner configuration path +export RUNNER_PATH=/opt/jrunner_conf/ + +# Database passwords (fill in your actual passwords) +export DB2PW=your_db2_password_here +export PGPW=your_postgres_password_here +export SQLCMDPASSWORD='your_sqlcmd_password_here' + +# Windows SQL Server connection (if needed) +# export MSW="sqlcmd.exe -S servername -C " + +# Alternative Java/Gradle versions (commented out) +#export JAVA_HOME=/opt/jdk-19.0.1 +#export PATH=$PATH:$JAVA_HOME/bin +#export PATH=$PATH:/opt/gradle/gradle-7.6/bin +#export RUNNER_PATH=/opt/runner/ + +# Deno installation +export DENO_INSTALL="$HOME/.deno" +export PATH="$DENO_INSTALL/bin:$PATH" + +# NVM (Node Version Manager) +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion From 04d5fba435e0c32c7ebc824695c122317e5f7976 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sat, 17 Jan 2026 23:10:00 -0500 Subject: [PATCH 04/11] update xmspa to use MSC for pipe-delimited output Co-Authored-By: Claude Sonnet 4.5 --- dotfiles/.bashrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotfiles/.bashrc b/dotfiles/.bashrc index 6697cfd..dc966c1 100644 --- a/dotfiles/.bashrc +++ b/dotfiles/.bashrc @@ -127,11 +127,12 @@ xmspa() { [[ -z "$file" ]] && return - eval "$MS" \ + eval "$MSC" \ -i "$file" | sed "2d" | vd -d "|" - > /dev/null 2>&1 } + alias nv='~/nvim-linux64/bin/nvim' alias gs='git status -s' alias ga='git status --untracked-files=all -s | fzf -m | awk "{print \$2}" | xargs git add ' From c6d2eeb5fcfe21caf06109808985fe991838cd08 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sat, 17 Jan 2026 23:15:18 -0500 Subject: [PATCH 05/11] update claude.md with sql server workflow and missing aliases Added documentation for xmspa function, MSC/MSW environment variables, bashrc_local_example template, and various missing aliases (git, todo, journal shortcuts). Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b32e396 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,120 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Overview + +This is a personal development environment setup repository containing: +- Shell scripts for automated installation of development tools +- Dotfiles for bash, vim, tmux, git, psql, and pspg +- The main `setup_env.sh` script deploys dotfiles as symlinks and installs base packages + +## Key Commands + +### Environment Setup +```bash +# Initial setup - installs packages, plugin managers, and creates symlinks +./setup_env.sh + +# Individual tool installations +./install_postgres.sh # PostgreSQL from official repository +./install_python3.sh # Latest Python 3 from deadsnakes PPA +./install_java_dev.sh # SDKMAN for Java development +./install_neovim.sh # Latest Neovim release +./install_visidata.sh # VisiData for terminal data exploration +``` + +### Git Operations +```bash +# Automated commit and push (defined in .bashrc) +vc # git add . && commit with timestamp && push + +# Custom git aliases (defined in .gitconfig) +git pushall # Push to all remotes +``` + +## Architecture + +### Dotfile Management System +The repository uses **symlink-based configuration deployment**. When `setup_env.sh` runs: +1. Each dotfile in `dotfiles/` is backed up if it exists (to `.backup`) +2. A symlink is created from `~/.` to the repository's `dotfiles/.` +3. This allows version control of configs while keeping them in their expected locations + +**Critical Files:** +- `dotfiles/.bashrc` - Main bash configuration with extensive aliases and functions +- `dotfiles/.bashrc_local` - Machine-specific environment variables (PG, MS connection strings) +- `dotfiles/.bashrc_local_example` - Template for setting up machine-specific configs +- `dotfiles/.vimrc` - Vim configuration using Vundle plugin manager +- `dotfiles/.tmux.conf` - Tmux configuration with vim-style pane navigation +- `dotfiles/.gitconfig` - Git configuration with custom log format and vimdiff +- `dotfiles/.psqlrc` - PostgreSQL client configuration +- `dotfiles/.pspgconf` - pspg (PostgreSQL pager) configuration + +### Custom Bash Workflow + +The `.bashrc` contains a sophisticated workflow for working with: + +**Database Query Management:** + +PostgreSQL workflow: +- `xnspa()` function - Interactive file selector for running PostgreSQL queries through nvim swap files, outputs to VisiData as CSV +- `xnsp` - Select and execute SQL file through fzf with pspg output +- `xns` - Select and execute SQL file through fzf +- `PG` environment variable - psql connection string to database (defined in `.bashrc_local`) + +SQL Server workflow: +- `xmspa()` function - Interactive file selector for SQL Server queries through nvim swap files, outputs to VisiData with pipe-delimited format +- `xmsp` - Select SQL file via fzf, execute with sqlcmd, pipe to pspg +- `xms` - Select SQL file via fzf, execute with sqlcmd +- `MS` environment variable - sqlcmd connection string to SQL Server (defined in `.bashrc_local`) +- `MSC` environment variable - sqlcmd connection string with pipe-delimited output format (defined in `.bashrc_local`) +- `MSW` environment variable - Windows sqlcmd connection (optional, defined in `.bashrc_local`) + +General: +- `ons` - List open nvim files in current directory tree + +**Git workflow aliases:** +- `gs` - git status short format +- `ga` - Interactive git add using fzf for file selection +- `gx` - Interactive git checkout using fzf +- `gr` - git reset HEAD +- `gc` - git commit verbose +- `gd` - git difftool +- `gl` - Pretty git log with colors + +**Todo tracking:** +- `td` - Find unchecked todo items using ripgrep +- `tdp` - Find priority todos (with 🔼 or ⏫) +- `tdtp` - Find top priority todos (⏫ only) +- `tdo` - Open todo in nvim at exact line +- `tdop` - Open priority todo in nvim at exact line + +**Other useful aliases:** +- `nv` - Launch Neovim from custom installation path +- `cj` - Navigate to journal directory +- `jr` - Journal sync (pull, commit, push) +- `hc` - Health/care notes sync (pull, push) + +### Plugin Managers +- **Vim**: Vundle (installed to `~/.vim/bundle/Vundle.vim`) +- **Tmux**: TPM - Tmux Plugin Manager (installed to `~/.tmux/plugins/tpm`) + - Plugins: tmux-resurrect, jimeh/tmux-themepack +- **Bash**: bash-git-prompt (installed to `~/.bash-git-prompt`) + +### Package Dependencies +Base packages installed by `setup_env.sh`: +- tmux, vim, git, pspg, bat, fzf, ripgrep + +## Important Conventions + +### Environment Variables +Machine-specific environment variables (database connections, tokens, passwords) belong in `dotfiles/.bashrc_local`, not `.bashrc`. This file is sourced by `.bashrc` and should contain sensitive or machine-specific configuration. + +The `.bashrc_local` file is gitignored for security. Use `dotfiles/.bashrc_local_example` as a template when setting up a new machine. Copy it to `.bashrc_local` and fill in your actual credentials. + +### Symlink Pattern +When modifying dotfiles, remember they are symlinked. Changes are automatically tracked by git since the actual files are in the repository's `dotfiles/` directory. + +### Git Commit Messages +Based on recent commits, this repository uses simple lowercase commit messages describing the change (e.g., "add cargo", "visidata", "install sdkman"). From abaf2669042509aaf62bda41ecdd18a1f11b96fe Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sat, 17 Jan 2026 23:28:44 -0500 Subject: [PATCH 06/11] improve install scripts with modern practices and transparency - Use modern signed-by method for PostgreSQL GPG keys - Auto-detect latest Python 3 version from deadsnakes PPA - Add command tracing (set -x) to show all commands as they run - Display sudo commands upfront before execution - Add user confirmation prompts (y/N) before installation - Improve error handling with set -euo pipefail - Add proper cleanup and verification steps - Use pipx for VisiData installation - Better output formatting with clear section headers Co-Authored-By: Claude Sonnet 4.5 --- install_java_dev.sh | 82 ++++++++++++++++++++++++++++++++++++++++-- install_neovim.sh | 80 +++++++++++++++++++++++++++++++++++++++-- install_postgres.sh | 65 ++++++++++++++++++++++++++++----- install_python3.sh | 87 +++++++++++++++++++++++++++++++++++++-------- install_visidata.sh | 69 +++++++++++++++++++++++++++++++++-- 5 files changed, 354 insertions(+), 29 deletions(-) diff --git a/install_java_dev.sh b/install_java_dev.sh index e0e4c75..467b658 100755 --- a/install_java_dev.sh +++ b/install_java_dev.sh @@ -1,3 +1,81 @@ -sudo apt install zip +#!/bin/bash +set -euo pipefail -curl -s "https://get.sdkman.io" | bash +echo "============================================" +echo "SDKMAN (Java Development) Installation Script" +echo "============================================" +echo "" +echo "This script will run the following commands with sudo:" +echo " - apt-get update" +echo " - apt-get install -y zip unzip curl" +echo "" +read -p "Continue with installation? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Installation cancelled." + exit 0 +fi + +echo "" +echo "Starting installation (commands will be shown as they run)..." +echo "" + +# Enable command tracing +set -x + +# Install prerequisites +sudo apt-get update +sudo apt-get install -y zip unzip curl + +set +x + +# Check if SDKMAN is already installed +if [ -d "$HOME/.sdkman" ]; then + echo "" + echo "============================================" + echo "SDKMAN is already installed at ~/.sdkman" + echo "To update SDKMAN, run: sdk selfupdate" + echo "============================================" + exit 0 +fi + +# Install SDKMAN +echo "Installing SDKMAN..." +set -x + +if ! curl -s "https://get.sdkman.io" | bash; then + set +x + echo "Error: SDKMAN installation failed" >&2 + exit 1 +fi + +set +x + +# Source SDKMAN to make it available in current session +export SDKMAN_DIR="$HOME/.sdkman" +[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh" + +echo "" +echo "============================================" +# Verify installation +if [ -d "$HOME/.sdkman" ] && [ -f "$HOME/.sdkman/bin/sdkman-init.sh" ]; then + echo "SDKMAN installed successfully!" + echo "" + echo "To start using SDKMAN, either:" + echo " 1. Restart your shell, or" + echo " 2. Run: source ~/.bashrc" + echo "" + echo "Then you can install Java with:" + echo " sdk list java # List available Java versions" + echo " sdk install java # Install latest Java" + echo " sdk install java 21.0.1-tem # Install specific version" + echo "" + echo "Other useful SDKMAN commands:" + echo " sdk install gradle # Install Gradle" + echo " sdk install maven # Install Maven" + echo " sdk list # List all available SDKs" +else + echo "Error: SDKMAN installation verification failed" >&2 + exit 1 +fi +echo "============================================" diff --git a/install_neovim.sh b/install_neovim.sh index 400e147..d9f3227 100755 --- a/install_neovim.sh +++ b/install_neovim.sh @@ -1,5 +1,79 @@ -curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux64.tar.gz -sudo rm -rf /opt/nvim +#!/bin/bash +set -euo pipefail + +echo "============================================" +echo "Neovim Latest Installation Script" +echo "============================================" +echo "" +echo "This script will run the following commands with sudo:" +echo " - apt-get update (if curl not installed)" +echo " - apt-get install -y curl (if needed)" +echo " - rm -rf /opt/nvim-linux64 (if old installation exists)" +echo " - tar -C /opt -xzf nvim-linux64.tar.gz" +echo "" +read -p "Continue with installation? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Installation cancelled." + exit 0 +fi + +echo "" +echo "Starting installation (commands will be shown as they run)..." +echo "" + +# Enable command tracing +set -x + +# Check for required tools +if ! command -v curl &> /dev/null; then + sudo apt-get update + sudo apt-get install -y curl +fi + +# Disable tracing temporarily for cleaner download messages +set +x + +# Download latest Neovim +echo "Downloading Neovim..." +TEMP_DIR=$(mktemp -d) +cd "$TEMP_DIR" + +if ! curl -fsSL -o nvim-linux64.tar.gz https://github.com/neovim/neovim/releases/latest/download/nvim-linux64.tar.gz; then + echo "Error: Failed to download Neovim" >&2 + rm -rf "$TEMP_DIR" + exit 1 +fi + +echo "Download complete." +set -x + +# Remove old installation if it exists +if [ -d /opt/nvim-linux64 ]; then + sudo rm -rf /opt/nvim-linux64 +fi + +# Extract to /opt sudo tar -C /opt -xzf nvim-linux64.tar.gz -export PATH="$PATH:/opt/nvim-linux64/bin" +set +x + +# Clean up +cd - > /dev/null +rm -rf "$TEMP_DIR" + +echo "" +echo "============================================" +# Verify installation +if [ -x /opt/nvim-linux64/bin/nvim ]; then + echo "Neovim installed successfully!" + /opt/nvim-linux64/bin/nvim --version | head -n1 + echo "" + echo "Neovim is installed at: /opt/nvim-linux64/bin/nvim" + echo "Add to PATH by adding this to your ~/.bashrc:" + echo ' export PATH="$PATH:/opt/nvim-linux64/bin"' +else + echo "Error: Neovim installation failed" >&2 + exit 1 +fi +echo "============================================" diff --git a/install_postgres.sh b/install_postgres.sh index e998f21..8fd6d6b 100755 --- a/install_postgres.sh +++ b/install_postgres.sh @@ -1,12 +1,61 @@ -# Create the file repository configuration: -sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' +#!/bin/bash +set -euo pipefail -# Import the repository signing key: -wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - +echo "============================================" +echo "PostgreSQL Installation Script" +echo "============================================" +echo "" +echo "This script will run the following commands with sudo:" +echo " - mkdir -p /etc/apt/keyrings" +echo " - curl ... | gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg" +echo " - tee /etc/apt/sources.list.d/pgdg.list" +echo " - apt-get update" +echo " - apt-get install -y postgresql" +echo "" +read -p "Continue with installation? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Installation cancelled." + exit 0 +fi -# Update the package lists: +echo "" +echo "Starting installation (commands will be shown as they run)..." +echo "" + +# Enable command tracing +set -x + +# Create directory for keyrings if it doesn't exist +sudo mkdir -p /etc/apt/keyrings + +# Download and install the PostgreSQL GPG key +if [ ! -f /etc/apt/keyrings/postgresql.gpg ]; then + curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | \ + sudo gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg +fi + +# Add PostgreSQL repository +echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | \ + sudo tee /etc/apt/sources.list.d/pgdg.list > /dev/null + +# Update package lists sudo apt-get update -# Install the latest version of PostgreSQL. -# If you want a specific version, use 'postgresql-12' or similar instead of 'postgresql': -sudo apt-get -y install postgresql +# Install latest PostgreSQL +sudo apt-get install -y postgresql + +# Disable command tracing for cleaner output +set +x + +echo "" +echo "============================================" +# Verify installation +if command -v psql &> /dev/null; then + echo "PostgreSQL installed successfully!" + psql --version +else + echo "Error: PostgreSQL installation failed" >&2 + exit 1 +fi +echo "============================================" diff --git a/install_python3.sh b/install_python3.sh index d280195..22f9963 100755 --- a/install_python3.sh +++ b/install_python3.sh @@ -1,24 +1,83 @@ #!/bin/bash +set -euo pipefail -# Update the package list +echo "============================================" +echo "Python 3 Latest Installation Script" +echo "============================================" +echo "" +echo "This script will run the following commands with sudo:" +echo " - apt-get update" +echo " - apt-get install -y software-properties-common" +echo " - add-apt-repository -y ppa:deadsnakes/ppa" +echo " - apt-get update" +echo " - apt-get install -y python3.X python3.X-venv python3.X-dev python3.X-distutils" +echo " - (to install pip via get-pip.py)" +echo "" +read -p "Continue with installation? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Installation cancelled." + exit 0 +fi + +echo "" +echo "Starting installation (commands will be shown as they run)..." +echo "" + +# Enable command tracing +set -x + +# Install prerequisites +sudo apt-get update +sudo apt-get install -y software-properties-common + +# Add deadsnakes PPA +sudo add-apt-repository -y ppa:deadsnakes/ppa sudo apt-get update -# Install the software-properties-common package -sudo apt-get install software-properties-common +# Disable tracing temporarily to find latest version cleanly +set +x -# Add the deadsnakes PPA to the sources list -sudo add-apt-repository ppa:deadsnakes/ppa +# Find the latest Python 3 version available +echo "Finding latest Python 3 version..." +LATEST_PYTHON=$(apt-cache search --names-only '^python3\.[0-9]+$' | \ + grep -oP 'python3\.\d+' | \ + sort -V | \ + tail -1) -# Update the package list again -sudo apt-get update +if [ -z "$LATEST_PYTHON" ]; then + echo "Error: Could not determine latest Python 3 version" >&2 + exit 1 +fi -# Check the latest version of Python 3 available -latest_version=$(apt-cache madison python3 | awk '{print $3}' | grep "^3\." | sort -V | tail -1) +echo "Installing $LATEST_PYTHON..." +set -x -# Install the latest version of Python 3 -sudo apt-get install -y python3=$latest_version +sudo apt-get install -y \ + "$LATEST_PYTHON" \ + "$LATEST_PYTHON-venv" \ + "$LATEST_PYTHON-dev" \ + "$LATEST_PYTHON-distutils" -# Verify the installation -python3 --version -which python3 +# Install pip for the new Python version +curl -sS https://bootstrap.pypa.io/get-pip.py | sudo "$LATEST_PYTHON" +# Disable command tracing for cleaner output +set +x + +echo "" +echo "============================================" +# Verify installation +if command -v "$LATEST_PYTHON" &> /dev/null; then + echo "$LATEST_PYTHON installed successfully!" + "$LATEST_PYTHON" --version + "$LATEST_PYTHON" -m pip --version +else + echo "Error: $LATEST_PYTHON installation failed" >&2 + exit 1 +fi +echo "" +echo "Note: Use '$LATEST_PYTHON' to run this version" +echo "To make it the default python3, run:" +echo " sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/$LATEST_PYTHON 1" +echo "============================================" diff --git a/install_visidata.sh b/install_visidata.sh index 03acc92..5611bf0 100755 --- a/install_visidata.sh +++ b/install_visidata.sh @@ -1,3 +1,68 @@ -pip3 install visidata +#!/bin/bash +set -euo pipefail -# https://www.visidata.org/install/ +echo "============================================" +echo "VisiData Installation Script" +echo "============================================" +echo "" +echo "This script will run the following commands with sudo:" +echo " - apt-get update (if pipx not installed)" +echo " - apt-get install -y pipx (if needed)" +echo "" +read -p "Continue with installation? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Installation cancelled." + exit 0 +fi + +echo "" +echo "Starting installation (commands will be shown as they run)..." +echo "" + +# Enable command tracing +set -x + +# Check if python3 is available +if ! command -v python3 &> /dev/null; then + set +x + echo "Error: python3 is not installed. Please install Python 3 first." >&2 + exit 1 +fi + +# Install pipx if not available (recommended way to install VisiData) +if ! command -v pipx &> /dev/null; then + sudo apt-get update + sudo apt-get install -y pipx + set +x + pipx ensurepath + set -x +fi + +set +x + +# Install VisiData using pipx +echo "Installing VisiData via pipx..." +set -x +pipx install visidata + +# Disable command tracing for cleaner output +set +x + +echo "" +echo "============================================" +# Verify installation +if command -v vd &> /dev/null; then + echo "VisiData installed successfully!" + vd --version + echo "" + echo "Note: If 'vd' command is not found, you may need to:" + echo " 1. Restart your shell, or" + echo " 2. Run: source ~/.bashrc" +else + echo "Warning: VisiData installed but 'vd' command not in PATH" >&2 + echo "You may need to restart your shell or run: source ~/.bashrc" >&2 +fi +echo "" +echo "Documentation: https://www.visidata.org/" +echo "============================================" From 0c1f15e31a45de13f7a84108fdedf81a28bd6ad7 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sun, 18 Jan 2026 10:19:38 -0500 Subject: [PATCH 07/11] install nvchad --- install_nvchad.sh | 113 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100755 install_nvchad.sh diff --git a/install_nvchad.sh b/install_nvchad.sh new file mode 100755 index 0000000..00372e5 --- /dev/null +++ b/install_nvchad.sh @@ -0,0 +1,113 @@ +#!/bin/bash +set -euo pipefail + +echo "============================================" +echo "NvChad Configuration Installation Script" +echo "============================================" +echo "" +echo "This script will:" +echo " - Backup existing ~/.config/nvim to ~/.config/nvim.backup (if exists)" +echo " - Clone your NvChad config from git@gitea.hptrow.me:pt/nvchad.git (customize branch)" +echo " - Launch nvim to auto-install lazy.nvim and all plugins" +echo "" +echo "Prerequisites:" +echo " - Neovim 0.9.5+ must be installed (run ./install_neovim.sh if needed)" +echo " - Git must be installed" +echo " - SSH key must be set up for gitea.hptrow.me" +echo "" +read -p "Continue with installation? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Installation cancelled." + exit 0 +fi + +echo "" +echo "Starting installation..." +echo "" + +# Check prerequisites +if ! command -v nvim &> /dev/null; then + echo "Error: Neovim is not installed or not in PATH" >&2 + echo "Please run ./install_neovim.sh first" >&2 + exit 1 +fi + +if ! command -v git &> /dev/null; then + echo "Error: Git is not installed" >&2 + exit 1 +fi + +# Verify Neovim version +NVIM_VERSION=$(nvim --version | head -n1 | grep -oP 'v\K[0-9]+\.[0-9]+' || echo "0.0") +REQUIRED_VERSION="0.9" +if [ "$(printf '%s\n' "$REQUIRED_VERSION" "$NVIM_VERSION" | sort -V | head -n1)" != "$REQUIRED_VERSION" ]; then + echo "Error: Neovim version $NVIM_VERSION is too old (need 0.9.5+)" >&2 + exit 1 +fi + +echo "Neovim version: $(nvim --version | head -n1)" +echo "" + +# Backup existing config if it exists +if [ -d ~/.config/nvim ]; then + echo "Backing up existing ~/.config/nvim to ~/.config/nvim.backup" + if [ -d ~/.config/nvim.backup ]; then + rm -rf ~/.config/nvim.backup + fi + mv ~/.config/nvim ~/.config/nvim.backup +fi + +# Clone the config +echo "Cloning NvChad config from gitea (customize branch)..." +set -x +git clone -b customize git@gitea.hptrow.me:pt/nvchad.git ~/.config/nvim +set +x + +if [ ! -d ~/.config/nvim ]; then + echo "Error: Failed to clone config" >&2 + exit 1 +fi + +echo "" +echo "Config cloned successfully!" +echo "" +echo "============================================" +echo "First Launch Setup" +echo "============================================" +echo "" +echo "Neovim will now launch and automatically:" +echo " 1. Bootstrap lazy.nvim plugin manager" +echo " 2. Install NvChad (as a plugin)" +echo " 3. Install all configured plugins" +echo "" +echo "This may take a few minutes on first run." +echo "After installation completes, close nvim with :q" +echo "" +read -p "Press Enter to launch nvim and complete setup..." -r +echo "" + +# Launch nvim to trigger plugin installation +nvim +q + +echo "" +echo "============================================" +echo "Installation Complete!" +echo "============================================" +echo "" +echo "Your NvChad configuration is installed at: ~/.config/nvim" +echo "" +echo "Key customizations in this config:" +echo " - Theme: vscode_dark" +echo " - Tab width: 4 spaces" +echo " - Obsidian.nvim integration" +echo " - SQL development tools (pg_format, sqlfluff)" +echo " - Mason for installing formatters" +echo " - Custom keybindings: g+w (format), gb (blame), more in lua/mappings.lua" +echo "" +echo "Next time you launch nvim, everything will be ready!" +echo "" +if [ -d ~/.config/nvim.backup ]; then + echo "Note: Your old config was backed up to ~/.config/nvim.backup" +fi +echo "============================================" From f71458a34fd7ee377e4a9127830dc6a8f91b3702 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Mon, 20 Apr 2026 21:01:35 -0400 Subject: [PATCH 08/11] add td time-tracking script and deploy_bin installer `td` is a Python CLI for tracking time on markdown todos identified by ^tid block-refs, writing to a CSV at $TD_LOG (default ./time.csv). Shell wrappers tstart/tstop/treport in .bashrc wrap it. setup_env.sh gains deploy_bin() which symlinks every file in dotfiles/bin/ into ~/.local/bin/, mirroring how deploy_configs handles dotfiles. Co-Authored-By: Claude Opus 4.7 --- dotfiles/.bashrc | 24 ++++++--- dotfiles/bin/td | 128 +++++++++++++++++++++++++++++++++++++++++++++++ setup_env.sh | 13 +++++ 3 files changed, 158 insertions(+), 7 deletions(-) create mode 100755 dotfiles/bin/td diff --git a/dotfiles/.bashrc b/dotfiles/.bashrc index dc966c1..8eb0403 100644 --- a/dotfiles/.bashrc +++ b/dotfiles/.bashrc @@ -102,21 +102,21 @@ alias opg="lsof 2>/dev/null +D . | grep 'pg.*swp$' | awk '{print \$9}' | sed 's/ alias osw="lsof 2>/dev/null +D . | awk '\$NF ~ /swp$/ {print \$9}' | sed 's/\.swp//g' | sed 's/\/\./\//g'" alias xpg="lsof 2>/dev/null +D . | grep 'pg.*swp$' | awk '{print \$9}' | sed 's/\.swp//g' | sed 's/\/\./\//g' | xargs -r $PG -f" alias xsw="lsof 2>/dev/null +D . | grep '.*swp$' | awk '{print \$9}' | sed 's/\.swp//g' | sed 's/\/\./\//g' | xargs -r $PG -f" -alias ons='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd)"' -alias xns='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | sed "s/\\.swo$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $PG -f %' -alias xnsp='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | sed "s/\\.swo$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $PG -f % | pspg' +alias ons='lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v "lsof:" | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd)"' +alias xns='lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v "lsof:" | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | sed "s/\\.swo$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $PG -f %' +alias xnsp='lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v "lsof:" | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | sed "s/\\.swo$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $PG -f % | pspg' # alias xnspa='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | sed "s/\\.swo$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $PG -f % --csv | vd - > /dev/null 2>&1' #alias xnsp='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd)" | fzf | xargs -I % $PG -f % | pspg' alias mns='fzf | xargs -I {} sqlcmd -U Pricing -S mid-sql02 -C -i {} | pspg' -alias xmsp='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MS -i % | pspg' -alias xms='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MSW -i % ' +alias xmsp='lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v "lsof:" | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MS -i % | pspg' +alias xms='lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v "lsof:" | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MSW -i % ' # alias xmspa='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MSC -i % | vd -f csv -' # alias xmspa='selected_file=$(lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd)" | fzf) && [ -n "$selected_file" ] && $MSC -i "$selected_file" | vd -f csv -' xmspa() { local file file=$( - lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | + lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v 'lsof:' | grep -o "/swap/.*" | sed 's|^/swap/||' | tr "%" "/" | @@ -143,6 +143,13 @@ alias tdtp='rg "\- \[[^(x|~)]\].*⏫"' # alias tdo='rg "\- \[[^x]\]" | fzf | xargs nvim' alias tdo='rg "\- \[[^(x|~)]\]" --line-number | fzf | awk -F: "{print \$1, \"+\"\$2}" | xargs -r nvim' alias tdop='rg "\- \[[^(x|~)]\].*(🔼|⏫)" --line-number | fzf | awk -F: "{print \$1, \"+\"\$2}" | xargs -r nvim' + +# time-track markdown todos — see `command td --help`. Log at $TD_LOG (default ./time.csv) +# `command td` bypasses the `td` alias (which is rg for finding todos). +tstart() { command td start "$@"; } +tstop() { command td stop; } +treport() { command td report "$@"; } + alias gr='git reset HEAD' alias gc='git commit -v' alias gd='git difftool' @@ -155,7 +162,7 @@ xnspa() { local file file=$( - lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | + lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v 'lsof:' | grep -o "/swap/.*" | sed 's|^/swap/||' | tr "%" "/" | @@ -218,3 +225,6 @@ bind 'set bell-style none' [ -f ~/setup_env/dotfiles/.bashrc_local ] && source ~/setup_env/dotfiles/.bashrc_local export PATH=$PATH:~/lua-language-server/bin [ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env" + +# opencode +export PATH=/home/ptrowbridge/.opencode/bin:$PATH diff --git a/dotfiles/bin/td b/dotfiles/bin/td new file mode 100755 index 0000000..83bd00e --- /dev/null +++ b/dotfiles/bin/td @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""td — time-track markdown todos identified by ^tid block-refs. + +Usage: + td start [--file PATH] [--desc TEXT] + td stop + td report [FILTER] # FILTER matches tid or file substring + td current # print the currently-running tid (or nothing) + td tidgen # print a new unique tid + +Log location: $TD_LOG (default ./time.csv) +""" +import argparse, csv, os, re, subprocess, sys +from datetime import datetime +from collections import defaultdict + +LOG = os.environ.get("TD_LOG", "./time.csv") +FIELDS = ["started_at", "stopped_at", "tid", "file", "description"] + +def now(): + return datetime.now().isoformat(timespec="seconds") + +def read_rows(): + if not os.path.exists(LOG): + return [] + with open(LOG, newline="") as f: + return list(csv.DictReader(f)) + +def write_rows(rows): + with open(LOG, "w", newline="") as f: + w = csv.DictWriter(f, fieldnames=FIELDS) + w.writeheader() + w.writerows(rows) + +def lookup_task(tid): + try: + out = subprocess.check_output( + ["rg", "--no-heading", "--with-filename", rf"\^{tid}\b", "."], + text=True, stderr=subprocess.DEVNULL, + ) + except (subprocess.CalledProcessError, FileNotFoundError): + return "", "" + first = out.splitlines()[0] if out else "" + if not first: + return "", "" + path, _, line = first.partition(":") + file = path.removeprefix("./") + desc = re.sub(r"^\s*-\s*\[.\]\s*", "", line) + desc = re.sub(r"\s*\^\S+\s*$", "", desc).strip() + return file, desc + +def cmd_start(args): + rows = read_rows() + ts = now() + stopped = [r["tid"] for r in rows if not r["stopped_at"]] + for r in rows: + if not r["stopped_at"]: + r["stopped_at"] = ts + file, desc = args.file or "", args.desc or "" + if not (file or desc): + file, desc = lookup_task(args.tid) + if not file: + print(f"warning: ^{args.tid} not found in {os.getcwd()} — logging with empty file/desc", file=sys.stderr) + rows.append({"started_at": ts, "stopped_at": "", "tid": args.tid, "file": file, "description": desc}) + write_rows(rows) + if stopped: + print(f" stopped {', '.join(stopped)} first") + snippet = f" ({file}: {desc[:50]})" if desc else "" + print(f"started {args.tid}{snippet}") + +def cmd_stop(args): + rows = read_rows() + if not rows: + print(f"no log at {LOG}"); sys.exit(1) + ts = now() + stopped = [] + for r in rows: + if not r["stopped_at"]: + r["stopped_at"] = ts + stopped.append(r["tid"]) + if not stopped: + print("nothing running"); sys.exit(1) + write_rows(rows) + print(f"stopped {', '.join(stopped)}") + +def cmd_report(args): + rows = read_rows() + if not rows: + print(f"no log at {LOG}"); sys.exit(1) + totals, files, running = defaultdict(int), {}, [] + for r in rows: + if args.filter and args.filter not in r["tid"] and args.filter not in r["file"]: + continue + start = datetime.fromisoformat(r["started_at"]) + files[r["tid"]] = r["file"] + if r["stopped_at"]: + stop = datetime.fromisoformat(r["stopped_at"]) + totals[r["tid"]] += int((stop - start).total_seconds()) + else: + running.append((r["tid"], start, r["file"], r["description"])) + for tid in sorted(totals): + t = totals[tid] + h, m = t // 3600, (t % 3600) // 60 + print(f"{tid:<24} {h:>3}h{m:02d}m {files.get(tid,'')}") + for tid, start, file, desc in running: + print(f"{tid:<24} RUNNING since {start.strftime('%H:%M')} {file}: {desc[:40]}") + +def cmd_current(args): + for r in read_rows(): + if not r["stopped_at"]: + print(r["tid"]); return + +def cmd_tidgen(args): + print(f"tid-{datetime.now().strftime('%Y%m%d-%H%M%S')}") + +def main(): + p = argparse.ArgumentParser(prog="td", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + sp = p.add_subparsers(dest="cmd", required=True) + s = sp.add_parser("start"); s.add_argument("tid"); s.add_argument("--file", default=""); s.add_argument("--desc", default=""); s.set_defaults(func=cmd_start) + sp.add_parser("stop").set_defaults(func=cmd_stop) + s = sp.add_parser("report"); s.add_argument("filter", nargs="?"); s.set_defaults(func=cmd_report) + sp.add_parser("current").set_defaults(func=cmd_current) + sp.add_parser("tidgen").set_defaults(func=cmd_tidgen) + args = p.parse_args() + args.func(args) + +if __name__ == "__main__": + main() diff --git a/setup_env.sh b/setup_env.sh index 241d4c8..09a7d32 100755 --- a/setup_env.sh +++ b/setup_env.sh @@ -65,6 +65,18 @@ deploy_configs() { source ~/.bashrc } +# Deploy executable scripts from dotfiles/bin/ into ~/.local/bin/ +deploy_bin() { + echo "Deploying scripts to ~/.local/bin/ ..." + local src_dir="$(pwd)/dotfiles/bin" + [[ -d "$src_dir" ]] || return 0 + mkdir -p ~/.local/bin + for script in "$src_dir"/*; do + [[ -f "$script" ]] || continue + create_symlink "$script" ~/.local/bin/"$(basename "$script")" + done +} + # Main script main() { install_packages @@ -72,6 +84,7 @@ main() { install_vundle install_git_bash_prompt deploy_configs + deploy_bin echo "Setup complete! Please restart your shell or run 'source ~/.bashrc' for changes to take effect." } From f27b5656c078ffbe4d8b6aafa20b5acf43dbb0d8 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Mon, 20 Apr 2026 21:03:51 -0400 Subject: [PATCH 09/11] document td time-tracking and deploy_bin in CLAUDE.md Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b32e396..3691b0d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,9 +37,9 @@ git pushall # Push to all remotes ### Dotfile Management System The repository uses **symlink-based configuration deployment**. When `setup_env.sh` runs: -1. Each dotfile in `dotfiles/` is backed up if it exists (to `.backup`) -2. A symlink is created from `~/.` to the repository's `dotfiles/.` -3. This allows version control of configs while keeping them in their expected locations +1. `deploy_configs` symlinks each dotfile in `dotfiles/` to `~/.` (backing up any existing file) +2. `deploy_bin` symlinks every file in `dotfiles/bin/` to `~/.local/bin/` +3. This allows version control of configs and user-local scripts while keeping them in their expected locations **Critical Files:** - `dotfiles/.bashrc` - Main bash configuration with extensive aliases and functions @@ -50,6 +50,7 @@ The repository uses **symlink-based configuration deployment**. When `setup_env. - `dotfiles/.gitconfig` - Git configuration with custom log format and vimdiff - `dotfiles/.psqlrc` - PostgreSQL client configuration - `dotfiles/.pspgconf` - pspg (PostgreSQL pager) configuration +- `dotfiles/bin/td` - Python CLI for time-tracking markdown todos (see Time tracking section) ### Custom Bash Workflow @@ -83,13 +84,20 @@ General: - `gd` - git difftool - `gl` - Pretty git log with colors -**Todo tracking:** +**Todo grep aliases (rg-based):** - `td` - Find unchecked todo items using ripgrep - `tdp` - Find priority todos (with 🔼 or ⏫) - `tdtp` - Find top priority todos (⏫ only) - `tdo` - Open todo in nvim at exact line - `tdop` - Open priority todo in nvim at exact line +**Time tracking (functions wrapping `dotfiles/bin/td`):** +- `tstart ` - Start a timer on a task identified by a `^tid-*` block-ref +- `tstop` - Stop the running timer +- `treport [filter]` - Show totals per tid, filterable by tid or filename +- Wrappers use `command td` to bypass the `td` rg alias. The CLI writes one row per entry to `$TD_LOG` (default `./time.csv`, cwd-scoped). +- Companion nvim integration (in the separate `~/.config/nvim` repo) adds `ts/tp/tr` keymaps with auto-generated tids. + **Other useful aliases:** - `nv` - Launch Neovim from custom installation path - `cj` - Navigate to journal directory From fcf1132a6168b04bbb2a098db259b727c04f09aa Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Fri, 24 Apr 2026 17:25:06 -0400 Subject: [PATCH 10/11] add td week subcommand, nvim centralization, and README td week parses git log for task create/complete events and joins with time.csv for weekly "what got done" reports. Nvim integration moved into dotfiles/nvim/ (symlinked in by new deploy_nvim step) so all three td surfaces live under dotfiles. README covers the deploy pattern and td. Co-Authored-By: Claude Opus 4.7 --- dotfiles/.bashrc | 1 + dotfiles/README.md | 94 +++++++++++++++++++++++++++ dotfiles/bin/td | 102 ++++++++++++++++++++++++++++- dotfiles/nvim/td.lua | 119 ++++++++++++++++++++++++++++++++++ dotfiles/nvim/td_mappings.lua | 11 ++++ setup_env.sh | 18 +++++ 6 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 dotfiles/README.md create mode 100644 dotfiles/nvim/td.lua create mode 100644 dotfiles/nvim/td_mappings.lua diff --git a/dotfiles/.bashrc b/dotfiles/.bashrc index 8eb0403..a9c61e5 100644 --- a/dotfiles/.bashrc +++ b/dotfiles/.bashrc @@ -149,6 +149,7 @@ alias tdop='rg "\- \[[^(x|~)]\].*(🔼|⏫)" --line-number | fzf | awk -F: "{pri tstart() { command td start "$@"; } tstop() { command td stop; } treport() { command td report "$@"; } +tweek() { command td week "$@"; } alias gr='git reset HEAD' alias gc='git commit -v' diff --git a/dotfiles/README.md b/dotfiles/README.md new file mode 100644 index 0000000..72e96a9 --- /dev/null +++ b/dotfiles/README.md @@ -0,0 +1,94 @@ +# dotfiles + +Symlink-deployed config. The parent `~/setup_env/setup_env.sh` installs each file here to its destination path — edit the file in this repo, the change goes live. + +| Source in this repo | Deployed to | Deployed by | +| --- | --- | --- | +| `.bashrc`, `.vimrc`, `.gitconfig`, `.pspgconf`, `.psqlrc`, `.tmux.conf`, `.bashrc_local` | `~/` | `deploy_configs` | +| `bin/*` | `~/.local/bin/` | `deploy_bin` | +| `nvim/*.lua` | `~/.config/nvim/lua/.lua` | `deploy_nvim` | + +On a fresh machine: clone the `setup_env` repo, then from `~/setup_env/` run: + +```bash +./install_neovim.sh +./install_nvchad.sh +./install_python3.sh +./setup_env.sh # creates all the symlinks + installs apt packages +``` + +After install you still need to add one line to the NvChad-provided `~/.config/nvim/lua/mappings.lua` (it's not tracked here because NvChad owns that file): + +```lua +require("td_mappings") +``` + +Everything else picks up automatically. + +--- + +## td — markdown todo time tracking + +Track time on markdown todos identified by an Obsidian block-ref `^tid-*` at the end of the task line. Data lives in `time.csv` at the vault root; reports cross-join time with git history to show what was created vs. completed. + +### The three surfaces (one script) + +All logic is in `bin/td` — a Python 3 stdlib script. The other surfaces are thin wrappers so the script is reachable from each editing context. + +| Surface | Location | Purpose | +| --- | --- | --- | +| Python script | `bin/td` → `~/.local/bin/td` | Single source of truth. All subcommands. | +| Shell wrappers | `.bashrc` (`tstart` / `tstop` / `treport` / `tweek`) | Bypass the `td` alias (see gotcha). | +| Nvim module | `nvim/td.lua` + `nvim/td_mappings.lua` | `:TdStart` / `:TdStop` / `:TdReport` / `:TdWeek` commands + `t{s,p,r,w}` keymaps. | + +### Data model + +`time.csv` at the vault root: + +``` +started_at, stopped_at, tid, file, description +``` + +- `tid` — block-ref identifier, format `tid-YYYYMMDD-HHMMSS`, appended as `^tid-...` to the task line in the markdown file +- `started_at` / `stopped_at` — ISO-8601 local timestamps; a running entry has an empty `stopped_at` +- one row per time segment; `td stop` fills in `stopped_at` on the open row + +Vault root is discovered by walking up from the current buffer file looking for `time.csv` or `.obsidian/`. + +### Subcommands + +| Command | What it does | +| --- | --- | +| `td start [--file PATH] [--desc TEXT]` | Start a timer. Auto-stops any running entry first. If `--file` / `--desc` aren't given, greps the vault for the tid to populate them. | +| `td stop` | Close the open entry. | +| `td report [FILTER]` | Total time per tid (filter matches tid or file substring). | +| `td current` | Print the currently-running tid (or nothing). | +| `td tidgen` | Print a fresh `tid-YYYYMMDD-HHMMSS`. | +| `td week [--since DATE]` | Task create/complete events from `git log -p`, joined with `time.csv`. Default range: since Monday 00:00 of this week. | + +### How `td week` works + +Scans `git log -p --reverse --no-renames` from cwd, pairs `-`/`+` task-line diffs within each commit by tid, classifies each event: + +- `[x]` **done** — `- [ ]` → `- [x]` transition in one commit +- `[ ]` **new** — added `- [ ]` line with a fresh tid +- `[+]` **done+new** — `- [x]` added with no prior `- [ ]` for that tid (created and completed in the same commit) +- `[o]` **reopen** — `- [x]` → `- [ ]` transition + +Event timestamp is commit time, not toggle time — batched commits share one timestamp. Fine for weekly "what did I get done" reports; not precise enough for hourly auditing. + +### Nvim integration + +`ts` / `:TdStart` on a `- [ ]` task line: +1. If the line has no `^tid-*` block ref, auto-generates `tid-YYYYMMDD-HHMMSS`, appends it, saves the buffer. +2. Starts the timer with the file path (relative to vault root) and the task text (minus checkbox + block ref) as description. + +Other mappings: `tp` stop, `tr` report float, `tw` week float. `q` or `` closes a float. + +Known limitation: NvChad defers `mappings.lua` via `vim.schedule`, so `:TdStart` etc. aren't available inside `-c` arguments on headless invocations. Interactive use is fine. + +### Gotchas + +- **`td` alone is aliased to `rg`** (for the `td` / `tdp` / `tdo` todo-grep family, defined in `.bashrc`). So `td week` runs `rg "\- \[[^(x|~)]\]" week` and errors out. Use `tweek` (shell wrapper that calls `command td week`) or `command td week` directly. +- **CSV is cwd-scoped.** `$TD_LOG` defaults to `./time.csv`, so subcommands must be run from (or under) the vault root. The nvim wrapper handles this by walking up to find the vault root; the shell wrappers trust your cwd. +- **Data-model changes go in `bin/td`.** The shell and nvim surfaces should stay thin — if a new subcommand is useful in nvim too, add it to `bin/td`, then a one-line `tfoo()` wrapper in `.bashrc` and a `M.foo` + mapping in `nvim/td.lua` / `nvim/td_mappings.lua`. diff --git a/dotfiles/bin/td b/dotfiles/bin/td index 83bd00e..cddb1be 100755 --- a/dotfiles/bin/td +++ b/dotfiles/bin/td @@ -7,11 +7,12 @@ Usage: td report [FILTER] # FILTER matches tid or file substring td current # print the currently-running tid (or nothing) td tidgen # print a new unique tid + td week [--since DATE] # task create/complete events from git log, joined with time.csv Log location: $TD_LOG (default ./time.csv) """ import argparse, csv, os, re, subprocess, sys -from datetime import datetime +from datetime import datetime, timedelta from collections import defaultdict LOG = os.environ.get("TD_LOG", "./time.csv") @@ -113,6 +114,104 @@ def cmd_current(args): def cmd_tidgen(args): print(f"tid-{datetime.now().strftime('%Y%m%d-%H%M%S')}") +TID_RE = re.compile(r"\^(tid-[\w-]+)") +TASK_ADD_RE = re.compile(r"^\+(?!\+\+)\s*-\s*\[([ xX])\]\s*(.*)$") +TASK_REM_RE = re.compile(r"^-(?!--)\s*-\s*\[([ xX])\]\s*(.*)$") + +def _tid_totals(): + totals = defaultdict(int) + for r in read_rows(): + if r["stopped_at"]: + start = datetime.fromisoformat(r["started_at"]) + stop = datetime.fromisoformat(r["stopped_at"]) + totals[r["tid"]] += int((stop - start).total_seconds()) + return totals + +def _scan_git(since): + SEP = "---TDWEEK-COMMIT---" + try: + out = subprocess.check_output( + ["git", "log", f"--since={since}", "--reverse", + f"--format={SEP}%n%cI", "-p", "--no-color", "--no-renames"], + text=True, stderr=subprocess.DEVNULL, + ) + except (subprocess.CalledProcessError, FileNotFoundError): + return + for chunk in out.split(SEP + "\n"): + if not chunk.strip(): + continue + lines = chunk.split("\n") + ts = lines[0] + current_file = None + adds, removes = [], [] + for ln in lines[1:]: + if ln.startswith("+++ b/"): + current_file = ln[6:].split("\t")[0].rstrip(); continue + if ln.startswith("--- ") or ln.startswith("diff --git") or ln.startswith("index ") or ln.startswith("@@"): + continue + if not current_file or current_file == "/dev/null": + continue + m = TASK_ADD_RE.match(ln) + if m: + status = m.group(1).lower().strip() or " " + tm = TID_RE.search(m.group(2)) + if tm: + desc = TID_RE.sub("", m.group(2)).strip() + adds.append((current_file, status, tm.group(1), desc)) + continue + m = TASK_REM_RE.match(ln) + if m: + status = m.group(1).lower().strip() or " " + tm = TID_RE.search(m.group(2)) + if tm: + removes.append((current_file, status, tm.group(1))) + rem_by_tid = {t[2]: t for t in removes} + for file, status, tid, desc in adds: + prior = rem_by_tid.get(tid) + if prior: + if prior[1] == " " and status == "x": + yield (ts, "done", tid, file, desc) + elif prior[1] == "x" and status == " ": + yield (ts, "reopen", tid, file, desc) + else: + yield (ts, "done+new" if status == "x" else "new", tid, file, desc) + +def _default_since(): + today = datetime.now() + monday = today - timedelta(days=today.weekday()) + return monday.strftime("%Y-%m-%d 00:00") + +def _fmt_dur(secs): + return f"{secs // 3600}h{(secs % 3600) // 60:02d}m" + +def cmd_week(args): + since = args.since or _default_since() + events = list(_scan_git(since)) + totals = _tid_totals() + if not events: + print(f"no task events since {since}"); return + by_day = defaultdict(list) + for ts, kind, tid, file, desc in events: + day = ts[:10] + by_day[day].append((ts, kind, tid, file, desc)) + marker = {"done": "[x]", "new": "[ ]", "done+new": "[+]", "reopen": "[o]"} + n_done = n_new = total_secs = 0 + counted_tids = set() + print(f"Week since {since}\n") + for day in sorted(by_day): + dt = datetime.fromisoformat(day) + print(f"{dt.strftime('%a %Y-%m-%d')}") + for ts, kind, tid, file, desc in by_day[day]: + t = totals.get(tid, 0) + dur = _fmt_dur(t) if t else " " + print(f" {marker.get(kind,'? ')} {tid:<24} {dur:>6} {file} — {desc[:60]}") + if kind in ("done", "done+new"): n_done += 1 + if kind in ("new", "done+new"): n_new += 1 + if tid not in counted_tids: + counted_tids.add(tid); total_secs += t + print() + print(f"Totals: completed {n_done} created {n_new} time {_fmt_dur(total_secs)}") + def main(): p = argparse.ArgumentParser(prog="td", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) sp = p.add_subparsers(dest="cmd", required=True) @@ -121,6 +220,7 @@ def main(): s = sp.add_parser("report"); s.add_argument("filter", nargs="?"); s.set_defaults(func=cmd_report) sp.add_parser("current").set_defaults(func=cmd_current) sp.add_parser("tidgen").set_defaults(func=cmd_tidgen) + s = sp.add_parser("week"); s.add_argument("--since", default=None); s.set_defaults(func=cmd_week) args = p.parse_args() args.func(args) diff --git a/dotfiles/nvim/td.lua b/dotfiles/nvim/td.lua new file mode 100644 index 0000000..8361c33 --- /dev/null +++ b/dotfiles/nvim/td.lua @@ -0,0 +1,119 @@ +local M = {} + +local function vault_root() + local file = vim.api.nvim_buf_get_name(0) + local dir = file ~= "" and vim.fn.fnamemodify(file, ":p:h") or vim.fn.getcwd() + local probe = dir + while probe ~= "/" and probe ~= "" do + if vim.loop.fs_stat(probe .. "/time.csv") or vim.loop.fs_stat(probe .. "/.obsidian") then + return probe + end + local parent = vim.fn.fnamemodify(probe, ":h") + if parent == probe then break end + probe = parent + end + return vim.fn.getcwd() +end + +local function relpath(abs, base) + local a = vim.fn.fnamemodify(abs, ":p") + local b = vim.fn.fnamemodify(base, ":p"):gsub("/$", "") + if a:sub(1, #b + 1) == b .. "/" then return a:sub(#b + 2) end + return a +end + +local function run(cmd, cwd, on_done) + local stdout, stderr = {}, {} + vim.fn.jobstart(cmd, { + cwd = cwd, + stdout_buffered = true, + stderr_buffered = true, + on_stdout = function(_, d) for _, l in ipairs(d) do if l ~= "" then stdout[#stdout + 1] = l end end end, + on_stderr = function(_, d) for _, l in ipairs(d) do if l ~= "" then stderr[#stderr + 1] = l end end end, + on_exit = function(_, code) + vim.schedule(function() if on_done then on_done(stdout, stderr, code) end end) + end, + }) +end + +local function notify(lines, level) + if #lines == 0 then return end + vim.notify(table.concat(lines, "\n"), level or vim.log.levels.INFO) +end + +local function extract_desc(line) + return (line:gsub("^%s*%-%s*%[.%]%s*", ""):gsub("%s*%^%S+%s*$", ""):gsub("%s+$", "")) +end + +local function ensure_tid_on_line() + local line = vim.api.nvim_get_current_line() + local tid = line:match("%^(tid%-%S+)") + if tid then return tid end + if not line:match("^%s*%-%s*%[.%]") then + vim.notify("td: not on a todo line (- [ ] ...)", vim.log.levels.WARN) + return nil + end + tid = os.date("tid-%Y%m%d-%H%M%S") + local new = line:gsub("%s+$", "") .. " ^" .. tid + vim.api.nvim_set_current_line(new) + vim.cmd("silent! write") + return tid +end + +function M.start() + local tid = ensure_tid_on_line() + if not tid then return end + local cwd = vault_root() + local file = relpath(vim.api.nvim_buf_get_name(0), cwd) + local desc = extract_desc(vim.api.nvim_get_current_line()) + run({ "td", "start", tid, "--file", file, "--desc", desc }, cwd, function(out, err) + notify(out); notify(err, vim.log.levels.WARN) + end) +end + +function M.stop() + run({ "td", "stop" }, vault_root(), function(out, err) + notify(out); notify(err, vim.log.levels.WARN) + end) +end + +local function float(title, lines) + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.api.nvim_buf_set_option(buf, "modifiable", false) + local width = math.min(120, vim.o.columns - 4) + local height = math.min(30, #lines + 2) + vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = width, + height = height, + row = math.floor((vim.o.lines - height) / 2), + col = math.floor((vim.o.columns - width) / 2), + style = "minimal", + border = "rounded", + title = " " .. title .. " ", + title_pos = "center", + }) + vim.api.nvim_buf_set_keymap(buf, "n", "q", "close", { noremap = true, silent = true }) + vim.api.nvim_buf_set_keymap(buf, "n", "", "close", { noremap = true, silent = true }) +end + +function M.report() + run({ "td", "report" }, vault_root(), function(out, err) + if #out == 0 then notify(err, vim.log.levels.WARN); return end + float("td report", out) + end) +end + +function M.week(opts) + local cmd = { "td", "week" } + if opts and opts.args and opts.args ~= "" then + for w in opts.args:gmatch("%S+") do cmd[#cmd + 1] = w end + end + run(cmd, vault_root(), function(out, err) + if #out == 0 then notify(err, vim.log.levels.WARN); return end + float("td week", out) + end) +end + +return M diff --git a/dotfiles/nvim/td_mappings.lua b/dotfiles/nvim/td_mappings.lua new file mode 100644 index 0000000..8b4a2b4 --- /dev/null +++ b/dotfiles/nvim/td_mappings.lua @@ -0,0 +1,11 @@ +-- td: time-track markdown todos (logic lives in ~/.local/bin/td and td.lua) +-- Loaded from ~/.config/nvim/lua/mappings.lua via require("td_mappings"). +local td = require("td") +vim.api.nvim_create_user_command("TdStart", td.start, {}) +vim.api.nvim_create_user_command("TdStop", td.stop, {}) +vim.api.nvim_create_user_command("TdReport", td.report, {}) +vim.api.nvim_create_user_command("TdWeek", td.week, { nargs = "*" }) +vim.keymap.set("n", "ts", td.start, { desc = "td: start timer on current task" }) +vim.keymap.set("n", "tp", td.stop, { desc = "td: stop (pause) timer" }) +vim.keymap.set("n", "tr", td.report, { desc = "td: report floating window" }) +vim.keymap.set("n", "tw", td.week, { desc = "td: week (tasks from git log)" }) diff --git a/setup_env.sh b/setup_env.sh index 09a7d32..ccbde00 100755 --- a/setup_env.sh +++ b/setup_env.sh @@ -77,6 +77,23 @@ deploy_bin() { done } +# Deploy nvim lua modules from dotfiles/nvim/ into ~/.config/nvim/lua/ +# NOTE: requires ~/.config/nvim/ to already exist (see install_neovim.sh + install_nvchad.sh). +# Modules are required by name from ~/.config/nvim/lua/mappings.lua — e.g. require("td_mappings"). +deploy_nvim() { + echo "Deploying nvim lua modules to ~/.config/nvim/lua/ ..." + local src_dir="$(pwd)/dotfiles/nvim" + [[ -d "$src_dir" ]] || return 0 + if [[ ! -d ~/.config/nvim/lua ]]; then + echo " ~/.config/nvim/lua missing — run install_neovim.sh + install_nvchad.sh first. Skipping." + return 0 + fi + for mod in "$src_dir"/*.lua; do + [[ -f "$mod" ]] || continue + create_symlink "$mod" ~/.config/nvim/lua/"$(basename "$mod")" + done +} + # Main script main() { install_packages @@ -85,6 +102,7 @@ main() { install_git_bash_prompt deploy_configs deploy_bin + deploy_nvim echo "Setup complete! Please restart your shell or run 'source ~/.bashrc' for changes to take effect." } From cbb783bfdfd7f62becc897e5ff74be30a62c73e6 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Wed, 6 May 2026 09:14:07 -0400 Subject: [PATCH 11/11] description --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3691b0d..4660b39 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -102,7 +102,7 @@ General: - `nv` - Launch Neovim from custom installation path - `cj` - Navigate to journal directory - `jr` - Journal sync (pull, commit, push) -- `hc` - Health/care notes sync (pull, push) +- `hc` - hc comapanies notes sync (pull, push) ### Plugin Managers - **Vim**: Vundle (installed to `~/.vim/bundle/Vundle.vim`)