7 Коміти 5e2871f728 ... f50defe81c

Автор SHA1 Опис Дата
  Tom McKenzie f50defe81c feat: add gw worktree helper 3 тижнів тому
  Tom McKenzie 7cc08bf635 Add bat config (plain style, syntax highlighting only) 3 тижнів тому
  Tom McKenzie 2a83de5b05 fix(zsh): load brew shellenv before compinit so completions register 1 місяць тому
  Tom McKenzie 3c29993ed4 feat: difftastic as default git diff + watcher-agnostic rerun helper 1 місяць тому
  Tom McKenzie 68609d86a0 feat: publish public-safe app settings (ghostty, zed, rectangle) 1 місяць тому
  Tom McKenzie 1a2ea1e715 refactor: harden dotfiles for cross-machine compatibility 1 місяць тому
  Tom McKenzie b59d5a1e7b chore: sync uncommitted dotfile changes from mactom.local 1 місяць тому

+ 5 - 1
.bashrc

@@ -26,4 +26,8 @@ PS1="\[\e[1;32m\]\h:\W \u$\[\e[0m\] "
 bind '"\e[A": history-search-backward'
 bind '"\e[B": history-search-forward'
 
-. "$HOME/.cargo/env"
+[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
+
+# LM Studio CLI (lms)
+[ -d "$HOME/.lmstudio/bin" ] && export PATH="$PATH:$HOME/.lmstudio/bin"
+

+ 58 - 47
.gitconfig

@@ -1,56 +1,50 @@
 [user]
-    email = tom@chillidonut.com
-    name = Tom McKenzie
-    signingkey = 896D4BE5
-
+	email = tom@chillidonut.com
+	name = Tom McKenzie
+	signingkey = 896D4BE5
 [color]
-    branch = auto
-    diff = auto
-    status = auto
-
+	branch = auto
+	diff = auto
+	status = auto
 [color "branch"]
-    current = yellow reverse
-    local = yellow
-    remote = green
-
+	current = yellow reverse
+	local = yellow
+	remote = green
 [color "diff"]
-    meta = yellow bold
-    frag = magenta bold
-    old = red
-    new = cyan
-
+	meta = yellow bold
+	frag = magenta bold
+	old = red
+	new = cyan
 [color "status"]
-    added = yellow
-    changed = green
-    untracked = cyan
-
+	added = yellow
+	changed = green
+	untracked = cyan
 [help]
-    autocorrect = 10
-
+	autocorrect = 10
 [alias]
-    p = push
-    pp = publish
-    s = switch
-    sc = switch create
-    rb = pull --rebase
-    rbc = rebase --continue
-    rba = rebase --abort
-    l = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(cyan)<%an>%Creset' --abbrev-commit --date=relative
-    b = for-each-ref --count=10 --sort='-authordate:iso8601' --format=' %(color:green)%(authordate:relative) %09%(color:white)%(objectname:short) %(color:yellow)%(refname:short) %09%(color:white)%(upstream:track) %(contents:subject)' refs/heads --no-merged
-    edit-conflicted = ! git diff --name-only | uniq | xargs $(echo $EDITOR)
-    history = log -p --
+	p = push
+	pp = publish
+	s = switch
+	sc = switch create
+	rb = pull --rebase
+	rbc = rebase --continue
+	rba = rebase --abort
+	l = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(cyan)<%an>%Creset' --abbrev-commit --date=relative
+	b = for-each-ref --count=10 --sort='-authordate:iso8601' --format=' %(color:green)%(authordate:relative) %09%(color:white)%(objectname:short) %(color:yellow)%(refname:short) %09%(color:white)%(upstream:track) %(contents:subject)' refs/heads --no-merged
+	edit-conflicted = ! git diff --name-only | uniq | xargs $(echo $EDITOR)
+	history = log -p --
 	pushf = push --force-with-lease
-    pullall = ! git pull && git submodule update --init --recursive --remote
-    publish = ! git push -u origin `git rev-parse --abbrev-ref HEAD`
-    diverged = ! git log --graph --date=relative --oneline `git rev-parse --abbrev-ref HEAD` origin/`git rev-parse --abbrev-ref HEAD`
-    fixup = "!f() { TARGET=$(git rev-parse "$1"); git commit --fixup=$TARGET ${@:2} && EDITOR=true git rebase -i --autostash --autosquash $TARGET^; }; f"
-    last = rev-list -1 HEAD
+	pullall = ! git pull && git submodule update --init --recursive --remote
+	publish = ! git push -u origin `git rev-parse --abbrev-ref HEAD`
+	diverged = ! git log --graph --date=relative --oneline `git rev-parse --abbrev-ref HEAD` origin/`git rev-parse --abbrev-ref HEAD`
+	fixup = "!f() { TARGET=$(git rev-parse "$1"); git commit --fixup=$TARGET ${@:2} && EDITOR=true git rebase -i --autostash --autosquash $TARGET^; }; f"
+	last = rev-list -1 HEAD
 [core]
-	excludesfile = /Users/tom/.gitignore_global
+	excludesfile = ~/.gitignore_global
 	whitespace = tab-in-indent,trailing-space,space-before-tab
 	ignorecase = false
-# editor = vim -c 'startinsert'
-
+	# use delta if installed, else fall back to less (don't break git without delta)
+	pager = command -v delta >/dev/null 2>&1 && delta || less -R
 [push]
 	default = upstream
 [status]
@@ -58,8 +52,6 @@
 [merge "npm-merge-driver"]
 	name = automatically merge npm lockfiles
 	driver = npx npm-merge-driver merge %A %O %B %P
-# [pull]
-#	rebase = preserve
 [pull]
 	ff = only
 [advice]
@@ -69,7 +61,26 @@
 	smudge = git-lfs smudge -- %f
 	process = git-lfs filter-process
 	required = true
-
 [include]
-    path = ~/dotfiles/.gitconfig_difftastic
-    condition = command -v difft >/dev/null 2>&1
+	path = ~/dotfiles/.gitconfig_difftastic
+	condition = command -v difft >/dev/null 2>&1
+[interactive]
+	diffFilter = command -v delta >/dev/null 2>&1 && delta --color-only || cat
+[delta]
+    features = decorations
+	navigate = true
+	line-numbers = true
+	hyperlinks = true
+ [delta "decorations"]
+     ; commit-decoration-style = bold yellow box ul
+     ; file-style = bold yellow ul
+     file-decoration-style = bold
+     hunk-header-decoration-style = bold
+[merge]
+	conflictStyle = zdiff3
+[credential "https://github.com"]
+	helper = 
+	helper = !gh auth git-credential
+[credential "https://gist.github.com"]
+	helper = 
+	helper = !gh auth git-credential

+ 23 - 0
.gitconfig_difftastic

@@ -0,0 +1,23 @@
+[diff]
+    tool = difftastic
+    # make difftastic the default for `git diff`, via a wrapper that falls back
+    # to plain diff if difft isn't installed (see ~/dotfiles/bin/git-difft).
+    # git log/show/add -p keep using the internal diff + delta pager.
+    external = git-difft
+
+[difftool]
+    prompt = false
+
+[difftool "difftastic"]
+    cmd = difft "$LOCAL" "$REMOTE"
+
+[pager]
+    difftool = true
+    # page `git diff` (now difftastic output) with less, NOT delta — delta would
+    # garble difftastic's already-formatted output. core.pager=delta still applies
+    # to log/show/blame.
+    diff = less -R
+
+[alias]
+    dft = difftool
+    dlog = "-c diff.external=difft log -p --ext-diff"

+ 16 - 0
.gitignore

@@ -4,3 +4,19 @@
 .rc.d/local.sh
 .zshrc.d/local.zsh
 bak
+
+# Application settings: ignore the whole tree by default, then opt-in ONLY the
+# files we've vetted as public-safe. Anything new under config/ stays private
+# until explicitly un-ignored here (guards against leaking app secrets).
+config/**
+!config/bat/
+!config/bat/config
+!config/ghostty/
+!config/ghostty/config
+!config/zed/
+!config/zed/settings.json
+!config/zed/keymap.json
+!config/zed/tasks.json
+!config/rectangle/
+!config/rectangle/com.knollsoft.Rectangle.plist
+!config/rectangle/import.sh

+ 5 - 0
.gitignore_global

@@ -8,5 +8,10 @@ Icon
 ._*
 
 .todo
+.worktrees
 bak
 _docs
+
+tmp_code_*
+
+**/.claude/settings.local.json

+ 81 - 3
.rc.d/aliases.sh

@@ -16,8 +16,8 @@ alias duh="du -sh * | sort -h"
 # export local .env file
 alias export-env="export \$(grep -v '^#' .env | xargs -0)"
 
-# docker / podman
-if [ -f '/usr/bin/podman' ] || [ -f '/opt/homebrew/bin/podman' ]; then
+# docker / podman — only alias to podman if docker CLI isn't installed
+if ! command -v docker >/dev/null 2>&1 && command -v podman >/dev/null 2>&1; then
   alias docker='podman'
   alias docker-compose='podman-compose'
   alias dc='docker-compose'
@@ -84,7 +84,85 @@ checkout() {
   cd "$HOME/repos/$DIR" || return
 }
 
-alias ac="git diff --staged | llm -m openrouter/meta-llama/llama-4-maverick:free -s 'Write a concise, imperative-mood git commit message summarizing these changes. Begin with feat, fix, bug, docs, or chore. No preamble or yapping.' | git commit -F -"
+alias ac='git diff --staged | head -c $((128 * 1024 * 3)) | llm -m openrouter/openrouter/free '\''Write a concise, imperative-mood git commit message summarizing these changes. Begin with feat, fix, bug, docs, or chore. No preamble or yapping.'\'' | git commit -F -'
 
 alias pbjson="pbpaste | jq -r"
 alias pbjsons="pbpaste | jq -sRr '@json'"
+
+alias vscode-server="docker run -it --init -p 3000:3000 -v "$(pwd):/home/workspace:cached" gitpod/openvscode-server"
+
+alias cc='claude --dangerously-skip-permissions --system-prompt "."'
+alias ccw='claude --dangerously-skip-permissions --system-prompt "." --tmux -w'
+
+# gw — create a git worktree under .claude/worktrees/<name>, copy in the
+#      .worktreeinclude (gitignored) files, cd into it, and print its path.
+#      Git's chatter goes to stderr so stdout is just the path, e.g:
+#          zed "$(gw new-feature)"
+#   usage: gw <name> [base-ref]   (base-ref defaults to HEAD)
+gw() {
+  local repo name wt
+  repo=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "gw: not in a git repo" >&2; return 1; }
+  name=$1
+  [ -n "$name" ] || { echo "usage: gw <name> [base-ref]" >&2; return 1; }
+  wt="$repo/.claude/worktrees/$name"
+  [ -e "$wt" ] && { echo "gw: $wt already exists" >&2; return 1; }
+
+  git -C "$repo" worktree add -b "worktree-$name" "$wt" "${2:-HEAD}" >&2 || return
+
+  # replicate .worktreeinclude: copy gitignored files matching its patterns
+  if [ -f "$repo/.worktreeinclude" ]; then
+    git -C "$repo" ls-files -z --others --ignored --exclude-from=.worktreeinclude \
+      | while IFS= read -r -d '' f; do
+          mkdir -p "$(dirname "$wt/$f")"
+          cp -p "$repo/$f" "$wt/$f"
+        done
+  fi
+
+  echo "$wt"
+  cd "$wt"
+}
+
+alias t-claude="tmux has-session -t claude-session || tmux new-session -d -s claude-session; tmux attach-session -t claude-session"
+alias t-code="tmux has-session -t code-session || tmux new-session -d -s code-session; tmux attach-session -t code-session"
+
+# tt — attach/create the tmux session for the CURRENT dir, matching Zed's naming
+# (parent_current via -A). Switches instead of nesting when already inside tmux.
+tt() {
+  local name
+  name="$(basename "$(dirname "$PWD")")_$(basename "$PWD")"
+  if [ -n "$TMUX" ]; then
+    tmux has-session -t "=$name" 2>/dev/null || tmux new-session -d -s "$name"
+    tmux switch-client -t "=$name"
+  else
+    tmux new-session -A -s "$name"
+  fi
+}
+
+# tmux picker: native choose-tree (real tree, '/' filter, live preview, -Z zoom).
+# -F colorizes the right-hand descriptive column — the session/window NAME on the
+# left is drawn plain by tmux and can't be styled. Three line types handled via
+# the pane_format / window_format conditional (else-branch = session line).
+ts() {
+  local h="$HOME" pane win sess fmt
+  pane="#[fg=magenta]#{pane_current_command}#[default] #[fg=blue]#{s|$h|~|:pane_current_path}#[default]"
+  win="#[fg=magenta]#{pane_current_command}#[default]  #[fg=blue]#{s|$h|~|:pane_current_path}#[default]#{?window_zoomed_flag, #[fg=yellow]Z#[default],}#{?window_active, #[fg=green]●#[default],}  #[fg=colour244]#{t/f/%b %d %H#:%M:window_activity}#[default]"
+  sess="#[fg=yellow]#{session_windows}w#[default]  #{?session_attached,#[fg=green]● attached#[default],#[fg=colour244]detached#[default]}  #[fg=colour244]#{t/f/%b %d %H#:%M:session_activity}#[default]"
+  fmt="#{?pane_format,$pane,#{?window_format,$win,$sess}}"
+  if [ -n "$TMUX" ]; then
+    tmux choose-tree -Zs -F "$fmt"
+  else
+    tmux attach \; choose-tree -Zs -F "$fmt" 2>/dev/null || echo "no tmux sessions"
+  fi
+}
+
+# rerun <cmd...> — re-run a command whenever files under the cwd change, using
+# whichever file watcher is installed (watchexec preferred, else entr). Lets the
+# same alias work across machines that have one or the other.
+if command -v watchexec >/dev/null 2>&1; then
+  rerun() { watchexec --restart -- "$@"; }
+elif command -v entr >/dev/null 2>&1; then
+  # entr takes the file list on stdin; feed it everything under cwd minus .git
+  rerun() { find . -type f -not -path '*/.git/*' | entr -r -- "$@"; }
+else
+  rerun() { echo "rerun: install watchexec or entr" >&2; return 127; }
+fi

