From 9f2a7162554c7e13044882f70d550dea5371cc49 Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Mon, 29 Jun 2026 17:03:07 -0700 Subject: [PATCH 1/5] chore: streamline dotfiles hygiene and terminal workflows Retire legacy and stale configs, align setup/CI docs with Makefile-driven automation, and ensure global gitignore is linked by Ansible. Harden shell startup behavior and improve ToggleTerm ergonomics with role-based terminals, then refresh the tmux cheatsheet to match the updated workflow. --- .github/workflows/tests.yml | 6 +- .gitignore | 2 - Makefile | 27 ++- README.md | 35 ++- Rprofile | 8 - ansible/dotfiles.yml | 5 +- ansible/tasks/link_files.yml | 1 + ansible/tasks/paperless_backup.yml | 1 - bash/bashrc | 20 +- cursor/README.md | 2 +- fish/config.fish | 43 ++-- grafana/query_grafana.bash | 30 --- hocon/preview_hocon | 6 - mailcap | 2 - msmtp/linux.rc | 13 -- msmtp/osx.rc | 14 -- mutt/accounts.muttrc | 8 - mutt/appearance.muttrc | 10 - mutt/colors | 1 - mutt/colors.muttrc | 151 ------------- mutt/compose.muttrc | 5 - mutt/encypt.muttrc | 23 -- mutt/headers.muttrc | 17 -- mutt/keymap.muttrc | 20 -- mutt/mutt-notmuch-py | 113 ---------- mutt/muttrc | 29 --- notmuch-config | 88 -------- nvim/lua/config/dbt.lua | 31 ++- nvim/lua/config/providers.lua | 2 +- nvim/lua/plugins/fyler.lua | 1 - nvim/lua/plugins/language.lua | 5 +- nvim/lua/plugins/toggleterm.lua | 211 +++++++++++++----- nvim/lua/plugins/vimwiki.lua | 2 +- offlineimap/offlineimap.py | 21 -- offlineimap/rc.linux | 25 --- offlineimap/rc.osx | 26 --- offlineimap/restart_offlineimap | 4 - .../uk.me.michaelbarton.offlineimap.plist | 23 -- ripgrep/rc | 2 +- systemd/user/offlineimap.service | 13 -- systemd/user/offlineimap.timer | 13 -- tmux/cheatsheet.md | 22 ++ urlview | 1 - yml/format_yml.py | 10 - 44 files changed, 278 insertions(+), 814 deletions(-) delete mode 100644 Rprofile delete mode 100755 grafana/query_grafana.bash delete mode 100755 hocon/preview_hocon delete mode 100644 mailcap delete mode 100644 msmtp/linux.rc delete mode 100644 msmtp/osx.rc delete mode 100644 mutt/accounts.muttrc delete mode 100644 mutt/appearance.muttrc delete mode 120000 mutt/colors delete mode 100644 mutt/colors.muttrc delete mode 100644 mutt/compose.muttrc delete mode 100644 mutt/encypt.muttrc delete mode 100644 mutt/headers.muttrc delete mode 100644 mutt/keymap.muttrc delete mode 100755 mutt/mutt-notmuch-py delete mode 100644 mutt/muttrc delete mode 100644 notmuch-config delete mode 100644 offlineimap/offlineimap.py delete mode 100644 offlineimap/rc.linux delete mode 100644 offlineimap/rc.osx delete mode 100755 offlineimap/restart_offlineimap delete mode 100644 offlineimap/uk.me.michaelbarton.offlineimap.plist delete mode 100644 systemd/user/offlineimap.service delete mode 100644 systemd/user/offlineimap.timer delete mode 100644 urlview delete mode 100755 yml/format_yml.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 17bb1449..b55b2bd4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,8 @@ name: Formatting checks -on: [push] +on: + push: + pull_request: env: TERM: xterm @@ -10,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 with: fetch-depth: 1 diff --git a/.gitignore b/.gitignore index 73ef0a93..884e25ae 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,6 @@ vim/log vim/plugged .netrwhist .idea -offlineimap/Account* -offlineimap/Repository* docker __pycache__ .DS_Store diff --git a/Makefile b/Makefile index 44cf052e..06e49a66 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,28 @@ +YAML_FILES := $(shell git ls-files '*.yml' '*.yaml' | while IFS= read -r file; do [ -f "$$file" ] && printf "%s " "$$file"; done) +MARKDOWN_FILES := $(shell git ls-files '*.md' | while IFS= read -r file; do [ -f "$$file" ] && printf "%s " "$$file"; done) +PYTHON_FILES := $(shell git ls-files '*.py' | while IFS= read -r file; do [ -f "$$file" ] && printf "%s " "$$file"; done) +LUA_FILES := $(shell git ls-files '*.lua' | while IFS= read -r file; do [ -f "$$file" ] && printf "%s " "$$file"; done) + +.PHONY: all apply fmt fmt_check nvim-health nvim-check + all: fmt apply nvim-check apply: uv run ansible-playbook -i ~/.dotfiles/ansible/inventory.ini ~/.dotfiles/ansible/dotfiles.yml fmt: - npx --loglevel error --yes prettier --write **/*.yml - uvx --with mdformat-gfm --with mdformat-frontmatter mdformat --wrap 80 --number **/*.md - uvx ruff format --line-length=100 **/*.py - uvx ruff check --fix --line-length=100 **/*.py - npx --loglevel error --yes @johnnymorganz/stylua-bin -- **/*.lua + @if [ -n "$(YAML_FILES)" ]; then npx --loglevel error --yes prettier --write $(YAML_FILES); fi + @if [ -n "$(MARKDOWN_FILES)" ]; then uvx --with mdformat-gfm --with mdformat-frontmatter mdformat --wrap 80 --number $(MARKDOWN_FILES); fi + @if [ -n "$(PYTHON_FILES)" ]; then uvx ruff format --line-length=100 $(PYTHON_FILES); fi + @if [ -n "$(PYTHON_FILES)" ]; then uvx ruff check --fix --line-length=100 $(PYTHON_FILES); fi + @if [ -n "$(LUA_FILES)" ]; then npx --loglevel error --yes @johnnymorganz/stylua-bin -- $(LUA_FILES); fi fmt_check: - npx --loglevel error --yes prettier --check **/*.yml - uvx --with mdformat-gfm --with mdformat-frontmatter mdformat --check --wrap 80 --number **/*.md - uvx ruff format --check --line-length=100 **/*.py - uvx ruff check --line-length=100 **/*.py - npx --loglevel error --yes @johnnymorganz/stylua-bin --check -- **/*.lua + @if [ -n "$(YAML_FILES)" ]; then npx --loglevel error --yes prettier --check $(YAML_FILES); fi + @if [ -n "$(MARKDOWN_FILES)" ]; then uvx --with mdformat-gfm --with mdformat-frontmatter mdformat --check --wrap 80 --number $(MARKDOWN_FILES); fi + @if [ -n "$(PYTHON_FILES)" ]; then uvx ruff format --check --line-length=100 $(PYTHON_FILES); fi + @if [ -n "$(PYTHON_FILES)" ]; then uvx ruff check --line-length=100 $(PYTHON_FILES); fi + @if [ -n "$(LUA_FILES)" ]; then npx --loglevel error --yes @johnnymorganz/stylua-bin --check -- $(LUA_FILES); fi nvim-health: nvim --headless "+checkhealth" +qa diff --git a/README.md b/README.md index 1200b309..07ae9741 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ My personal dotfiles for macOS, managed with Ansible. -## Ansible +## Quick start ```bash # Clone the repository @@ -10,35 +10,28 @@ git clone https://github.com/michaelbarton/dotfiles.git ~/.dotfiles # Apply configuration with Ansible cd ~/.dotfiles -./ansible/apply_ansible +make apply ``` -### Fish Shell +## Run specific Ansible tags ```bash -curl -sL https://git.io/fisher | source && fisher install jorgebucaran/fisher -fish -c "fisher update" +uv run ansible-playbook -i ~/.dotfiles/ansible/inventory.ini ~/.dotfiles/ansible/dotfiles.yml --tags "hooks,setup" ``` -### Email Setup - -1. Create maildb directory: +## Maintenance - ```bash - mkdir -p ~/.maildb/michaelbarton - ``` - -2. Set up Gmail password in keychain: - - ```bash - security add-generic-password -a acct.gmail -s acct.gmail -w - ``` +```bash +make fmt_check +make nvim-check +``` -3. Set up automated email sync: +### Fish Shell - ```bash - launchctl load -w ~/Library/LaunchAgents/uk.me.michaelbarton.offlineimap.plist - ``` +```bash +curl -sL https://git.io/fisher | source && fisher install jorgebucaran/fisher +fish -c "fisher update" +``` ## Customization diff --git a/Rprofile b/Rprofile deleted file mode 100644 index b68b3787..00000000 --- a/Rprofile +++ /dev/null @@ -1,8 +0,0 @@ -options( "device" = "quartz" ) - -setHook(packageEvent("grDevices", "onLoad"), - function(...) grDevices::quartz.options(width=4, height=4)) - -options(prompt="R> ", digits=4, show.signif.stars=FALSE) - -options(repos=structure(c(CRAN="https://cloud.r-project.org/"))) diff --git a/ansible/dotfiles.yml b/ansible/dotfiles.yml index 6bc41b99..db78580f 100644 --- a/ansible/dotfiles.yml +++ b/ansible/dotfiles.yml @@ -6,8 +6,9 @@ # - Configuring OS-specific launch agents # - Linking dotfiles to their proper locations # -# To run only specific tags: ./apply_ansible [tag1,tag2] -# Available tags: links, python, agents +# To run only specific tags: +# uv run ansible-playbook -i ~/.dotfiles/ansible/inventory.ini ~/.dotfiles/ansible/dotfiles.yml --tags "hooks,setup" +# Common tags: links, nvim, tmux, skills, hooks, setup, agents, paperless, paperless-backup - name: Playbook to manage dotfiles hosts: localhost diff --git a/ansible/tasks/link_files.yml b/ansible/tasks/link_files.yml index 767ccd05..0a948d74 100644 --- a/ansible/tasks/link_files.yml +++ b/ansible/tasks/link_files.yml @@ -32,6 +32,7 @@ # Misc. scripts and config - { src: "zettel/today.sh", dest: ".bin/today" } - { src: "git/gitconfig", dest: ".gitconfig" } + - { src: "gitignore", dest: ".gitignore" } - { src: "markdown/md2docx.sh", dest: ".bin/md2docx" } - { src: "poetry/update_poetry_dependencies.py", diff --git a/ansible/tasks/paperless_backup.yml b/ansible/tasks/paperless_backup.yml index 46cc0adb..9e6c581f 100644 --- a/ansible/tasks/paperless_backup.yml +++ b/ansible/tasks/paperless_backup.yml @@ -21,7 +21,6 @@ cmd: launchctl load "{{ ansible_user_dir }}/Library/LaunchAgents/uk.me.michaelbarton.paperless-backup.plist" args: creates: "{{ ansible_user_dir }}/Library/LaunchAgents/uk.me.michaelbarton.paperless-backup.plist" - # One-time manual setup required (run these once before first backup): # # security add-generic-password -a $USER -s paperless-backup-b2-id -w '' diff --git a/bash/bashrc b/bash/bashrc index 2da82bac..d032c3c4 100644 --- a/bash/bashrc +++ b/bash/bashrc @@ -1,6 +1,8 @@ #!/bin/bash -set -o nounset +if [ -n "${BASH_VERSION:-}" ]; then + set -o nounset +fi ################################################################### # @@ -57,11 +59,10 @@ alias grep='grep --color=auto' PYTHON_BIN=${HOME}/.venv/bin USER_BIN=${HOME}/.bin LOCAL_BIN=${HOME}/.local/bin -DOTFILES_BIN=${HOME}/.dotfiles/bin HOMEBREW_BIN=/opt/homebrew/bin NPM_BIN=${HOME}/.npm-global/bin -export PATH=${NPM_BIN}:${HOMEBREW_BIN}:${DOTFILES_BIN}:${USER_BIN}:${LOCAL_BIN}:${PATH} +export PATH=${NPM_BIN}:${HOMEBREW_BIN}:${USER_BIN}:${LOCAL_BIN}:${PATH} ################################################################### # @@ -69,9 +70,14 @@ export PATH=${NPM_BIN}:${HOMEBREW_BIN}:${DOTFILES_BIN}:${USER_BIN}:${LOCAL_BIN}: # ################################################################### -# fzf should use ripgrep for search -export FZF_DEFAULT_COMMAND='rg --files' -export FZF_DEFAULT_OPTS='-m --height 50% --border' +# fzf and ripgrep search defaults +if command -v fd >/dev/null 2>&1; then + export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git' +else + export FZF_DEFAULT_COMMAND='rg --files --hidden --glob "!.git"' +fi +export FZF_DEFAULT_OPTS='--height 50% --border' +export RIPGREP_CONFIG_PATH="$HOME/.config/ripgrep/rc" export MANWIDTH=80 export EDITOR=nvim @@ -107,7 +113,7 @@ export LANG='en_GB.UTF-8' # ################################################################### -if [ -n "$BASH_VERSION" ]; then +if [ -n "${BASH_VERSION:-}" ]; then # Shell history improvements export HISTSIZE=50000 export HISTFILESIZE=100000 diff --git a/cursor/README.md b/cursor/README.md index 58891a83..e6c35618 100644 --- a/cursor/README.md +++ b/cursor/README.md @@ -15,7 +15,7 @@ ln -sf ~/.dotfiles/cursor/rules ~/.cursor/rules Or run Ansible with the `hooks` or `setup` tag: ```bash -./apply_ansible hooks +uv run ansible-playbook -i ~/.dotfiles/ansible/inventory.ini ~/.dotfiles/ansible/dotfiles.yml --tags hooks ``` ### Cursor: enable third-party hooks diff --git a/fish/config.fish b/fish/config.fish index 2851a080..45da2b83 100644 --- a/fish/config.fish +++ b/fish/config.fish @@ -4,20 +4,22 @@ set -x XDG_DATA_HOME $HOME/.local/share set -x XDG_CACHE_HOME $HOME/.cache set -x XDG_STATE_HOME $HOME/.local/state -# --- Cursor Terminal Fix --- -# The complex prompt from starship can cause the terminal in Cursor (which is -# based on VS Code) to hang, as it has trouble determining when a command -# has finished executing. -# To fix this, we detect if we are running inside Cursor's terminal by -# checking if TERM_PROGRAM is "vscode". If it is, we use a simple, minimal -# prompt. Otherwise, we use the full starship prompt. -# This ensures a stable experience in Cursor without sacrificing the rich -# prompt in other terminal emulators. -# - -# +# --- Cursor terminal prompt safety --- +# Keep Cursor terminals stable by avoiding starship there, but still load the +# rest of the fish environment (aliases, PATH, functions, etc). if test "$TERM_PROGRAM" = vscode - exit 0 + function fish_prompt + set_color cyan + echo -n (prompt_pwd) " > " + set_color normal + end + + function fish_right_prompt + end +else + if command -v starship &>/dev/null + starship init fish | source + end end # Auto-attach to tmux in Ghostty (but not inside nvim, scripts, or existing tmux) @@ -25,9 +27,10 @@ if test "$TERM_PROGRAM" = ghostty; and not set -q TMUX; and not set -q NVIM; and tmux new-session -A -s main end -# Initialize starship and zoxide -starship init fish | source -zoxide init fish | source +# Initialize zoxide if installed +if command -v zoxide &>/dev/null + zoxide init fish | source +end # Initialize mise for runtime version management (if installed) if command -v mise &>/dev/null @@ -81,20 +84,22 @@ alias grep='grep --color=auto' set -x PYTHON_BIN $HOME/.venv/bin set -x USER_BIN $HOME/.bin set -x LOCAL_BIN $HOME/.local/bin -set -x DOTFILES_BIN $HOME/.dotfiles/bin set -x HOMEBREW_BIN /opt/homebrew/bin set -x NPM_BIN $HOME/.npm-global/bin # Prepend paths to PATH (fish uses fish_add_path for this) fish_add_path $NPM_BIN fish_add_path $HOMEBREW_BIN -fish_add_path $DOTFILES_BIN fish_add_path $USER_BIN fish_add_path $LOCAL_BIN fish_add_path $PYTHON_BIN # FZF configuration with better preview -set -x FZF_DEFAULT_COMMAND 'fd --type f --hidden --follow --exclude .git' +if command -v fd &>/dev/null + set -x FZF_DEFAULT_COMMAND 'fd --type f --hidden --follow --exclude .git' +else + set -x FZF_DEFAULT_COMMAND 'rg --files --hidden --glob !.git' +end # --tmux opens fzf as a tmux popup (silently ignored outside tmux) # bat --paging=never prevents bat spawning a nested pager in fzf preview set -x FZF_DEFAULT_OPTS '--tmux center,80%,70% --layout=reverse --preview "bat --style=numbers --color=always --paging=never --line-range :500 {}" --preview-window=right:60%:wrap' diff --git a/grafana/query_grafana.bash b/grafana/query_grafana.bash deleted file mode 100755 index 3b82f629..00000000 --- a/grafana/query_grafana.bash +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -set -o errexit -set -o nounset -set -o xtrace - -CONTAINER_NAME=${1:?"Usage: $0 []"} -URL="http://localhost:3100/loki/api/v1/query_range" -START_TIME=$(date -u -v-24H +%s) -END_TIME=$(date -u +%s) -QUERY="{pipelineName=\"${CONTAINER_NAME}\"} | json | line_format \"{{.log}}\" | json | line_format \"{{.message}}\" |= \"level=\"" - -# Check if the second argument (additional_filter) is provided -if [ $# -gt 1 ]; then - ADDITIONAL_FILTER="|=\"${2}\"" - QUERY="${QUERY}${ADDITIONAL_FILTER}" -fi - -RESPONSE=$(curl --get "${URL}" \ - --data-urlencode "query=${QUERY}" \ - --data-urlencode "start=${START_TIME}" \ - --data-urlencode "end=${END_TIME}" \ - --data-urlencode "limit=1000") - -if [ "$(echo "$RESPONSE" | jq '.status')" != "\"success\"" ]; then - echo "Error: Query failed. Response was: $RESPONSE" >&2 - exit 1 -fi - -echo "$RESPONSE" | jq -r ".data.result | map(.stream) | flatten | map(.message) | .[]" diff --git a/hocon/preview_hocon b/hocon/preview_hocon deleted file mode 100755 index f8e2da6c..00000000 --- a/hocon/preview_hocon +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -set -o nounset -SRC=$1 - -cat $SRC | pyhocon -f json | jq diff --git a/mailcap b/mailcap deleted file mode 100644 index 58edbde8..00000000 --- a/mailcap +++ /dev/null @@ -1,2 +0,0 @@ -text/html; w3m -T text/html -dump %s; copiousoutput -application/pdf; pdftotext %s - | par s0p0; copiousoutput diff --git a/msmtp/linux.rc b/msmtp/linux.rc deleted file mode 100644 index a9d1d179..00000000 --- a/msmtp/linux.rc +++ /dev/null @@ -1,13 +0,0 @@ -# Set default values for all following accounts. -defaults -auth on -tls on - -# Gmail -account gmail -host smtp.gmail.com -port 587 -protocol smtp -from mail@michaelbarton.me.uk -user mail@michaelbarton.me.uk -passwordeval secret-tool lookup password gmail diff --git a/msmtp/osx.rc b/msmtp/osx.rc deleted file mode 100644 index d806e59e..00000000 --- a/msmtp/osx.rc +++ /dev/null @@ -1,14 +0,0 @@ -# Set default values for all following accounts. -defaults -auth on -tls on - -# Gmail -account gmail -host smtp.gmail.com -port 587 -protocol smtp -from mail@michaelbarton.me.uk -user mail@michaelbarton.me.uk -# Change according to KeyChain entry name -passwordeval security find-generic-password -w -a acct.gmail diff --git a/mutt/accounts.muttrc b/mutt/accounts.muttrc deleted file mode 100644 index 8e088cb1..00000000 --- a/mutt/accounts.muttrc +++ /dev/null @@ -1,8 +0,0 @@ -set folder = $HOME/.maildb/michaelbarton -set spoolfile = "+INBOX" -folder-hook $HOME/.maildb/ "\ - set postponed = +'[Google Mail].Drafts' \ - record = +'[Google Mail].Sent Mail' \ - from = '$realname ' \ - sendmail = 'msmtp -a gmail' \ - sendmail_wait = 0 diff --git a/mutt/appearance.muttrc b/mutt/appearance.muttrc deleted file mode 100644 index d5f41a7c..00000000 --- a/mutt/appearance.muttrc +++ /dev/null @@ -1,10 +0,0 @@ -set sort_aux = 'last-date-received' -set index_format = "[%Z] %D %-20.20F %s" -set date_format = "%m/%d" -set nostrict_threads -set sort = 'threads' - -#set sidebar_width=26 -#set sidebar_visible=yes -#set sidebar_delim='|' -#mailboxes $HOME/.maildb/michaelbarton/Inbox $HOME/.maildb/jgi/Inbox $HOME/.maildb/michaelbarton/bioboxes $HOME/.maildb/michaelbarton/ci-server diff --git a/mutt/colors b/mutt/colors deleted file mode 120000 index 47815ac2..00000000 --- a/mutt/colors +++ /dev/null @@ -1 +0,0 @@ -/Users/michael.barton/.dotfiles/solarized/mutt-colors-solarized/mutt-colors-solarized-dark-256.muttrc \ No newline at end of file diff --git a/mutt/colors.muttrc b/mutt/colors.muttrc deleted file mode 100644 index b3186518..00000000 --- a/mutt/colors.muttrc +++ /dev/null @@ -1,151 +0,0 @@ -# vim: filetype=muttrc - -# -# -# make sure that you are using mutt linked against slang, not ncurses, or -# suffer the consequences of weird color issues. use "mutt -v" to check this. - -# custom body highlights ----------------------------------------------- -# highlight my name and other personally relevant strings -#color body color136 color234 "(ethan|schoonover)" -# custom index highlights ---------------------------------------------- -# messages which mention my name in the body -#color index color136 color234 "~b \"phil(_g|\!| gregory| gold)|pgregory\" !~N !~T !~F !~p !~P" -#color index J_cream color230 "~b \"phil(_g|\!| gregory| gold)|pgregory\" ~N !~T !~F !~p !~P" -#color index color136 color37 "~b \"phil(_g|\!| gregory| gold)|pgregory\" ~T !~F !~p !~P" -#color index color136 J_magent "~b \"phil(_g|\!| gregory| gold)|pgregory\" ~F !~p !~P" -## messages which are in reference to my mails -#color index J_magent color234 "~x \"(mithrandir|aragorn)\\.aperiodic\\.net|thorin\\.hillmgt\\.com\" !~N !~T !~F !~p !~P" -#color index J_magent color230 "~x \"(mithrandir|aragorn)\\.aperiodic\\.net|thorin\\.hillmgt\\.com\" ~N !~T !~F !~p !~P" -#color index J_magent color37 "~x \"(mithrandir|aragorn)\\.aperiodic\\.net|thorin\\.hillmgt\\.com\" ~T !~F !~p !~P" -#color index J_magent color160 "~x \"(mithrandir|aragorn)\\.aperiodic\\.net|thorin\\.hillmgt\\.com\" ~F !~p !~P" - -# for background in 16 color terminal, valid background colors include: -# base03, bg, black, any of the non brights - -# basic colors --------------------------------------------------------- -color normal color241 color234 -color error color160 color234 -color tilde color235 color234 -color message color37 color234 -color markers color160 color254 -color attachment color254 color234 -color search color61 color234 -#color status J_black J_status -color status color241 color235 -color indicator color234 color136 -color tree color136 color234 # arrow in threads - -# basic monocolor screen -mono bold bold -mono underline underline -mono indicator reverse -mono error bold - -# index ---------------------------------------------------------------- - -#color index color160 color234 "~D(!~p|~p)" # deleted -#color index color235 color234 ~F # flagged -#color index color166 color234 ~= # duplicate messages -#color index color240 color234 "~A!~N!~T!~p!~Q!~F!~D!~P" # the rest -#color index J_base color234 "~A~N!~T!~p!~Q!~F!~D" # the rest, new -color index color160 color234 "~A" # all messages -color index color166 color234 "~E" # expired messages -color index color33 color234 "~N" # new messages -color index color33 color234 "~O" # old messages -color index color61 color234 "~Q" # messages that have been replied to -color index color240 color234 "~R" # read messages -color index color33 color234 "~U" # unread messages -color index color33 color234 "~U~$" # unread, unreferenced messages -color index color241 color234 "~v" # messages part of a collapsed thread -color index color241 color234 "~P" # messages from me -color index color37 color234 "~p!~F" # messages to me -color index color37 color234 "~N~p!~F" # new messages to me -color index color37 color234 "~U~p!~F" # unread messages to me -color index color240 color234 "~R~p!~F" # messages to me -color index color160 color234 "~F" # flagged messages -color index color160 color234 "~F~p" # flagged messages to me -color index color160 color234 "~N~F" # new flagged messages -color index color160 color234 "~N~F~p" # new flagged messages to me -color index color160 color234 "~U~F~p" # new flagged messages to me -color index color235 color160 "~D" # deleted messages -color index color245 color234 "~v~(!~N)" # collapsed thread with no unread -color index color136 color234 "~v~(~N)" # collapsed thread with some unread -color index color64 color234 "~N~v~(~N)" # collapsed thread with unread parent -# statusbg used to indicated flagged when foreground color shows other status -# for collapsed thread -color index color160 color235 "~v~(~F)!~N" # collapsed thread with flagged, no unread -color index color136 color235 "~v~(~F~N)" # collapsed thread with some unread & flagged -color index color64 color235 "~N~v~(~F~N)" # collapsed thread with unread parent & flagged -color index color64 color235 "~N~v~(~F)" # collapsed thread with unread parent, no unread inside, but some flagged -color index color37 color235 "~v~(~p)" # collapsed thread with unread parent, no unread inside, some to me directly -color index color136 color160 "~v~(~D)" # thread with deleted (doesn't differentiate between all or partial) -#color index color136 color234 "~(~N)" # messages in threads with some unread -#color index color64 color234 "~S" # superseded messages -#color index color160 color234 "~T" # tagged messages -#color index color166 color160 "~=" # duplicated messages - -# message headers ------------------------------------------------------ - -#color header color240 color234 "^" -color hdrdefault color240 color234 -color header color241 color234 "^(From)" -color header color33 color234 "^(Subject)" - -# body ----------------------------------------------------------------- - -color quoted color33 color234 -color quoted1 color37 color234 -color quoted2 color136 color234 -color quoted3 color160 color234 -color quoted4 color166 color234 - -color signature color240 color234 -color bold color235 color234 -color underline color235 color234 -color normal color244 color234 -# -color body color245 color234 "[;:][-o][)/(|]" # emoticons -color body color245 color234 "[;:][)(|]" # emoticons -color body color245 color234 "[*]?((N)?ACK|CU|LOL|SCNR|BRB|BTW|CWYL|\ - |FWIW|vbg|GD&R|HTH|HTHBE|IMHO|IMNSHO|\ - |IRL|RTFM|ROTFL|ROFL|YMMV)[*]?" -color body color245 color234 "[ ][*][^*]*[*][ ]?" # more emoticon? -color body color245 color234 "[ ]?[*][^*]*[*][ ]" # more emoticon? - -## pgp - -color body color160 color234 "(BAD signature)" -color body color37 color234 "(Good signature)" -color body color234 color234 "^gpg: Good signature .*" -color body color241 color234 "^gpg: " -color body color241 color160 "^gpg: BAD signature from.*" -mono body bold "^gpg: Good signature" -mono body bold "^gpg: BAD signature from.*" - -# yes, an insance URL regex -color body color160 color234 "([a-z][a-z0-9+-]*://(((([a-z0-9_.!~*'();:&=+$,-]|%[0-9a-f][0-9a-f])*@)?((([a-z0-9]([a-z0-9-]*[a-z0-9])?)\\.)*([a-z]([a-z0-9-]*[a-z0-9])?)\\.?|[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+)(:[0-9]+)?)|([a-z0-9_.!~*'()$,;:@&=+-]|%[0-9a-f][0-9a-f])+)(/([a-z0-9_.!~*'():@&=+$,-]|%[0-9a-f][0-9a-f])*(;([a-z0-9_.!~*'():@&=+$,-]|%[0-9a-f][0-9a-f])*)*(/([a-z0-9_.!~*'():@&=+$,-]|%[0-9a-f][0-9a-f])*(;([a-z0-9_.!~*'():@&=+$,-]|%[0-9a-f][0-9a-f])*)*)*)?(\\?([a-z0-9_.!~*'();/?:@&=+$,-]|%[0-9a-f][0-9a-f])*)?(#([a-z0-9_.!~*'();/?:@&=+$,-]|%[0-9a-f][0-9a-f])*)?|(www|ftp)\\.(([a-z0-9]([a-z0-9-]*[a-z0-9])?)\\.)*([a-z]([a-z0-9-]*[a-z0-9])?)\\.?(:[0-9]+)?(/([-a-z0-9_.!~*'():@&=+$,]|%[0-9a-f][0-9a-f])*(;([-a-z0-9_.!~*'():@&=+$,]|%[0-9a-f][0-9a-f])*)*(/([-a-z0-9_.!~*'():@&=+$,]|%[0-9a-f][0-9a-f])*(;([-a-z0-9_.!~*'():@&=+$,]|%[0-9a-f][0-9a-f])*)*)*)?(\\?([-a-z0-9_.!~*'();/?:@&=+$,]|%[0-9a-f][0-9a-f])*)?(#([-a-z0-9_.!~*'();/?:@&=+$,]|%[0-9a-f][0-9a-f])*)?)[^].,:;!)? \t\r\n<>\"]" -# and a heavy handed email regex -#color body J_magent color234 "((@(([0-9a-z-]+\\.)*[0-9a-z-]+\\.?|#[0-9]+|\\[[0-9]?[0-9]?[0-9]\\.[0-9]?[0-9]?[0-9]\\.[0-9]?[0-9]?[0-9]\\.[0-9]?[0-9]?[0-9]\\]),)*@(([0-9a-z-]+\\.)*[0-9a-z-]+\\.?|#[0-9]+|\\[[0-9]?[0-9]?[0-9]\\.[0-9]?[0-9]?[0-9]\\.[0-9]?[0-9]?[0-9]\\.[0-9]?[0-9]?[0-9]\\]):)?[0-9a-z_.+%$-]+@(([0-9a-z-]+\\.)*[0-9a-z-]+\\.?|#[0-9]+|\\[[0-2]?[0-9]?[0-9]\\.[0-2]?[0-9]?[0-9]\\.[0-2]?[0-9]?[0-9]\\.[0-2]?[0-9]?[0-9]\\])" - -# Various smilies and the like -#color body color230 color234 "<[Gg]>" # -#color body color230 color234 "<[Bb][Gg]>" # -#color body color136 color234 " [;:]-*[})>{(<|]" # :-) etc... -# *bold* -#color body color33 color234 "(^|[[:space:][:punct:]])\\*[^*]+\\*([[:space:][:punct:]]|$)" -#mono body bold "(^|[[:space:][:punct:]])\\*[^*]+\\*([[:space:][:punct:]]|$)" -# _underline_ -#color body color33 color234 "(^|[[:space:][:punct:]])_[^_]+_([[:space:][:punct:]]|$)" -#mono body underline "(^|[[:space:][:punct:]])_[^_]+_([[:space:][:punct:]]|$)" -# /italic/ (Sometimes gets directory names) -#color body color33 color234 "(^|[[:space:][:punct:]])/[^/]+/([[:space:][:punct:]]|$)" -#mono body underline "(^|[[:space:][:punct:]])/[^/]+/([[:space:][:punct:]]|$)" - -# Border lines. -#color body color33 color234 "( *[-+=#*~_]){6,}" - -#folder-hook . "color status J_black J_status " -#folder-hook gmail/inbox "color status J_black color136 " -#folder-hook gmail/important "color status J_black color136 " - diff --git a/mutt/compose.muttrc b/mutt/compose.muttrc deleted file mode 100644 index 6b8e8517..00000000 --- a/mutt/compose.muttrc +++ /dev/null @@ -1,5 +0,0 @@ -set forward_format = "Fwd: %s" -set fast_reply=yes -set forward_decode # decode when forwarding -set reply_to # reply to Reply to: field -set use_envelope_from = yes diff --git a/mutt/encypt.muttrc b/mutt/encypt.muttrc deleted file mode 100644 index 1eedbea3..00000000 --- a/mutt/encypt.muttrc +++ /dev/null @@ -1,23 +0,0 @@ - -set pgp_decode_command="gpg --status-fd=2 %?p?--passphrase-fd 0? --no-verbose --quiet --batch --output - %f" -set pgp_verify_command="gpg --status-fd=2 --no-verbose --quiet --batch --output - --verify %s %f" -set pgp_decrypt_command="gpg --status-fd=2 %?p?--passphrase-fd 0? --no-verbose --quiet --batch --output - %f" -set pgp_sign_command="gpg --no-verbose --batch --quiet --output - %?p?--passphrase-fd 0? --armor --detach-sign --textmode %?a?-u %a? %f" -set pgp_clearsign_command="gpg --no-verbose --batch --quiet --output - %?p?--passphrase-fd 0? --armor --textmode --clearsign %?a?-u %a? %f" -set pgp_encrypt_only_command="pgpewrap gpg --batch --quiet --no-verbose --output - --encrypt --textmode --armor --always-trust -- -r %r -- %f" -set pgp_encrypt_sign_command="pgpewrap gpg %?p?--passphrase-fd 0? --batch --quiet --no-verbose --textmode --output - --encrypt --sign %?a?-u %a? --armor --always-trust -- -r %r -- %f" - -set pgp_import_command="gpg --no-verbose --import %f" -set pgp_export_command="gpg --no-verbose --export --armor %r" -set pgp_verify_key_command="gpg --verbose --batch --fingerprint --check-sigs %r" -set pgp_list_pubring_command="gpg --no-verbose --batch --quiet --with-colons --list-keys %r" -set pgp_list_secring_command="gpg --no-verbose --batch --quiet --with-colons --list-secret-keys %r" - -set pgp_good_sign="^gpg: Good signature from" - -set pgp_use_gpg_agent = yes -set pgp_timeout = 3600 -set crypt_replyencrypt = yes -set crypt_replysign = yes - -set pgp_sign_as = 6EB517B0 diff --git a/mutt/headers.muttrc b/mutt/headers.muttrc deleted file mode 100644 index 62290913..00000000 --- a/mutt/headers.muttrc +++ /dev/null @@ -1,17 +0,0 @@ -# Ignore all headers -ignore * - -# Then un-ignore the ones I want to see -unignore From: -unignore To: -unignore Reply-To: -unignore Subject: -unignore Date: -unignore Organization: -unignore Newsgroups: -unignore CC: -unignore BCC: -unignore User-Agent: - -# Now order the visable header lines -hdr_order Date: From: To: CC: BCC: Reply-To: Subject: Organization: User-Agent: diff --git a/mutt/keymap.muttrc b/mutt/keymap.muttrc deleted file mode 100644 index 314de486..00000000 --- a/mutt/keymap.muttrc +++ /dev/null @@ -1,20 +0,0 @@ -macro index,pager q "~/.maildb/jgi/INBOX" "Go to jgi mail" -macro index,pager w '~/.maildb/jgi/[Gmail].All Mail' "Go to all mail" -macro index,pager e '~/.maildb/jgi/[Gmail].Sent Mail' "Go to sent mail" -macro index,pager r '~/.maildb/jgi/[Gmail].Drafts' "Go to drafts" - -macro index,pager a "~/.maildb/michaelbarton/INBOX" "Go to michaelbarton mail" -macro index,pager s '~/.maildb/michaelbarton/[Google Mail].All Mail' "Go to all mail" -macro index,pager d '~/.maildb/michaelbarton/[Google Mail].Sent Mail' "Go to sent mail" -macro index,pager f '~/.maildb/michaelbarton/[Google Mail].Drafts' "Go to drafts" - -# Search -macro index S "unset wait_keymutt-notmuch-py ~/.maildb/jgi/temporary/search+temporary/search" "search mail (using notmuch)" - -# Sidebar -macro index,pager "" "Next sidebar folder" -macro index,pager "" "Previous sidebar folder" -macro index,pager "" "Open selected sidebar folder" - -# Show encryption menu after composing message -bind compose p pgp-menu diff --git a/mutt/mutt-notmuch-py b/mutt/mutt-notmuch-py deleted file mode 100755 index 637b9e2e..00000000 --- a/mutt/mutt-notmuch-py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python -""" -mutt-notmuch-py - -This is a Gmail-only version of the original mutt-notmuch script. - -It will interactively ask you for a search query and then symlink the matching -messages to $HOME/.cache/mutt_results. - -Add this to your muttrc. - -macro index / "unset wait_keymutt-notmuch-py~/.cache/mutt_results" \ - "search mail (using notmuch)" - -This script overrides the $HOME/.cache/mutt_results each time you run a query. - -Install this by adding this file somewhere on your PATH. - -Tested on OSX Lion and Arch Linux. - -(c) 2012 - Honza Pokorny -Licensed under BSD -""" - -import os -import hashlib - -from commands import getoutput -from mailbox import Maildir -from optparse import OptionParser -from collections import defaultdict - - -def digest(filename): - with open(filename) as f: - return hashlib.sha1(f.read()).hexdigest() - - -def pick_all_mail(messages): - for m in messages: - if "All Mail" in m: - return m - - -def empty_dir(directory): - box = Maildir(directory) - box.clear() - - -def command(cmd): - return getoutput(cmd) - - -def main(dest_box, is_gmail): - query = raw_input("Query: ") - - command("mkdir -p %s/cur" % dest_box) - command("mkdir -p %s/new" % dest_box) - - empty_dir(dest_box) - - files = command("notmuch search --output=files %s" % query).split("\n") - - data = defaultdict(list) - messages = [] - - for f in files: - if not f: - continue - - try: - sha = digest(f) - data[sha].append(f) - except IOError: - print("File %s does not exist" % f) - - for sha in data: - if is_gmail and len(data[sha]) > 1: - messages.append(pick_all_mail(data[sha])) - else: - messages.append(data[sha][0]) - - for m in messages: - if not m: - continue - - target = os.path.join(dest_box, "cur", os.path.basename(m)) - if not os.path.exists(target): - os.symlink(m, target) - - -if __name__ == "__main__": - p = OptionParser("usage: %prog [OPTIONS] [RESULTDIR]") - p.add_option( - "-g", - "--gmail", - dest="gmail", - action="store_true", - default=True, - help="gmail-specific behavior", - ) - p.add_option( - "-G", "--not-gmail", dest="gmail", action="store_false", help="gmail-specific behavior" - ) - (options, args) = p.parse_args() - - if args: - dest = args[0] - else: - dest = "~/.cache/mutt_results" - - # Use expanduser() so that os.symlink() won't get weirded out by tildes. - main(os.path.expanduser(dest).rstrip("/"), options.gmail) diff --git a/mutt/muttrc b/mutt/muttrc deleted file mode 100644 index e3f8d978..00000000 --- a/mutt/muttrc +++ /dev/null @@ -1,29 +0,0 @@ -# General options - -set realname = "Michael Barton" -set mbox_type = Maildir - -my_hdr X-URL: http://www.michaelbarton.me.uk - -set editor = "nvim" -set mail_check = 60 -set timeout = 60 -set header_cache=~/.mutt/cache/headers -set message_cachedir=~/.mutt/cache/bodies -set move = no -set include -set auto_tag = yes -alternative_order text/plain text/html * -auto_view text/html -bind editor ^T complete -set query_command="/usr/local/bin/lbdbq '%s'" -set pager_index_lines= 10 -set implicit_autoview = yes - -source "$HOME/.dotfiles/mutt/compose.muttrc" -source "$HOME/.dotfiles/mutt/appearance.muttrc" -source "$HOME/.dotfiles/mutt/headers.muttrc" -source "$HOME/.dotfiles/mutt/keymap.muttrc" -source "$HOME/.dotfiles/mutt/colors.muttrc" -source "$HOME/.dotfiles/mutt/encypt.muttrc" -source "$HOME/.dotfiles/mutt/accounts.muttrc" diff --git a/notmuch-config b/notmuch-config deleted file mode 100644 index e4150838..00000000 --- a/notmuch-config +++ /dev/null @@ -1,88 +0,0 @@ -# .notmuch-config - Configuration file for the notmuch mail system -# -# For more information about notmuch, see http://notmuchmail.org - -# Database configuration -# -# The only value supported here is 'path' which should be the top-level -# directory where your mail currently exists and to where mail will be -# delivered in the future. Files should be individual email messages. -# Notmuch will store its database within a sub-directory of the path -# configured here named ".notmuch". -# -[database] -path=/Users/mbarton/.maildb/jgi - -# User configuration -# -# Here is where you can let notmuch know how you would like to be -# addressed. Valid settings are -# -# name Your full name. -# primary_email Your primary email address. -# other_email A list (separated by ';') of other email addresses -# at which you receive email. -# -# Notmuch will use the various email addresses configured here when -# formatting replies. It will avoid including your own addresses in the -# recipient list of replies, and will set the From address based on the -# address to which the original email was addressed. -# -[user] -name=Michael Barton -primary_email=mbarton@lbl.gov -other_email=mail@michaelbarton.me.uk; - -# Configuration for "notmuch new" -# -# The following options are supported here: -# -# tags A list (separated by ';') of the tags that will be -# added to all messages incorporated by "notmuch new". -# -# ignore A list (separated by ';') of file and directory names -# that will not be searched for messages by "notmuch new". -# -# NOTE: *Every* file/directory that goes by one of those -# names will be ignored, independent of its depth/location -# in the mail store. -# -[new] -tags=unread;inbox; -ignore= - -# Search configuration -# -# The following option is supported here: -# -# exclude_tags -# A ;-separated list of tags that will be excluded from -# search results by default. Using an excluded tag in a -# query will override that exclusion. -# -[search] -exclude_tags=deleted;spam; - -# Maildir compatibility configuration -# -# The following option is supported here: -# -# synchronize_flags Valid values are true and false. -# -# If true, then the following maildir flags (in message filenames) -# will be synchronized with the corresponding notmuch tags: -# -# Flag Tag -# ---- ------- -# D draft -# F flagged -# P passed -# R replied -# S unread (added when 'S' flag is not present) -# -# The "notmuch new" command will notice flag changes in filenames -# and update tags, while the "notmuch tag" and "notmuch restore" -# commands will notice tag changes and update flags in filenames -# -[maildir] -synchronize_flags=true diff --git a/nvim/lua/config/dbt.lua b/nvim/lua/config/dbt.lua index 962891bd..dd226b57 100644 --- a/nvim/lua/config/dbt.lua +++ b/nvim/lua/config/dbt.lua @@ -82,7 +82,7 @@ vim.keymap.set("n", "dg", function() -- Try to find ref('model') or ref("model") around cursor local ref_model = nil - for start_pos, name, end_pos in line:gmatch("()ref%(['\"]([^'\"]+)['\"]%)()" ) do + for start_pos, name, end_pos in line:gmatch("()ref%(['\"]([^'\"]+)['\"]%)()") do if col >= start_pos and col <= end_pos then ref_model = name break @@ -92,7 +92,7 @@ vim.keymap.set("n", "dg", function() -- Try source('source_name', 'table_name') if no ref found local source_name, source_table = nil, nil if not ref_model then - for start_pos, src, tbl, end_pos in line:gmatch("()source%(['\"]([^'\"]+)['\"]%s*,%s*['\"]([^'\"]+)['\"]%)()" ) do + for start_pos, src, tbl, end_pos in line:gmatch("()source%(['\"]([^'\"]+)['\"]%s*,%s*['\"]([^'\"]+)['\"]%)()") do if col >= start_pos and col <= end_pos then source_name, source_table = src, tbl break @@ -157,17 +157,23 @@ vim.keymap.set("n", "df", function() map("i", "", function() local name = get_model_name() actions.close(prompt_bufnr) - if name then dbt_cmd_raw("uv run dbt run -s " .. name) end + if name then + dbt_cmd_raw("uv run dbt run -s " .. name) + end end) map("i", "", function() local name = get_model_name() actions.close(prompt_bufnr) - if name then dbt_cmd_raw("uv run dbt build -s " .. name) end + if name then + dbt_cmd_raw("uv run dbt build -s " .. name) + end end) map("i", "", function() local name = get_model_name() actions.close(prompt_bufnr) - if name then dbt_cmd_raw("uv run dbt test -s " .. name) end + if name then + dbt_cmd_raw("uv run dbt test -s " .. name) + end end) return true end, @@ -317,7 +323,13 @@ for line in raw.splitlines(): w.writerows(preview) break "]] - local cmd = "cd " .. root .. " && uv run dbt show -s " .. model .. " --limit 500 --output json --log-format json | " .. json_to_csv .. " | vd -f csv" + local cmd = "cd " + .. root + .. " && uv run dbt show -s " + .. model + .. " --limit 500 --output json --log-format json | " + .. json_to_csv + .. " | vd -f csv" require("toggleterm.terminal").Terminal :new({ cmd = cmd, @@ -402,7 +414,12 @@ vim.keymap.set("n", "dA", function() local script_path = vim.fn.stdpath("config") .. "/dbt/dbt_analyse.py" local shell_script = string.format( [[cd %s && uv run python3 %s --model %s --root %s --filepath %s --prompt %s || (echo "Press enter to close..." && read)]], - root, script_path, model, root, filepath, prompt_path + root, + script_path, + model, + root, + filepath, + prompt_path ) vim.fn.jobstart({ "tmux", "new-window", "-n", "dbt:" .. model, shell_script }, { detach = true }) vim.notify("Opened interactive cursor-agent session in tmux window 'dbt:" .. model .. "'", vim.log.levels.INFO) diff --git a/nvim/lua/config/providers.lua b/nvim/lua/config/providers.lua index d5f7de76..142581d9 100644 --- a/nvim/lua/config/providers.lua +++ b/nvim/lua/config/providers.lua @@ -17,4 +17,4 @@ vim.g.loaded_ruby_provider = 0 -- Python provider is useful for some plugins -- To enable, run: pip3 install neovim -- For now, we'll leave it enabled as it's commonly used --- vim.g.loaded_python3_provider = 0 \ No newline at end of file +-- vim.g.loaded_python3_provider = 0 diff --git a/nvim/lua/plugins/fyler.lua b/nvim/lua/plugins/fyler.lua index ea766cd1..a87c39f6 100644 --- a/nvim/lua/plugins/fyler.lua +++ b/nvim/lua/plugins/fyler.lua @@ -63,4 +63,3 @@ return { }, }, } - diff --git a/nvim/lua/plugins/language.lua b/nvim/lua/plugins/language.lua index b064ff34..e33bf08f 100644 --- a/nvim/lua/plugins/language.lua +++ b/nvim/lua/plugins/language.lua @@ -54,8 +54,9 @@ return { formatters = { mdformat = { prepend_args = { - "--wrap", "80", -- Wrap text at 80 characters - "--number", -- Use numbered lists consistently + "--wrap", + "80", -- Wrap text at 80 characters + "--number", -- Use numbered lists consistently }, }, prettier = { diff --git a/nvim/lua/plugins/toggleterm.lua b/nvim/lua/plugins/toggleterm.lua index 1c4e33fe..8d08363f 100644 --- a/nvim/lua/plugins/toggleterm.lua +++ b/nvim/lua/plugins/toggleterm.lua @@ -3,6 +3,8 @@ return { version = "*", config = function() local toggleterm = require("toggleterm") + local Terminal = require("toggleterm.terminal").Terminal + local terminal_module = require("toggleterm.terminal") toggleterm.setup({ -- Terminal window size @@ -30,58 +32,106 @@ return { shell = vim.o.shell, -- use your default shell }) - -- Set up key mappings for terminals - vim.keymap.set("n", "tt", "ToggleTerm", { desc = "[T]oggle [T]erminal" }) - vim.keymap.set("n", "tf", "ToggleTerm direction=float", { desc = "[T]oggle [F]loating terminal" }) - vim.keymap.set( - "n", - "tv", - "ToggleTerm direction=vertical", - { desc = "[T]oggle [V]ertical terminal" } - ) - vim.keymap.set( - "n", - "th", - "ToggleTerm direction=horizontal", - { desc = "[T]oggle [H]orizontal terminal" } - ) - - -- Send text to terminal - vim.keymap.set("n", "tl", function() - local term_id = vim.v.count > 0 and vim.v.count or 1 + local role_config = { + run = { count = 1, direction = "horizontal" }, + test = { count = 2, direction = "horizontal" }, + scratch = { count = 3, direction = "float" }, + } + + local role_order = { "run", "test", "scratch" } + local managed_terminals = {} + local last_role = "run" + + local function notify_role(action, role) + vim.notify(string.format("%s %s terminal", action, role), vim.log.levels.INFO, { title = "ToggleTerm" }) + end - -- Get terminal by ID and toggle it if closed - local term = require("toggleterm.terminal").get(term_id) - if term and not term:is_open() then - term:toggle() + local function ensure_terminal(role) + local cfg = role_config[role] + if not managed_terminals[role] then + managed_terminals[role] = Terminal:new({ + count = cfg.count, + direction = cfg.direction, + hidden = true, + close_on_exit = false, + }) end + return managed_terminals[role] + end - -- Send the current line - require("toggleterm").send_lines_to_terminal("single_line", true, { args = term_id }) - end, { desc = "[T]erminal send [L]ine" }) + local function open_role(role) + local term = ensure_terminal(role) + term:open() + last_role = role + notify_role("Focused", role) + return term + end + + local function toggle_last_role() + local term = ensure_terminal(last_role) + term:toggle() + notify_role("Toggled", last_role) + end + + local function close_current_terminal() + local current_id = vim.b.toggle_number + if current_id then + local term = terminal_module.get(current_id) + if term then + term:close() + vim.notify("Closed current terminal", vim.log.levels.INFO, { title = "ToggleTerm" }) + return + end + end - vim.keymap.set("v", "ts", function() - local term_id = vim.v.count > 0 and vim.v.count or 1 + local term = ensure_terminal(last_role) + if term:is_open() then + term:close() + notify_role("Closed", last_role) + else + vim.notify("No managed terminal is currently open", vim.log.levels.INFO, { title = "ToggleTerm" }) + end + end + + local function close_all_terminals() + local closed = 0 + for _, role in ipairs(role_order) do + local term = managed_terminals[role] + if term and term:is_open() then + term:close() + closed = closed + 1 + end + end - -- Get terminal by ID and toggle it if closed - local term = require("toggleterm.terminal").get(term_id) - if term and not term:is_open() then - term:toggle() + if closed == 0 then + vim.notify("No managed terminals were open", vim.log.levels.INFO, { title = "ToggleTerm" }) + else + vim.notify( + string.format("Closed %d managed terminal(s)", closed), + vim.log.levels.INFO, + { title = "ToggleTerm" } + ) end + end - -- Send the visual selection - require("toggleterm").send_lines_to_terminal("visual_selection", true, { args = term_id }) - end, { desc = "[T]erminal [S]end visual selection" }) + local function send_to_last_target(mode) + local term = open_role(last_role) + local send_mode = mode == "visual" and "visual_selection" or "single_line" + require("toggleterm").send_lines_to_terminal(send_mode, true, { args = term.id }) + if mode == "visual" then + notify_role("Sent selection to", last_role) + else + notify_role("Sent line to", last_role) + end + end - -- Run a just target via a picker - vim.keymap.set("n", "tj", function() + local function run_just_target() local output = vim.fn.systemlist("just --list --unsorted 2>/dev/null") if vim.v.shell_error ~= 0 or #output == 0 then vim.notify("No justfile found or no targets available", vim.log.levels.WARN) return end - -- Skip the header line ("Available recipes:") and parse target names local targets = {} for i, line in ipairs(output) do if i > 1 then @@ -93,37 +143,74 @@ return { end vim.ui.select(targets, { prompt = "just target> " }, function(choice) - if choice then - require("toggleterm.terminal").Terminal - :new({ cmd = "just " .. choice, close_on_exit = false, direction = "horizontal" }) - :toggle() + if not choice then + return + end + + local term = open_role("run") + if term.job_id then + vim.api.nvim_chan_send(term.job_id, "just " .. choice .. "\n") + vim.notify("Started just " .. choice .. " in run terminal", vim.log.levels.INFO, { title = "ToggleTerm" }) + else + vim.notify("Run terminal is not ready yet", vim.log.levels.WARN, { title = "ToggleTerm" }) end end) - end, { desc = "[T]oggle [J]ust target" }) - - -- Terminal navigation keymaps - function _G.set_terminal_keymaps() - local opts = { buffer = 0 } - vim.keymap.set("t", "", [[]], opts) - vim.keymap.set("t", "jk", [[]], opts) - vim.keymap.set("t", "", [[TmuxNavigateLeft]], opts) - vim.keymap.set("t", "", [[TmuxNavigateDown]], opts) - vim.keymap.set("t", "", [[TmuxNavigateUp]], opts) - vim.keymap.set("t", "", [[TmuxNavigateRight]], opts) - vim.keymap.set("t", "", [[]], opts) end - -- Auto-apply terminal keymaps when opening a terminal - vim.cmd("autocmd! TermOpen term://* lua set_terminal_keymaps()") + local term_keymaps_group = vim.api.nvim_create_augroup("ToggleTermLocalKeymaps", { clear = true }) + vim.api.nvim_create_autocmd("TermOpen", { + group = term_keymaps_group, + pattern = "term://*", + callback = function(event) + if vim.bo[event.buf].filetype ~= "toggleterm" then + return + end + + local opts = { buffer = event.buf } + vim.keymap.set("t", "", [[]], opts) + vim.keymap.set("t", "jk", [[]], opts) + -- Route directional moves through vim-tmux-navigator for consistent pane navigation. + vim.keymap.set("t", "", [[TmuxNavigateLeft]], opts) + vim.keymap.set("t", "", [[TmuxNavigateDown]], opts) + vim.keymap.set("t", "", [[TmuxNavigateUp]], opts) + vim.keymap.set("t", "", [[TmuxNavigateRight]], opts) + vim.keymap.set("t", "", [[]], opts) + end, + }) + + vim.keymap.set("n", "tr", function() + open_role("run") + end, { desc = "[T]erminal [R]un" }) + + vim.keymap.set("n", "te", function() + open_role("test") + end, { desc = "[T]erminal t[E]st" }) + + vim.keymap.set("n", "ts", function() + open_role("scratch") + end, { desc = "[T]erminal [S]cratch" }) + + vim.keymap.set("n", "ta", toggle_last_role, { desc = "[T]erminal toggle l[A]st" }) + vim.keymap.set("n", "tx", close_current_terminal, { desc = "[T]erminal close current" }) + vim.keymap.set("n", "tX", close_all_terminals, { desc = "[T]erminal close all" }) + vim.keymap.set("n", "tl", function() + send_to_last_target("line") + end, { desc = "[T]erminal send [L]ine" }) + vim.keymap.set("v", "tl", function() + send_to_last_target("visual") + end, { desc = "[T]erminal send [L]ines" }) + vim.keymap.set("n", "tj", run_just_target, { desc = "[T]erminal run [J]ust target" }) end, keys = { { "", desc = "Toggle terminal" }, - { "tt", desc = "Toggle terminal" }, - { "tf", desc = "Toggle floating terminal" }, - { "tv", desc = "Toggle vertical terminal" }, - { "th", desc = "Toggle horizontal terminal" }, - { "ts", desc = "Send visual selection to terminal", mode = "v" }, + { "tr", desc = "Open run terminal" }, + { "te", desc = "Open test terminal" }, + { "ts", desc = "Open scratch terminal" }, + { "ta", desc = "Toggle last terminal" }, + { "tx", desc = "Close current terminal" }, + { "tX", desc = "Close all terminals" }, { "tl", desc = "Send line to terminal" }, - { "tj", desc = "Run just target" }, + { "tl", desc = "Send selection to terminal", mode = "v" }, + { "tj", desc = "Run just target in terminal" }, }, } diff --git a/nvim/lua/plugins/vimwiki.lua b/nvim/lua/plugins/vimwiki.lua index adee2cd2..40f053f4 100644 --- a/nvim/lua/plugins/vimwiki.lua +++ b/nvim/lua/plugins/vimwiki.lua @@ -16,7 +16,7 @@ return { pattern = "vimwiki", group = group, callback = function() - pcall(vim.keymap.del, 'n', 'wn') + pcall(vim.keymap.del, "n", "wn") end, }) end, diff --git a/offlineimap/offlineimap.py b/offlineimap/offlineimap.py deleted file mode 100644 index d46fd1cb..00000000 --- a/offlineimap/offlineimap.py +++ /dev/null @@ -1,21 +0,0 @@ -import subprocess - - -def get_keychain_pass(account): - command = ["/usr/bin/security", "find-generic-password", "-w", "-a", account] - process = subprocess.Popen(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE) - process.wait() - if process.returncode != 0: - raise RuntimeError("Error running command: {}".format(process.stderr.read().decode())) - - password_block = process.stdout.read().decode().strip() - if password_block: - return password_block - raise RuntimeError( - "No password found with `{}`:\n {}".format(" ".join(command), password_block) - ) - - -def get_keyring_pass(key, value): - command = ["secret-tool", "lookup", key, value] - return subprocess.check_output(command).decode().strip() diff --git a/offlineimap/rc.linux b/offlineimap/rc.linux deleted file mode 100644 index 10a6646d..00000000 --- a/offlineimap/rc.linux +++ /dev/null @@ -1,25 +0,0 @@ -# Applies to all accounts -[general] -metadata = ~/.offlineimap -pythonfile = ~/.offlineimap/offlineimap.py -maxsyncaccounts = 2 -ui = basic -ssl = yes -accounts = Personal - -[Account Personal] -localrepository = personal-local -remoterepository = personal-remote -maxage = 30 - -[Repository personal-local] -type = Maildir -localfolders = ~/.maildb/ -restoreatime = no - -[Repository personal-remote] -type = Gmail -remoteuser = mail@michaelbarton.me.uk -remotepasseval = get_keyring_pass("password", "gmail") -realdelete = no -sslcacertfile = /etc/ssl/certs/ca-certificates.crt diff --git a/offlineimap/rc.osx b/offlineimap/rc.osx deleted file mode 100644 index af01c943..00000000 --- a/offlineimap/rc.osx +++ /dev/null @@ -1,26 +0,0 @@ -# Applies to all accounts -[general] -metadata = ~/.offlineimap -pythonfile = ~/.offlineimap.py -maxsyncaccounts = 2 -ui = basic -ssl = yes -accounts = Personal - -[Account Personal] -localrepository = personal-local -remoterepository = personal-remote -maxage = 30 - -[Repository personal-local] -type = Maildir -localfolders = ~/.maildb/michaelbarton -restoreatime = no - -[Repository personal-remote] -type = Gmail -remoteuser = mail@michaelbarton.me.uk -remotepasseval = get_keychain_pass(account="acct.gmail") -realdelete = no -# This is installed by `brew install openssl` and may change with versions -sslcacertfile = /usr/local/etc/openssl@3/cert.pem diff --git a/offlineimap/restart_offlineimap b/offlineimap/restart_offlineimap deleted file mode 100755 index 4d31edd6..00000000 --- a/offlineimap/restart_offlineimap +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -launchctl stop uk.me.michaelbarton.offlineimap -launchctl start uk.me.michaelbarton.offlineimap diff --git a/offlineimap/uk.me.michaelbarton.offlineimap.plist b/offlineimap/uk.me.michaelbarton.offlineimap.plist deleted file mode 100644 index 6218aa04..00000000 --- a/offlineimap/uk.me.michaelbarton.offlineimap.plist +++ /dev/null @@ -1,23 +0,0 @@ - - - - - Debug - - KeepAlive - - Label - uk.me.michaelbarton.offlineimap - ProgramArguments - - /usr/local/bin/offlineimap - -q - -u - Noninteractive.Quiet - - RunAtLoad - - StartInterval - 240 - - diff --git a/ripgrep/rc b/ripgrep/rc index 40117e54..473c6778 100644 --- a/ripgrep/rc +++ b/ripgrep/rc @@ -1,6 +1,6 @@ # ripgrep config file # Set RIPGREP_CONFIG_PATH=$HOME/.config/ripgrep/rc to activate -# (set in fish/config.fish) +# (set in shell configs in this repo) # Smart case: case-insensitive unless pattern has uppercase --smart-case diff --git a/systemd/user/offlineimap.service b/systemd/user/offlineimap.service deleted file mode 100644 index 2ca4ce17..00000000 --- a/systemd/user/offlineimap.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Sync email to computuer - -[Service] -# execute the action and stop -Type=oneshot -ExecStart=/usr/bin/offlineimap - -# give 120 seconds for offlineimap to gracefully stop before hard killing it -TimeoutStopSec=120 - -[Install] -WantedBy=mail.target diff --git a/systemd/user/offlineimap.timer b/systemd/user/offlineimap.timer deleted file mode 100644 index 36310d07..00000000 --- a/systemd/user/offlineimap.timer +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Offlineimap Query Timer -Requires=offlineimap.service - -[Timer] -# start the service 2 minutes after a boot-up -OnBootSec=2m - -# the timer will run the service every 5 minutes -OnUnitInactiveSec=5m - -[Install] -WantedBy=default.target diff --git a/tmux/cheatsheet.md b/tmux/cheatsheet.md index 21ab00bc..eae71ca9 100644 --- a/tmux/cheatsheet.md +++ b/tmux/cheatsheet.md @@ -48,6 +48,28 @@ are unbound to avoid conflicts. | `y` | Yank selection | | `q` | Exit copy mode | +## Neovim ToggleTerm (`t`) + +Fixed terminal roles make it easier to keep context: + +| Keys | Action | +| ------------ | -------------------------------------------------------- | +| `tr` | Focus run terminal (slot 1) | +| `te` | Focus test terminal (slot 2) | +| `ts` | Focus scratch terminal (slot 3) | +| `ta` | Toggle last-focused terminal | +| `tx` | Close current terminal | +| `tX` | Close all managed terminals | +| `tl` | Send line (or visual selection) to last-focused terminal | +| `tj` | Pick and run a `just` target in the run terminal | + +Inside ToggleTerm buffers: + +| Keys | Action | +| -------------- | ------------------------------------- | +| `` / `jk` | Leave terminal insert mode | +| `Ctrl-h/j/k/l` | Navigate panes via vim-tmux-navigator | + ## Ghostty | Keys | Action | diff --git a/urlview b/urlview deleted file mode 100644 index 2445afd0..00000000 --- a/urlview +++ /dev/null @@ -1 +0,0 @@ -COMMAND open %s diff --git a/yml/format_yml.py b/yml/format_yml.py deleted file mode 100755 index a1e4b49c..00000000 --- a/yml/format_yml.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import yaml - -with open(sys.argv[1]) as fh_in: - contents = yaml.safe_load(fh_in) - -with open(sys.argv[1], "w") as fh_out: - fh_out.write(yaml.dump(contents)) From 74a8f66bed871e881152aa94c6793de7ed4e186b Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Tue, 14 Jul 2026 15:41:16 -0700 Subject: [PATCH 2/5] feat(planning): reframe premortem for analysis work; add Context/Verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tune the plan-quality rule to the work actually done (~80% one-off analysis and reporting, not maintained software), based on a survey of 54 saved plans and 216 session transcripts. - Premortem now has two framings by work type: analysis/report work uses "this result was shared and the conclusion turned out to be wrong because…" with analysis-specific failure classes (fan-out, wrong denominator, population mismatch, stale data, misread columns); the "3 months later" framing is kept only for maintained pipelines/tools/config. - Promote Context to a required first section (was in 87% of plans already). - Add Verification as its own section, distinct from the done-checklist (was in 72% of plans already). - Sync plan-review.sh gate list to match (9 gates). - Add cursor/rules/CHANGELOG.md recording the reasoning and rejected alternatives (mode selector, relaxing the 60s rule, reviving Outside view). Co-Authored-By: Claude Opus 4.8 --- claude/hooks/plan-review.sh | 16 ++++---- cursor/rules/CHANGELOG.md | 82 +++++++++++++++++++++++++++++++++++++ cursor/rules/planning.mdc | 6 ++- 3 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 cursor/rules/CHANGELOG.md diff --git a/claude/hooks/plan-review.sh b/claude/hooks/plan-review.sh index 85bab5a4..d778481b 100755 --- a/claude/hooks/plan-review.sh +++ b/claude/hooks/plan-review.sh @@ -39,13 +39,15 @@ prompt=$( You just edited a plan document. Before any implementation, audit it against the planning rule in below. The gates are: -1. Success & exit criteria -2. Invariants -3. What is the grain of the data -4. Failure modes (including the one-line premortem) -5. Assumptions & unknowns -6. Minimal viable change -7. Visualization confirmation (only when the plan involves figures) +1. Context +2. Success & exit criteria +3. Invariants +4. What is the grain of the data +5. Failure modes (including the one-line premortem) +6. Assumptions & unknowns +7. Verification +8. Minimal viable change +9. Visualization confirmation (only when the plan involves figures) A gate passes ONLY if you can quote the plan line(s) that satisfy it. Paraphrases, "implied somewhere," or "this is obvious" do NOT count. diff --git a/cursor/rules/CHANGELOG.md b/cursor/rules/CHANGELOG.md new file mode 100644 index 00000000..d1db3235 --- /dev/null +++ b/cursor/rules/CHANGELOG.md @@ -0,0 +1,82 @@ +# Changelog — agent planning/dbt rules + +Rationale log for `cursor/rules/planning.mdc` and `cursor/rules/dbt.mdc`. + +Git records *what* changed line-by-line; this file records *why*, and the +alternatives considered and rejected — so a later edit doesn't silently +re-litigate a settled decision. Newest first. Each entry: What / Why / +Considered & rejected. + +--- + +## 2026-07-14 — Reframe premortem for analysis work; promote Context & Verification + +**What changed** +- Split the Premortem into two framings chosen by what the work produces: + analysis/report → "this result was shared and the conclusion turned out to be + wrong because …" (with analysis-specific failure classes); maintained + pipeline/tool/config → the original "it is 3 months later …". +- Promoted **Context** to a required first section. +- Added **Verification** as its own section (reproduce commands, distinct from + the done-checklist). +- Synced `claude/hooks/plan-review.sh` gate list to match (now 9 gates). + +**Why** +- Evidence: survey of all 54 saved plans in `~/.claude/plans/` (2026-06-15 → + 2026-07-14) plus 216 Claude Code session transcripts. ~80% of the operator's + work is one-off analysis, data investigation, and report authoring — not + maintained software — so the "3 months later" premortem framing mismatched the + common case. The failure that actually bites is a wrong conclusion escaping + into a report/Slack/validation doc. +- Context appeared in 87% of plans and Verification in 72% despite neither being + mandated — codifying what plans already do, not adding ceremony. +- The template otherwise showed no friction: grain/fan-out gates catch real bugs + and the viz/failure gates self-suppress via honest `N/A`. + +**Considered & rejected** +- *Work-type "mode selector" at the top of every plan* — rejected as + over-templating; the existing `N/A` self-suppression already degrades + gracefully across work types, with no observed friction. +- *Relaxing the 60-second-review rule* — rejected. Plans do run long (median + ~200 lines) but transcripts show zero complaint, and loosening the rule would + only license more length. +- *Reintroducing "Outside view"* — rejected; it reappeared organically in ~24% + of plans but was deliberately cut in #98, so not re-adding without an explicit + ask. + +--- + +## 2026-06-28 — Replace "Outside view" with Success & grain gates (#98) + +*(reconstructed from git history)* + +**What changed** +- Renamed `Exit criteria` → `Success & exit criteria` (added a one-sentence + end-state before the done checklist). +- Promoted grain from a sub-bullet inside Invariants to its own standalone gate, + "What is the grain of the data". +- Dropped the "Outside view" gate (reference-class effort/risk estimation with a + buffer multiplier). + +**Why** +- Grain was getting buried inside Invariants where it was easy to skip; as the + spec the operator reviews, it needed to stand on its own. +- Outside-view effort estimation wasn't earning its keep in analytical/dbt work. + +--- + +## 2026-06-12 — Introduce rule + hook system (#94) + +*(reconstructed from git history)* + +**What changed** +- Introduced `planning.mdc` and the deployment/hook system (Cursor rules symlink + + Claude Code PostToolUse/Stop hooks). +- Made `planning.mdc` the single source of truth: the hook substitutes its body + rather than duplicating the gates in a separate template; dropped an initial + `claude/prompts/plan-review.md` (Claude Code has no `prompts/` convention). + +**Why / Considered & rejected** +- Dropped production-engineering gates as ill-suited to analytical/dbt work: + **Reversibility**, **System 2 triggers**, and **three-tier boundaries for + actions** (with a shop-specific "Never do" list). diff --git a/cursor/rules/planning.mdc b/cursor/rules/planning.mdc index 29cfdfe6..f57f0ff6 100644 --- a/cursor/rules/planning.mdc +++ b/cursor/rules/planning.mdc @@ -7,6 +7,7 @@ alwaysApply: true Every plan MUST include these sections (bullets, reviewable in under 60 seconds): +- **Context**: One or two sentences — why this work, what prompted it, the root cause or question being answered. This is the first thing the operator reads. - **Success & exit criteria**: One sentence on the desired end state (user-visible or system-observable outcome), followed by the done checklist: - Build succeeds (e.g. `just build`, `dbt build`) - Rendered output is correct if applicable (e.g. `quarto render`) @@ -24,8 +25,11 @@ Every plan MUST include these sections (bullets, reviewable in under 60 seconds) - "join fan-out from a 1:N relationship" - "grain change in intermediate breaks downstream" - "database lock from stale process" - - **Premortem** (one line): "It is 3 months later and this failed because …" (top 2–3 concrete reasons) + - **Premortem** (one line), framed by what the work produces: + - *Analysis, investigation, or a data-backed report* (the common case): "This result was shared and the conclusion turned out to be wrong because …" (top 2–3 concrete reasons). Look first at the failure classes analysis actually produces: join fan-out / double counting, wrong denominator, the analyzed population not matching the stated universe, stale or partial input data, misread column semantics, a plausible number nobody spot-checked against a known case. + - *A pipeline, model, tool, or config that will be maintained*: "It is 3 months later and this failed because …" (top 2–3 concrete reasons) - **Assumptions & unknowns**: What we treat as true but have not verified; what this plan does *not* cover; what would invalidate the plan ("if X is wrong, we stop") +- **Verification**: The exact commands to reproduce/observe the result end-to-end (e.g. the `duckdb` / `grep` / `quarto render` invocation and what output confirms success). Distinct from the done-checklist: this is the runbook, not the acceptance test. ### Minimal viable change From da3eb9eb7001c19c3a2201cf66156e4d1a7a2d1b Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Tue, 14 Jul 2026 15:53:41 -0700 Subject: [PATCH 3/5] refactor(planning): derive plan-review gate list from the rule (single source) The audit gate list was duplicated in planning.mdc's section structure and in a hardcoded numbered list inside plan-review.sh; both had to be edited together by hand, with nothing catching a mismatch. Make the rule the sole source: - planning.mdc carries the canonical gate list as a HTML comment (invisible in Cursor's rendered view). - plan-review.sh extracts that manifest at runtime instead of hardcoding it, strips the comment from the injected rule body, and fails loud (exit 1) if the manifest is missing. Drift is now structurally impossible. Chosen over a CI drift-check, which would need to normalize the two deliberately-different naming schemes and add test infra the repo lacks. Also fixes CHANGELOG.md mdformat compliance (was failing `make fmt_check`) and logs both this change and the fix in cursor/rules/CHANGELOG.md. Co-Authored-By: Claude Opus 4.8 --- claude/hooks/plan-review.sh | 31 ++++++++++++++++--------- cursor/rules/CHANGELOG.md | 45 ++++++++++++++++++++++++++++++++----- cursor/rules/planning.mdc | 12 ++++++++++ 3 files changed, 72 insertions(+), 16 deletions(-) diff --git a/claude/hooks/plan-review.sh b/claude/hooks/plan-review.sh index d778481b..0e88bd4b 100755 --- a/claude/hooks/plan-review.sh +++ b/claude/hooks/plan-review.sh @@ -31,23 +31,32 @@ if [ ! -f "$planning_rule" ]; then exit 1 fi -# Strip the YAML frontmatter (between the first two `---` lines). -rule_body=$(awk 'BEGIN{f=0} /^---$/{f++; next} f>=2{print}' "$planning_rule") +# The gate list is the single source of truth: it lives in the rule as a +# `` comment (invisible in Cursor's rendered view) and is +# extracted here rather than duplicated, so the two can never drift. +gates=$(awk '/^$/{f=0} f' "$planning_rule") +if [ -z "$gates" ]; then + echo "plan-review hook: no gate manifest () in $planning_rule" >&2 + exit 1 +fi + +# Strip the YAML frontmatter (between the first two `---` lines) and the gate +# manifest comment, leaving the human-readable rule to inject verbatim. +rule_body=$(awk ' + BEGIN{f=0} + /^---$/{f++; next} + f<2{next} + /^$/{if(s){s=0; next}} + !s{print} +' "$planning_rule") prompt=$( cat < below. The gates are: -1. Context -2. Success & exit criteria -3. Invariants -4. What is the grain of the data -5. Failure modes (including the one-line premortem) -6. Assumptions & unknowns -7. Verification -8. Minimal viable change -9. Visualization confirmation (only when the plan involves figures) +$gates A gate passes ONLY if you can quote the plan line(s) that satisfy it. Paraphrases, "implied somewhere," or "this is obvious" do NOT count. diff --git a/cursor/rules/CHANGELOG.md b/cursor/rules/CHANGELOG.md index d1db3235..301447d8 100644 --- a/cursor/rules/CHANGELOG.md +++ b/cursor/rules/CHANGELOG.md @@ -7,11 +7,40 @@ alternatives considered and rejected — so a later edit doesn't silently re-litigate a settled decision. Newest first. Each entry: What / Why / Considered & rejected. ---- +______________________________________________________________________ + +## 2026-07-14 — Make the plan-review gate list single-source + +**What changed** + +- The audit gate list now lives only in `planning.mdc`, as a + `` HTML comment (invisible in Cursor's rendered view). +- `claude/hooks/plan-review.sh` extracts that manifest at runtime and injects it + in place of its former hardcoded numbered list; it also strips the comment + from the rule body it injects, and fails loud (`exit 1`) if the manifest is + missing. + +**Why** + +- The gate list was duplicated in two files that had to be edited together by + hand; the premortem-reframing change above required exactly that dual edit. + Nothing caught a mismatch, so a future rule edit could leave the hook auditing + against a stale rubric. Deriving the list from the rule makes drift + structurally impossible. + +**Considered & rejected** + +- *A CI/`make check` that detects divergence* — rejected: it would need to + normalize the two deliberately-different naming schemes, and the repo has no + pre-commit or shell-test harness. Eliminating the second copy is less + infrastructure and strictly more reliable than detecting drift after the fact. + +______________________________________________________________________ ## 2026-07-14 — Reframe premortem for analysis work; promote Context & Verification **What changed** + - Split the Premortem into two framings chosen by what the work produces: analysis/report → "this result was shared and the conclusion turned out to be wrong because …" (with analysis-specific failure classes); maintained @@ -22,6 +51,7 @@ Considered & rejected. - Synced `claude/hooks/plan-review.sh` gate list to match (now 9 gates). **Why** + - Evidence: survey of all 54 saved plans in `~/.claude/plans/` (2026-06-15 → 2026-07-14) plus 216 Claude Code session transcripts. ~80% of the operator's work is one-off analysis, data investigation, and report authoring — not @@ -34,6 +64,7 @@ Considered & rejected. and the viz/failure gates self-suppress via honest `N/A`. **Considered & rejected** + - *Work-type "mode selector" at the top of every plan* — rejected as over-templating; the existing `N/A` self-suppression already degrades gracefully across work types, with no observed friction. @@ -44,13 +75,14 @@ Considered & rejected. of plans but was deliberately cut in #98, so not re-adding without an explicit ask. ---- +______________________________________________________________________ ## 2026-06-28 — Replace "Outside view" with Success & grain gates (#98) *(reconstructed from git history)* **What changed** + - Renamed `Exit criteria` → `Success & exit criteria` (added a one-sentence end-state before the done checklist). - Promoted grain from a sub-bullet inside Invariants to its own standalone gate, @@ -59,24 +91,27 @@ Considered & rejected. buffer multiplier). **Why** + - Grain was getting buried inside Invariants where it was easy to skip; as the spec the operator reviews, it needed to stand on its own. - Outside-view effort estimation wasn't earning its keep in analytical/dbt work. ---- +______________________________________________________________________ ## 2026-06-12 — Introduce rule + hook system (#94) *(reconstructed from git history)* **What changed** -- Introduced `planning.mdc` and the deployment/hook system (Cursor rules symlink - + Claude Code PostToolUse/Stop hooks). + +- Introduced `planning.mdc` and the deployment/hook system: Cursor rules symlink + and Claude Code PostToolUse/Stop hooks. - Made `planning.mdc` the single source of truth: the hook substitutes its body rather than duplicating the gates in a separate template; dropped an initial `claude/prompts/plan-review.md` (Claude Code has no `prompts/` convention). **Why / Considered & rejected** + - Dropped production-engineering gates as ill-suited to analytical/dbt work: **Reversibility**, **System 2 triggers**, and **three-tier boundaries for actions** (with a shop-specific "Never do" list). diff --git a/cursor/rules/planning.mdc b/cursor/rules/planning.mdc index f57f0ff6..28fddbe9 100644 --- a/cursor/rules/planning.mdc +++ b/cursor/rules/planning.mdc @@ -3,6 +3,18 @@ description: Planning and task execution rules alwaysApply: true --- + + ## Planning Every plan MUST include these sections (bullets, reviewable in under 60 seconds): From 487462f37a5327600d8ca89709fc6759e9fed522 Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Fri, 17 Jul 2026 09:57:51 -0700 Subject: [PATCH 4/5] feat(dotfiles): modernise Python scaffold to uv, fix SQL exit-127 fall-through Command-history review found the cookiecutter template still generating Poetry+black+isort+jfrog against a uv/ruff/duckdb workflow, and ~100+ exit-127s from vim scratch-SQL falling through to fish when duckdb wasn't the live terminal process. - python cookiecutter template: Poetry -> PEP 621 + uv, CodeArtifact index instead of jfrog, ruff format/check instead of black/isort, fix hardcoded package name, fix setuptools package discovery, add a passing placeholder test so the scaffold's own fmt/test/run chain succeeds out of the box. - Bring the existing ~/.config/uv/uv.toml under dotfiles management. - nvim: dedicated `sql` toggleterm role (`tq`) that always launches duckdb, so send-to-terminal SQL can no longer fall through to a shell. - fish: dq/dv helpers for the tmp/scratch.sql duckdb loop; harden the coreutils guard to check each g* binary individually. - Add ~/.duckdbrc and ~/.visidatarc (deliberately omit `.timer on` from duckdbrc: it writes to stdout and corrupts the dv->visidata CSV pipe). --- ansible/dotfiles.yml | 1 + ansible/tasks/link_files.yml | 3 ++ duckdb/duckdbrc | 3 ++ fish/config.fish | 44 ++++++++++------ nvim/lua/plugins/toggleterm.lua | 9 +++- .../Justfile | 13 +++-- .../pyproject.toml | 52 +++++++++++-------- .../tests/test_main.py | 2 + uv/uv.toml | 1 + visidata/visidatarc | 2 + 10 files changed, 84 insertions(+), 46 deletions(-) create mode 100644 duckdb/duckdbrc create mode 100644 uv/uv.toml create mode 100644 visidata/visidatarc diff --git a/ansible/dotfiles.yml b/ansible/dotfiles.yml index db78580f..43296e50 100644 --- a/ansible/dotfiles.yml +++ b/ansible/dotfiles.yml @@ -33,6 +33,7 @@ - { path: "{{ config_dir }}/atuin" } - { path: "{{ config_dir }}/lazygit" } - { path: "{{ config_dir }}/ripgrep" } + - { path: "{{ config_dir }}/uv" } - { path: "{{ ansible_user_dir }}/cache" } - { path: "{{ ansible_user_dir }}/.bin" } - { path: "{{ ansible_user_dir }}/.hammerspoon" } diff --git a/ansible/tasks/link_files.yml b/ansible/tasks/link_files.yml index 0a948d74..0065206d 100644 --- a/ansible/tasks/link_files.yml +++ b/ansible/tasks/link_files.yml @@ -24,6 +24,7 @@ - { src: "atuin/config.toml", dest: ".config/atuin/config.toml" } - { src: "lazygit/config.yml", dest: ".config/lazygit/config.yml" } - { src: "starship/starship.toml", dest: ".config/starship.toml" } + - { src: "uv/uv.toml", dest: ".config/uv/uv.toml" } # Scripts for working with data - { src: "parquet/csv_to_parquet.py", dest: ".bin/csv_to_parquet" } @@ -33,6 +34,8 @@ - { src: "zettel/today.sh", dest: ".bin/today" } - { src: "git/gitconfig", dest: ".gitconfig" } - { src: "gitignore", dest: ".gitignore" } + - { src: "duckdb/duckdbrc", dest: ".duckdbrc" } + - { src: "visidata/visidatarc", dest: ".visidatarc" } - { src: "markdown/md2docx.sh", dest: ".bin/md2docx" } - { src: "poetry/update_poetry_dependencies.py", diff --git a/duckdb/duckdbrc b/duckdb/duckdbrc new file mode 100644 index 00000000..e9cba6b8 --- /dev/null +++ b/duckdb/duckdbrc @@ -0,0 +1,3 @@ +.nullvalue ∅ +.maxwidth 0 +.mode duckbox diff --git a/fish/config.fish b/fish/config.fish index 45da2b83..a2e4ad34 100644 --- a/fish/config.fish +++ b/fish/config.fish @@ -156,22 +156,16 @@ set -x LANG 'en_GB.UTF-8' # ################################################################### -# Only create aliases if coreutils is installed -# Check for coreutils by looking for one of its binaries instead of running brew list -if test -x /opt/homebrew/bin/gcat - # Use hardcoded path for M1/M2 Macs (adjust if on Intel Mac) - set brew_prefix /opt/homebrew - - # Only alias the most commonly used commands to reduce startup time - alias cp="$brew_prefix/bin/gcp" - alias date="$brew_prefix/bin/gdate" - alias echo="$brew_prefix/bin/gecho" - alias mv="$brew_prefix/bin/gmv" - alias rm="$brew_prefix/bin/grm" - alias sed="$brew_prefix/bin/gsed" - alias sort="$brew_prefix/bin/gsort" - alias tail="$brew_prefix/bin/gtail" - +# Only alias the most commonly used commands to reduce startup time. +# Check each g* binary individually (rather than gating on gcat alone) so a +# partial coreutils install doesn't silently skip every alias below it. +# Use hardcoded path for M1/M2 Macs (adjust if on Intel Mac) +set brew_prefix /opt/homebrew +for pair in cp:gcp date:gdate echo:gecho mv:gmv rm:grm sed:gsed sort:gsort tail:gtail + set -l parts (string split ":" $pair) + if test -x "$brew_prefix/bin/$parts[2]" + alias $parts[1]="$brew_prefix/bin/$parts[2]" + end end ################################################################### @@ -276,6 +270,24 @@ function qwatch kill $quarto_pid 2>/dev/null end +# Run tmp/scratch.sql against duckdb (default: in-memory, or pass a db path) +function dq + if not test -f tmp/scratch.sql + echo "dq: tmp/scratch.sql not found" + return 1 + end + duckdb $argv[1] < tmp/scratch.sql +end + +# Same as dq, but pipe CSV output into visidata +function dv + if not test -f tmp/scratch.sql + echo "dv: tmp/scratch.sql not found" + return 1 + end + duckdb -csv $argv[1] < tmp/scratch.sql | vd -f csv - +end + # LESS colors for man pages set -gx LESS_TERMCAP_us \e\[1\;32m set -gx LESS_TERMCAP_md \e\[1\;31m diff --git a/nvim/lua/plugins/toggleterm.lua b/nvim/lua/plugins/toggleterm.lua index 8d08363f..e1cfc46c 100644 --- a/nvim/lua/plugins/toggleterm.lua +++ b/nvim/lua/plugins/toggleterm.lua @@ -36,9 +36,10 @@ return { run = { count = 1, direction = "horizontal" }, test = { count = 2, direction = "horizontal" }, scratch = { count = 3, direction = "float" }, + sql = { count = 4, direction = "horizontal", cmd = "duckdb" }, } - local role_order = { "run", "test", "scratch" } + local role_order = { "run", "test", "scratch", "sql" } local managed_terminals = {} local last_role = "run" @@ -52,6 +53,7 @@ return { managed_terminals[role] = Terminal:new({ count = cfg.count, direction = cfg.direction, + cmd = cfg.cmd, hidden = true, close_on_exit = false, }) @@ -190,6 +192,10 @@ return { open_role("scratch") end, { desc = "[T]erminal [S]cratch" }) + vim.keymap.set("n", "tq", function() + open_role("sql") + end, { desc = "[T]erminal [Q]uery (duckdb)" }) + vim.keymap.set("n", "ta", toggle_last_role, { desc = "[T]erminal toggle l[A]st" }) vim.keymap.set("n", "tx", close_current_terminal, { desc = "[T]erminal close current" }) vim.keymap.set("n", "tX", close_all_terminals, { desc = "[T]erminal close all" }) @@ -206,6 +212,7 @@ return { { "tr", desc = "Open run terminal" }, { "te", desc = "Open test terminal" }, { "ts", desc = "Open scratch terminal" }, + { "tq", desc = "Open duckdb query terminal" }, { "ta", desc = "Toggle last terminal" }, { "tx", desc = "Close current terminal" }, { "tX", desc = "Close all terminals" }, diff --git a/python/cookiecutter/scratch-python-template/{{cookiecutter.timestamp}}__{{cookiecutter.project_name}}/Justfile b/python/cookiecutter/scratch-python-template/{{cookiecutter.timestamp}}__{{cookiecutter.project_name}}/Justfile index feb83df0..8f22c3e3 100644 --- a/python/cookiecutter/scratch-python-template/{{cookiecutter.timestamp}}__{{cookiecutter.project_name}}/Justfile +++ b/python/cookiecutter/scratch-python-template/{{cookiecutter.timestamp}}__{{cookiecutter.project_name}}/Justfile @@ -1,17 +1,16 @@ # Launch Jupyter Notebook notebook: - poetry run jupyter notebook notebooks/ + uv run jupyter notebook notebooks/ -# Format the code using black +# Format and lint the code using ruff fmt: - poetry run isort . - poetry run black . + uv run ruff format . + uv run ruff check --fix . # Run the tests using pytest test: - poetry run pytest tests/ + uv run pytest tests/ # Run the main script run: - poetry run python {{ cookiecutter.package_name }}/main.py - + uv run python {{ cookiecutter.package_name }}/main.py diff --git a/python/cookiecutter/scratch-python-template/{{cookiecutter.timestamp}}__{{cookiecutter.project_name}}/pyproject.toml b/python/cookiecutter/scratch-python-template/{{cookiecutter.timestamp}}__{{cookiecutter.project_name}}/pyproject.toml index 79f82d8c..6e761062 100644 --- a/python/cookiecutter/scratch-python-template/{{cookiecutter.timestamp}}__{{cookiecutter.project_name}}/pyproject.toml +++ b/python/cookiecutter/scratch-python-template/{{cookiecutter.timestamp}}__{{cookiecutter.project_name}}/pyproject.toml @@ -1,25 +1,26 @@ -[tool.poetry] -name = "karius_taxonomy_mapping" +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "{{ cookiecutter.package_name }}" version = "0.1.0" description = "" -authors = ["Your Name "] - -[tool.poetry.dependencies] -python = "^3.10" -jupyter = "^1.0.0" +requires-python = ">=3.10" +dependencies = [ + "jupyter>=1.0.0", +] -[tool.poetry.dev-dependencies] -bump2version = "^1.0.1" -edx-lint = "^5.3.6" -mypy = "^1.8.0" -pandas-stubs = "^2.2.0.240218" -pydocstyle = "^6.3.0" -pytest = "^9.0.3" -ruff = "^0.8.0" +[dependency-groups] +dev = [ + "mypy>=1.8.0", + "pandas-stubs>=2.2.0.240218", + "pytest>=9.0.3", + "ruff>=0.8.0", +] -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.setuptools.packages.find] +include = ["{{ cookiecutter.package_name }}*"] [tool.ruff] line-length = 100 @@ -37,8 +38,15 @@ select = [ "RUF", # ruff-specific rules ] -[[tool.poetry.source]] -name = 'jfrog' -url = 'https://karius.jfrog.io/artifactory/api/pypi/karius-pypi-virtual/simple/' -secondary = true +# Private packages: CodeArtifact index `karius`. `aws@` in the URL is required +# for CodeArtifact basic auth + keyrings.codeartifact. User-level: +# `keyring-provider = "subprocess"` in ~/.config/uv/uv.toml, `uv tool install +# keyring --with keyrings.codeartifact`. See: +# https://docs.astral.sh/uv/guides/integration/aws/ +[[tool.uv.index]] +name = "karius" +url = "https://aws@karius-573640641260.d.codeartifact.us-east-1.amazonaws.com/pypi/karius-pypi/simple/" +explicit = true +# [tool.uv.sources] +# kli = { index = "karius" } diff --git a/python/cookiecutter/scratch-python-template/{{cookiecutter.timestamp}}__{{cookiecutter.project_name}}/tests/test_main.py b/python/cookiecutter/scratch-python-template/{{cookiecutter.timestamp}}__{{cookiecutter.project_name}}/tests/test_main.py index e69de29b..3ada1ee4 100644 --- a/python/cookiecutter/scratch-python-template/{{cookiecutter.timestamp}}__{{cookiecutter.project_name}}/tests/test_main.py +++ b/python/cookiecutter/scratch-python-template/{{cookiecutter.timestamp}}__{{cookiecutter.project_name}}/tests/test_main.py @@ -0,0 +1,2 @@ +def test_placeholder(): + assert True diff --git a/uv/uv.toml b/uv/uv.toml new file mode 100644 index 00000000..cab1b162 --- /dev/null +++ b/uv/uv.toml @@ -0,0 +1 @@ +keyring-provider = "subprocess" diff --git a/visidata/visidatarc b/visidata/visidatarc new file mode 100644 index 00000000..959676e6 --- /dev/null +++ b/visidata/visidatarc @@ -0,0 +1,2 @@ +options.motd_url = '' +options.disp_date_fmt = '%Y-%m-%d' From 6071b67935f1df748e08da3927213d6f8a205109 Mon Sep 17 00:00:00 2001 From: Michael Barton Date: Mon, 20 Jul 2026 10:14:31 -0700 Subject: [PATCH 5/5] chore(dotfiles): fix ghostty config errors, pin claude defaults, ignore local claude settings Ghostty 1.3 renamed auto-install-terminfo into shell-integration-features (ssh-terminfo flag) and changed notify-on-command-finish from a boolean to an enum, so the old config values were failing ghostty +validate-config on startup. --- claude/settings.json | 2 ++ ghostty/config | 6 +++--- gitignore | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/claude/settings.json b/claude/settings.json index bf317b4c..c9f8ecdf 100644 --- a/claude/settings.json +++ b/claude/settings.json @@ -7,6 +7,7 @@ ], "defaultMode": "auto" }, + "model": "sonnet", "hooks": { "PostToolUse": [ { @@ -47,6 +48,7 @@ } ] }, + "effortLevel": "high", "tui": "fullscreen", "theme": "auto", "skipAutoPermissionPrompt": true diff --git a/ghostty/config b/ghostty/config index 867f9d12..df251475 100644 --- a/ghostty/config +++ b/ghostty/config @@ -6,10 +6,10 @@ theme = Catppuccin Frappe command = /opt/homebrew/bin/fish # Auto-copy Ghostty terminfo to SSH remote hosts (prevents "unknown terminal" errors) -auto-install-terminfo = true +shell-integration-features = ssh-terminfo -# Desktop notification when a long-running command finishes -notify-on-command-finish = true +# Desktop notification when a long-running command finishes (only when window isn't focused) +notify-on-command-finish = unfocused # Let tmux handle all window/tab management keybind = super+t=unbind diff --git a/gitignore b/gitignore index 83275666..39ece709 100644 --- a/gitignore +++ b/gitignore @@ -259,4 +259,5 @@ coverage/ .sass-cache/ *.pid *.seed -*.pid.lock \ No newline at end of file +*.pid.lock +**/.claude/settings.local.json