+ 3 - 0
.rc.d/applications.sh

@@ -14,5 +14,8 @@ if [ -d "$HOME/.rbenv" ]; then
   eval "$(rbenv init -)"
 fi
 
+# dotfiles bin/ helpers (aws-profile, uv-upgrade, git-difft, ...)
+[ -d "$HOME/dotfiles/bin" ] && export PATH="$PATH:$HOME/dotfiles/bin"
+
 # shhh
 export HOMEBREW_NO_ENV_HINTS=1

+ 3 - 0
.rc.d/git.sh

@@ -53,6 +53,7 @@ alias gdt='git diff-tree --no-commit-id --name-only -r'
 alias gdw='git diff --word-diff'
 
 alias gds='git diff --stat'
+alias gdsw='watch -n 0.5 -c -d git -c color.ui=always diff --stat'
 
 alias gf='git fetch'
 alias gfa='git fetch --all --prune'
@@ -76,3 +77,5 @@ alias gsu='git submodule update'
 alias gp='git push'
 alias gpf='git push --force-with-lease'
 alias gpl='git pull'
+
+alias gsom='git checkout origin/main'

+ 4 - 2
.rc.d/lazy-nvm.sh

@@ -11,7 +11,9 @@ function lazy_nvm {
   if [ -d "${HOME}/.nvm" ]; then
     export NVM_DIR="$HOME/.nvm"
     [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # linux
-    [ -s "$(brew --prefix nvm)/nvm.sh" ] && source $(brew --prefix nvm)/nvm.sh # osx
+    if command -v brew >/dev/null 2>&1; then
+      [ -s "$(brew --prefix nvm)/nvm.sh" ] && source "$(brew --prefix nvm)/nvm.sh" # osx
+    fi
     [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # nvm bash_completion
   fi
 }
@@ -22,4 +24,4 @@ function npm { lazy_nvm; npm "$@"; }
 function node { lazy_nvm; node "$@"; }
 function npx { lazy_nvm; npx "$@"; }
 function yarn { lazy_nvm; yarn "$@"; }
-function pnpm { lazy_nvm; yarn "$@"; }
+function pnpm { lazy_nvm; pnpm "$@"; }

+ 64 - 0
.tmux.conf

@@ -0,0 +1,64 @@
+set -g mouse on
+set -g base-index 1
+setw -g pane-base-index 1
+set -g history-limit 50000
+set -s escape-time 0
+set -g focus-events on
+set -g extended-keys on
+set -g display-time 4000
+set -g status-interval 5
+setw -g aggressive-resize on
+set -g status-keys emacs
+
+# Status bar as a tab bar
+set -g status-position bottom
+set -g status-style 'bg=colour235 fg=colour7'
+set -g status-left ' '
+# prefix-highlight (native #{client_prefix} — no plugin needed) + copy/sync indicators.
+# Indicators prepend to status-right so [+] [d] stay flush-right (mouse math intact).
+set -g status-right '#{?client_prefix,#[bg=colour3#,fg=colour235#,bold] ^B #[default] ,}#{?#{==:#{pane_mode},copy-mode},#[bg=colour2#,fg=colour235#,bold] COPY #[default] ,}#{?pane_synchronized,#[bg=colour1#,fg=colour15#,bold] SYNC #[default] ,} [+] [d] '
+# Tab label = the app's pane title (Claude Code sets a good one), trimmed to 24;
+# falls back to the running command for bare shells (where pane_title == hostname).
+# #{E:...} forces the stored format in @wtitle to expand. Replaces #W to dodge the
+# window-name pollution (apps leak junk like "2.1.156" into the window NAME).
+set -g @wtitle '#{?#{||:#{==:#{pane_title},},#{==:#{pane_title},#{host}}},#{pane_current_command},#{=/24/…:pane_title}}'
+# Tab: index:(NpZ) title — pane count + zoom flag prefixed in parens, only when split.
+setw -g window-status-format ' #I:#{?#{>:#{window_panes},1},(#{window_panes}p#{?window_zoomed_flag,Z,}) ,}#{E:@wtitle} '
+setw -g window-status-current-format ' #I:#{?#{>:#{window_panes},1},(#{window_panes}p#{?window_zoomed_flag,Z,}) ,}#{E:@wtitle} '
+setw -g window-status-current-style 'bg=colour4 fg=colour15 bold'
+set -g pane-border-status top
+
+# Click [+] to create new tab, [d] to detach
+bind -n MouseDown1StatusRight run-shell -b '\
+  x="#{mouse_x}"; \
+  w=$(tmux display-message -p "#{window_width}"); \
+  pos=$((w - x)); \
+  if [ "$pos" -ge 0 ] && [ "$pos" -le 3 ]; then \
+    tmux detach-client; \
+  elif [ "$pos" -ge 4 ] && [ "$pos" -le 8 ]; then \
+    tmux new-window; \
+  fi'
+
+# Close tab: just type exit (or Ctrl-d) in the shell
+
+# Window management menu (prefix + w)
+bind w display-menu -T "Windows" \
+    "New Window"      n "new-window" \
+    "Kill Window"     k "kill-window" \
+    "Next Window"     l "next-window" \
+    "Prev Window"     h "previous-window" \
+    "" \
+    "Horizontal Split" - "split-window -v" \
+    "Vertical Split"   | "split-window -h" \
+    "" \
+    "Kill Pane"        x "kill-pane" \
+    "Zoom Pane"        z "resize-pane -Z" \
+    "" \
+    "Choose Session"   s "choose-tree -s"
+
+bind-key -n M-w new-window
+
+# Fork Claude Code session into a new window
+bind R source-file ~/.tmux.conf \; display "Reloaded"
+
+bind f run-shell 'T="Fork ($(date "+%a %H:%M"))"; tmux new-window -a -n "$T" "claude --dangerously-skip-permissions --continue --fork-session"; tmux select-pane -T "$T"'

+ 22 - 2
.zshrc

@@ -4,6 +4,14 @@
 # ensure /usr/local/bin is in path
 export PATH=/usr/local/bin:~/.local/bin:$PATH
 
+# Homebrew — load EARLY so brew's completions are on $fpath before compinit runs
+# (zgen triggers compinit below; otherwise _zoxide & all brew completions get missed).
+if [ -x /opt/homebrew/bin/brew ]; then
+  eval "$(/opt/homebrew/bin/brew shellenv)"
+elif [ -x /usr/local/bin/brew ]; then
+  eval "$(/usr/local/bin/brew shellenv)"
+fi
+
 # load all files from .shell/zshrc.d directory
 if [ -d $HOME/.zshrc.d ]; then
   for file in $HOME/.zshrc.d/*.zsh; do
@@ -30,7 +38,7 @@ if [ -f '/Users/tom/google-cloud-sdk/path.zsh.inc' ]; then . '/Users/tom/google-
 # The next line enables shell command completion for gcloud.
 if [ -f '/Users/tom/google-cloud-sdk/completion.zsh.inc' ]; then . '/Users/tom/google-cloud-sdk/completion.zsh.inc'; fi
 
-if [ -f '/opt/homebrew/bin/brew' ]; then eval "$(/opt/homebrew/bin/brew shellenv)"; fi
+# (brew shellenv moved to the top so completions load before compinit)
 
 # bun completions
 [ -s "/Users/tom/.bun/_bun" ] && source "/Users/tom/.bun/_bun"
@@ -42,4 +50,16 @@ export PATH="$BUN_INSTALL/bin:$PATH"
 # zoxide
 if command -v zoxide >/dev/null 2>&1; then eval "$(zoxide init zsh)"; fi
 
-. "$HOME/.cargo/env"
+[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
+
+# opencode
+[ -d "$HOME/.opencode/bin" ] && export PATH="$HOME/.opencode/bin:$PATH"
+
+# LM Studio CLI (lms)
+[ -d "$HOME/.lmstudio/bin" ] && export PATH="$PATH:$HOME/.lmstudio/bin"
+
+
+# Added by LM Studio CLI (lms)
+export PATH="$PATH:/Users/tom/.lmstudio/bin"
+# End of LM Studio CLI section
+

+ 14 - 0
bin/git-difft

@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+# git diff.external wrapper: render diffs with difftastic when it's installed,
+# else fall back to a plain unified diff so `git diff` never hard-fails on a
+# machine without difft.
+#
+# git calls an external differ with 7 args:
+#   path  old-file  old-hex  old-mode  new-file  new-hex  new-mode
+if command -v difft >/dev/null 2>&1; then
+  exec difft "$@"
+fi
+# Fallback: plain unified diff. `diff` exits 1 when files differ, but git treats a
+# non-zero exit from an external differ as failure ("external diff died"), so we
+# swallow it and always exit 0.
+diff -u -L "a/$1" -L "b/$1" -- "$2" "$5" || true

+ 46 - 0
bin/uv-upgrade

@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+
+import re
+import subprocess
+import tomllib
+from pathlib import Path
+
+
+def uv(subcommand: str, packages: list[str], group: str | None):
+    extra_arguments = []
+    if group:
+        extra_arguments.extend(["--group", group])
+
+    subprocess.check_call(["uv", subcommand, *packages, "--no-sync"] + extra_arguments)
+
+
+def main():
+    """WARNING:
+    from the `pyproject.toml` file, this may delete:
+        - comments
+        - upper bounds etc
+        - markers
+        - ordering of dependencies
+        - tool.uv.sources
+    """
+    pyproject = tomllib.loads(Path("pyproject.toml").read_text())
+    package_name_pattern = re.compile(r"^([-a-zA-Z\d]+)(\[[-a-zA-Z\d,]+])?")
+    for group, dependencies in {
+        None: pyproject["project"]["dependencies"],
+        **pyproject.get("dependency-groups", {}),
+    }.items():
+        to_remove = []
+        to_add = []
+        for dependency in dependencies:
+            package_match = package_name_pattern.match(dependency)
+            assert package_match, f"invalid package name '{dependency}'"
+            package, extras = package_match.groups()
+            to_remove.append(package)
+            to_add.append(f"{package}{extras or ''}")
+        uv("remove", to_remove, group=group)
+        uv("add", to_add, group=group)
+    subprocess.check_call(["uv", "sync"])
+
+
+if __name__ == "__main__":
+    main()

+ 3 - 0
config/bat/config

@@ -0,0 +1,3 @@
+# bat config — just syntax highlighting.
+# plain = no line numbers, no file headers, no grid/borders.
+--style="plain"

+ 33 - 0
config/ghostty/config

@@ -0,0 +1,33 @@
+theme = Dark Pastel
+
+background-blur-radius = 20
+background-opacity = 0.85
+
+# font-family = JetBrainsMono
+font-family = Menlo
+font-size = 13
+font-thicken
+cursor-style = block
+cursor-style-blink = true
+
+scrollback-limit = 4294967295
+
+shell-integration = zsh
+shell-integration-features = no-cursor
+copy-on-select = clipboard
+
+window-save-state = always
+
+macos-titlebar-style = tabs
+macos-option-as-alt = true
+macos-icon = custom-style
+# macos-icon-frame = chrome
+macos-icon-ghost-color = #9580FF
+# macos-icon-screen-color = #1E1E2E, #302D41
+
+keybind = global:alt+`=toggle_quick_terminal
+quick-terminal-position = bottom
+quick-terminal-autohide = false
+quick-terminal-animation-duration = 0.1
+# copy-on-select = false
+keybind = shift+enter=text:\x1b\r

+ 192 - 0
config/rectangle/com.knollsoft.Rectangle.plist

@@ -0,0 +1,192 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>allowAnyShortcut</key>
+	<true/>
+	<key>almostMaximize</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>44</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>alternateDefaultShortcuts</key>
+	<true/>
+	<key>bottomHalf</key>
+	<dict/>
+	<key>bottomLeft</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>38</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>bottomRight</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>37</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>center</key>
+	<dict/>
+	<key>centerHalf</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>42</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>centerThird</key>
+	<dict/>
+	<key>firstFourth</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>41</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>firstThird</key>
+	<dict/>
+	<key>firstThreeFourths</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>41</integer>
+		<key>modifierFlags</key>
+		<integer>655360</integer>
+	</dict>
+	<key>firstTwoThirds</key>
+	<dict/>
+	<key>footprintAnimationDurationMultiplier</key>
+	<real>0.75</real>
+	<key>hapticFeedbackOnSnap</key>
+	<integer>1</integer>
+	<key>hideMenubarIcon</key>
+	<true/>
+	<key>internalTilingNotified</key>
+	<true/>
+	<key>landscapeSnapAreas</key>
+	<string>[1,{"action":15},2,{"action":2},8,{"action":14},5,{"action":34},3,{"action":16},6,{"action":13},4,{"compound":-2},7,{"compound":-4}]</string>
+	<key>larger</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>47</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>lastFourth</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>39</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>lastThird</key>
+	<dict/>
+	<key>lastThreeFourths</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>39</integer>
+		<key>modifierFlags</key>
+		<integer>655360</integer>
+	</dict>
+	<key>lastTwoThirds</key>
+	<dict/>
+	<key>lastVersion</key>
+	<string>101</string>
+	<key>launchOnLogin</key>
+	<true/>
+	<key>leftHalf</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>33</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>maximize</key>
+	<dict/>
+	<key>maximizeHeight</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>44</integer>
+		<key>modifierFlags</key>
+		<integer>655360</integer>
+	</dict>
+	<key>nextDisplay</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>30</integer>
+		<key>modifierFlags</key>
+		<integer>655360</integer>
+	</dict>
+	<key>previousDisplay</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>33</integer>
+		<key>modifierFlags</key>
+		<integer>655360</integer>
+	</dict>
+	<key>reflowTodo</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>45</integer>
+		<key>modifierFlags</key>
+		<integer>786432</integer>
+	</dict>
+	<key>restore</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>6</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>rightHalf</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>30</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>smaller</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>43</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>stageSize</key>
+	<real>30.634340286254883</real>
+	<key>subsequentExecutionMode</key>
+	<integer>0</integer>
+	<key>todo</key>
+	<integer>2</integer>
+	<key>toggleTodo</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>11</integer>
+		<key>modifierFlags</key>
+		<integer>786432</integer>
+	</dict>
+	<key>topHalf</key>
+	<dict/>
+	<key>topLeft</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>4</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>topRight</key>
+	<dict>
+		<key>keyCode</key>
+		<integer>40</integer>
+		<key>modifierFlags</key>
+		<integer>524288</integer>
+	</dict>
+	<key>unsnapRestore</key>
+	<integer>2</integer>
+	<key>windowSnapping</key>
+	<integer>2</integer>
+</dict>
+</plist>

+ 28 - 0
config/rectangle/import.sh

@@ -0,0 +1,28 @@
+#!/bin/bash
+# Restore Rectangle settings (shortcuts + behavior) on a new machine.
+#
+# Rectangle stores its config as a macOS preferences domain (a cached binary
+# plist), so it can't be symlinked like a text config. We version a *cleaned*
+# export (shortcuts + behavior only — no window-frame or Sparkle-updater noise)
+# and import it here.
+#
+# NOTE: `defaults import` REPLACES the Rectangle domain. Any existing prefs are
+# backed up to bak/ first. Safe on a fresh machine.
+set -euo pipefail
+
+DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DOMAIN="com.knollsoft.Rectangle"
+SRC="$DIR/${DOMAIN}.plist"
+BAK="$HOME/dotfiles/bak"
+
+[ -f "$SRC" ] || { echo "missing $SRC" >&2; exit 1; }
+
+mkdir -p "$BAK"
+if defaults export "$DOMAIN" - >/dev/null 2>&1; then
+  defaults export "$DOMAIN" "$BAK/${DOMAIN}.backup.plist" 2>/dev/null \
+    && echo "Backed up existing Rectangle prefs to bak/${DOMAIN}.backup.plist"
+fi
+
+defaults import "$DOMAIN" "$SRC"
+killall Rectangle 2>/dev/null || true
+echo "Rectangle settings imported. Relaunch Rectangle to apply."

+ 87 - 0
config/zed/keymap.json

@@ -0,0 +1,87 @@
+[
+  {
+    "bindings": {
+      "cmd-k cmd-,": "zed::OpenSettings",
+      "cmd-shift-r": "editor::Rename",
+      "cmd-shift-c": "workspace::CopyRelativePath",
+      "cmd-alt-shift-c": "editor::CopyPermalinkToLine",
+      "cmd-o": "file_finder::Toggle",
+      "cmd-shift-o": ["projects::OpenRecent", { "create_new_window": false }],
+      "cmd-t": "outline::Toggle",
+      "cmd-shift-t": "pane::ReopenClosedItem",
+      "cmd-p": "command_palette::Toggle",
+      "cmd-shift-p": "project_symbols::Toggle",
+      "alt-cmd-o": "workspace::Open",
+      "cmd-alt-n": ["task::Spawn", { "task_name": "agent terminal" }]
+    }
+  },
+  {
+    "context": "Editor",
+    "bindings": {
+      "cmd-shift-k": "editor::DeleteLine",
+      // "cmd-shift-e": "pane::RevealInProjectPanel"
+      "alt-shift-i": "editor::SplitSelectionIntoLines",
+      "cmd-,": "editor::Hover",
+      "cmd-;": "editor::FindAllReferences",
+      "cmd-alt-ctrl-[": "editor::FoldSelectedRanges",
+      "cmd-k cmd-l": "vim::ConvertToLowerCase",
+      "cmd-k cmd-u": "editor::ConvertToUpperCase",
+      "cmd-k cmd-i": "editor::ConvertToUpperCamelCase",
+      "cmd-k cmd-o": "editor::ConvertToLowerCamelCase"
+    }
+  },
+  {
+    "context": "ProjectPanel",
+    "bindings": {
+      "cmd-h": "project_panel::CollapseAllEntries"
+    }
+  },
+  {
+    "context": "ProjectPanel && not_editing",
+    "bindings": {
+      "alt-enter": "project_panel::Open"
+    }
+  },
+  {
+    // cmd-N selects tab N (mac-native). This shadows the default
+    // workspace::ActivatePane (jump to split N), so that moves to ctrl-N below
+    // — which was the now-redundant default tab binding (pane::ActivateItem).
+    "context": "Pane",
+    "bindings": {
+      "cmd-1": ["pane::ActivateItem", 0],
+      "cmd-2": ["pane::ActivateItem", 1],
+      "cmd-3": ["pane::ActivateItem", 2],
+      "cmd-4": ["pane::ActivateItem", 3],
+      "cmd-5": ["pane::ActivateItem", 4],
+      "cmd-6": ["pane::ActivateItem", 5],
+      "cmd-7": ["pane::ActivateItem", 6],
+      "cmd-8": ["pane::ActivateItem", 7],
+      "cmd-9": ["pane::ActivateItem", 8],
+      "ctrl-1": ["workspace::ActivatePane", 0],
+      "ctrl-2": ["workspace::ActivatePane", 1],
+      "ctrl-3": ["workspace::ActivatePane", 2],
+      "ctrl-4": ["workspace::ActivatePane", 3],
+      "ctrl-5": ["workspace::ActivatePane", 4],
+      "ctrl-6": ["workspace::ActivatePane", 5],
+      "ctrl-7": ["workspace::ActivatePane", 6],
+      "ctrl-8": ["workspace::ActivatePane", 7],
+      "ctrl-9": ["workspace::ActivatePane", 8]
+    }
+  }
+  // inline suggestions
+  // {
+  //   "context": "Editor && inline_completion && !showing_completions",
+  //   "bindings": {
+  //     // "tab": "editor::Tab",
+  //     // "alt+tab": "editor::AcceptInlineCompletion"
+  //     "tab": "editor::AcceptInlineCompletion"
+  //   }
+  // },
+  // {
+  //   "context": "Editor && showing_completions",
+  //   "bindings": {
+  //     "enter": "editor::Newline",
+  //     "tab": "editor::ConfirmCompletion"
+  //   }
+  // }
+]

+ 205 - 0
config/zed/settings.json

@@ -0,0 +1,205 @@
+//
+// Zed settings — keep this MINIMAL: only overrides vs Zed defaults.
+// Full key reference: command palette -> "zed: open default settings".
+// Docs: https://zed.dev/docs/configuring-zed
+//
+// Rebuilt 2026-05-31: AI/agent config, MCP context servers, and the
+// transparency theme_overrides were stripped for a clean baseline.
+// Backup of the old config: ~/zed-backup-20260531-151032.zip
+//
+{
+  // theme
+  "agent_servers": {
+    "claude-acp": {
+      "default_config_options": {
+        "mode": "acceptEdits",
+      },
+      "type": "registry",
+    },
+  },
+  "minimum_split_diff_width": 220.0,
+  "diff_view_style": "split",
+  "agent": {
+    "use_modifier_to_send": true,
+    "thinking_display": "preview",
+    "play_sound_when_agent_done": "when_hidden",
+    "sidebar_side": "right",
+    "button": true,
+    "favorite_models": [],
+    "model_parameters": [],
+  },
+  "theme": {
+    "mode": "dark",
+    "light": "One Light",
+    "dark": "macOS Classic Dark"
+  },
+  "icon_theme": {
+    "mode": "dark",
+    "light": "Monospace Icon Theme - Dark",
+    "dark": "Monospace Icon Theme - Dark"
+  },
+
+  // fonts
+  "ui_font_family": "Iosevka Aile",
+  "ui_font_size": 14,
+  "buffer_font_family": "Iosevka Slab Extended",
+  "buffer_font_size": 14,
+
+  // editor
+  "show_whitespaces": "selection",
+  "soft_wrap": "editor_width",
+  "always_treat_brackets_as_autoclosed": true,
+  "use_autoclose": true,
+  "confirm_quit": true,
+  "active_pane_modifiers": {
+    "inactive_opacity": 0.8,
+  },
+
+  // one focused window per project
+  "cli_default_open_behavior": "new_window",
+  "use_system_window_tabs": false,
+
+  // ui chrome
+  "title_bar": {
+    "show_menus": false,
+    "show_branch_status_icon": false,
+  },
+  "status_bar": {
+    "show_active_file": false,
+    "line_endings_button": false,
+  },
+  "collaboration_panel": {
+    "button": false,
+  },
+  "bottom_dock_layout": "full",
+
+  // tabs
+  "preview_tabs": {
+    "enable_keep_preview_on_code_navigation": true,
+    "enable_preview_from_file_finder": true,
+  },
+  "tab_bar": {
+    "show_pinned_tabs_in_separate_row": true,
+    "show_nav_history_buttons": true,
+  },
+  "tabs": {
+    "show_diagnostics": "errors",
+    "activate_on_close": "left_neighbour",
+    "file_icons": false,
+    "close_position": "left",
+    "git_status": true,
+  },
+
+  // search
+  "search": {
+    "button": true,
+    "regex": true,
+  },
+
+  // minimap
+  "minimap": {
+    "thumb_border": "none",
+    "show": "auto",
+    "thumb": "hover",
+    // "thumb_border": "left_open",
+    "current_line_highlight": "line",
+  },
+
+  // git
+  "git": {
+    "inline_blame": {
+      "min_column": 80,
+      "show_commit_summary": true,
+    },
+    "git_gutter": null,
+    "worktree_directory": ".worktrees",
+  },
+
+  // terminal
+  "terminal": {
+    "font_family": "Iosevka Term Slab",
+    "toolbar": {
+      "breadcrumbs": true,
+    },
+    "env": {
+      "EDITOR": "zed --wait",
+    },
+  },
+
+  // panels
+  "project_panel": {
+    "entry_spacing": "comfortable",
+    "button": true,
+    "indent_size": 7,
+    "auto_reveal_entries": true,
+    "auto_fold_dirs": true,
+  },
+  "outline_panel": {
+    "dock": "left",
+    "indent_size": 7,
+  },
+
+  // type hints / lsp
+  "inlay_hints": {
+    "enabled": false,
+    "show_type_hints": false,
+    "show_parameter_hints": true,
+    "show_other_hints": true,
+  },
+  "lsp": {
+    "typescript-language-server": {
+      "initialization_options": {
+        "preferences": {
+          "includeInlayParameterNameHints": "all",
+          "includeInlayFunctionParameterTypeHints": true,
+          "includeInlayVariableTypeHints": true,
+          "includeInlayPropertyDeclarationTypeHints": true,
+          "includeInlayFunctionLikeReturnTypeHints": true,
+        },
+      },
+    },
+    "pyright": {
+      "settings": {
+        "python.analysis": {
+          "diagnosticMode": "workspace",
+        },
+        "python": {
+          "pythonPath": ".venv/bin/python",
+        },
+      },
+    },
+    "typos": {
+      "initialization_options": {
+        "diagnosticSeverity": "Hint",
+      },
+    },
+  },
+
+  // languages / formatting
+  "languages": {
+    "JSON": {
+      "prettier": { "allowed": true, "parser": "json" },
+    },
+    "JavaScript": {
+      "format_on_save": "on",
+      "code_actions_on_format": { "source.organizeImports": true },
+    },
+    "TypeScript": {
+      "format_on_save": "on",
+      "code_actions_on_format": { "source.organizeImports": true },
+    },
+    "TSX": {
+      "format_on_save": "on",
+      "code_actions_on_format": {
+        "source.organizeImports": true,
+        "source.fixAll": true,
+      },
+    },
+    "Python": {
+      "format_on_save": "on",
+      "formatter": {
+        "external": { "command": "black", "arguments": ["-"] },
+      },
+    },
+  },
+}

+ 20 - 0
config/zed/tasks.json

@@ -0,0 +1,20 @@
+// Static tasks. Bound to keys in keymap.json.
+[
+  {
+    // Attach (or create) the project's tmux session — same parent_current naming
+    // as the `tt` shell alias. Opens as a CENTER tab, not the bottom dock.
+    // allow_concurrent_runs:false → one canonical, reusable tmux terminal.
+    "label": "agent terminal",
+    "command": "tmux new-session -A -s \"$(basename \"$(dirname \"$ZED_WORKTREE_ROOT\")\")_$(basename \"$ZED_WORKTREE_ROOT\")\" \"$SHELL -ic cc\"",
+    "cwd": "$ZED_WORKTREE_ROOT",
+    "reveal_target": "center",
+    "use_new_terminal": false,
+    "allow_concurrent_runs": false,
+    "reveal": "always",
+    "hide": "on_success",
+    "shell": "system",
+    "env": {
+      "EDITOR": "zed --wait",
+    },
+  },
+]

+ 29 - 3
setup.sh

@@ -3,14 +3,40 @@
 # symlink dotfiles
 dir="$HOME/dotfiles"
 olddir="$HOME/dotfiles/bak"   # backup directory
-files=".zshrc .zshrc.d .rc.d .bashrc .vimrc .bash_logout .gitconfig .gitignore_global .gemrc .vim"    # list of files/folders to symlink in homedir
+files=".zshrc .zshrc.d .rc.d .bashrc .vimrc .bash_logout .gitconfig .gitignore_global .gemrc .vim .tmux.conf"    # list of files/folders to symlink in homedir
 
 ## create dotfiles_old in homedir
 mkdir -p $olddir
 
 ## move any existing dotfiles in homedir to dotfiles_old directory, then create symlinks
 for file in $files; do
-    mv -f "$HOME/$file" $olddir
+    [ -e "$HOME/$file" ] && mv -f "$HOME/$file" $olddir
     echo "Creating symlink to $file in ~"
-    ln -s "$dir/$file" "$HOME/$file"
+    ln -sfn "$dir/$file" "$HOME/$file"
 done
+
+# ── App settings ─────────────────────────────────────────────────────────────
+# Symlink vetted, public-safe app configs from config/ into their real homes.
+# (See .gitignore — config/ is ignored by default, these files are opted in.)
+link_config() {   # link_config <repo-path> <abs-dest>
+    local src="$dir/$1" dest="$2"
+    [ -e "$src" ] || return 0
+    mkdir -p "$(dirname "$dest")"
+    if [ -e "$dest" ] && [ ! -L "$dest" ]; then
+        mkdir -p "$olddir/$(dirname "$1")"
+        mv -f "$dest" "$olddir/$1"
+    fi
+    ln -sfn "$src" "$dest"
+    echo "Linked $1 -> $dest"
+}
+
+link_config config/bat/config        "$HOME/.config/bat/config"
+link_config config/ghostty/config    "$HOME/.config/ghostty/config"
+link_config config/zed/settings.json "$HOME/.config/zed/settings.json"
+link_config config/zed/keymap.json   "$HOME/.config/zed/keymap.json"
+link_config config/zed/tasks.json    "$HOME/.config/zed/tasks.json"
+
+# Rectangle: prefs domain can't be symlinked — import the cleaned export instead.
+if command -v defaults >/dev/null 2>&1 && [ -f "$dir/config/rectangle/import.sh" ]; then
+    bash "$dir/config/rectangle/import.sh" || true
+fi