commit b487b0e8557d549225d07facbafbee6b00f1a605 Author: bcherb2 Date: Mon Aug 17 00:11:52 2026 -0400 feat: public dotfiles tier — no credential, no identity, one installer Fresh history. This is the repo a throwaway VM clones anonymously: it brings a machine to a working baseline and carries nothing that makes it mine. 56 files. 50 land in $HOME, 3 are chezmoi metadata, 2 are repo documentation, 1 is the manifest, and a 15-file test harness stays behind in .tests/. What did not travel, and why: encrypted_private_bws-token.age a real credential; age is dropped entirely .chezmoidata/bws.toml env-var -> secret-id map; belongs with the tier that can use it SECRETS.md documentation of the rules, not config finish-setup.sh.tmpl superseded by dotup nvim/init.lua.backup dead file dot_claude/**, dot_codex/**, 120 files of agent config, private tier dot_pi/** De-identified rather than dropped: .gitconfig [user], the GitHub ssh rewrite and both Gitea host rewrites are identity, not configuration. They move behind an [include] of ~/.config/git/config.local, which the private tier writes. Git treats a missing include as a no-op, so a public-only machine reads the file and stops. .zshrc the two gitea aliases carried a personal domain and a LAN IP. They move behind a guarded source of ~/.config/zsh/local.zsh, the sibling of the secrets.zsh seam phase 2 established. nvim a commented-out LM Studio endpoint naming a LAN address. ghostty a stale auto-generated header naming an absolute home directory. Newly captured, never tracked before: ~/.zshenv, ~/.config/gh/config.yml. The former sourced ~/.cargo/env unguarded, so every zsh on a machine without rustup printed an error -- the same shape as the unguarded oh-my-zsh source phase 2 fixed. It is guarded now. .chezmoiexternal.toml grows from one entry to six. oh-my-zsh, powerlevel10k, zsh-autosuggestions, zsh-ai and tpm were hand-installed and declared nowhere, which is why `chezmoi init --apply` on a clean box produced a .zshrc that broke the shell it configures. The theme and both plugins nest under .oh-my-zsh/custom/, which is what $ZSH_CUSTOM resolves to. dotup gains an install engine. It resolves each selected package to a channel (apt, brew, npm, uv, snap, deb, flatpak, tarball, script, builtin) through one function every consumer reads, probes apt-cache before batching so a name apt does not know moves to brew instead of failing all thirty, and retries individually if a batch still fails -- which earned its keep on the first real container run, where mermaid-cli's puppeteer dependency failed and the other twelve npm packages installed anyway. --unattended computes safe defaults fresh from the manifest rather than inheriting a state file, and refuses private and invasive rows outright even when a stale state file ticks them. The manifest gains @spec, a second directive kind alongside @needs, carrying the argument a channel needs but a package name cannot supply -- the scoped npm name, the flatpak app id, the .deb source. The TSV stays five columns wide. Three bugs the container runs found, all fixed here: * `apt install nodejs` gives you node WITHOUT npm on Ubuntu, so all thirteen npm packages failed on a fresh box. The manifest asks apt for both names. * A tool installed a moment ago is not on this process's PATH -- uv lands in ~/.local/bin, npm -g honours the ~/.npmrc prefix, linuxbrew is outside a non-login PATH. Resolved by looking in the places we just wrote to, never by exporting a modified PATH. * `A || { B && C; }` is one || list, so when `command -v sudo` failed the list failed and `set -e` killed dotup at load. On a non-root machine with no sudo it died before printing anything. There is a regression test. .zshenv and .p10k.zsh are marked private_. Both are shell code the login shell executes and both applied at 664, group-writable. Third occurrence of the class of bug phase 1 found on .pi/agent/auth.json and phase 2 found on .zshrc; the first one found on purpose rather than by accident. Verification: 81 assertions, 81/81 on this box and in ubuntu:24.04, ubuntu:22.04 and debian:12. The installer is driven against a directory of fake package managers that record what they were asked to do and install nothing, so the engine is exercised end to end without a package landing on the test machine. `gitleaks detect` over the full history and the working tree: no leaks found, with no allowlist and no .gitleaks.toml. Co-Authored-By: Claude Opus 5 diff --git a/.chezmoi.toml.tmpl b/.chezmoi.toml.tmpl new file mode 100644 index 0000000..fd4cace --- /dev/null +++ b/.chezmoi.toml.tmpl @@ -0,0 +1,19 @@ +# chezmoi configuration for the PUBLIC tier. +# Rendered into the config file named by `chezmoi init` (see README.md). +# +# There is deliberately no `encryption` key and no [age] section. The previous +# repo carried both, pointing at ~/.config/chezmoi/key.txt and an age recipient. +# That layer is gone: the one secret in the system is fetched over an +# authenticated HTTPS connection at install time, so there is nothing left on +# disk for a key to unlock. On one of the three machines the identity file was +# already 0 bytes, mode 664 — broken, silently, with nothing surfacing it. +# +# There are deliberately no [data] prompts either. This tier must clone and +# apply on a throwaway VM with nobody at the keyboard, so it may not ask a +# question it cannot answer. Your name, email, signing key and Gitea hosts are +# identity, not configuration; they are prompted once by the PRIVATE tier and +# land in ~/.config/git/config.local and ~/.config/zsh/local.zsh, which this +# tier includes if present and ignores if not. +# +# If this file ever needs a real key, that is a signal worth pausing on: the +# public tier's whole value is that it is the same on every machine. diff --git a/.chezmoiexternal.toml b/.chezmoiexternal.toml new file mode 100644 index 0000000..51de223 --- /dev/null +++ b/.chezmoiexternal.toml @@ -0,0 +1,76 @@ +# Externals — the shell dependency chain, declared for the first time. +# +# None of these were tracked or declared anywhere before. `.zshrc` sourced +# $ZSH/oh-my-zsh.sh unguarded, so `chezmoi init --apply` on a clean box produced +# a .zshrc that broke the shell it configures, and nothing in the repo said why. +# The guard went into .zshrc in phase 2; the six entries below are the other +# half — the thing the guard is waiting for. +# +# ORDER MATTERS. chezmoi processes externals in target-path order, so +# `.oh-my-zsh` is cloned before anything under `.oh-my-zsh/custom/`. The theme +# and the two plugins are nested there deliberately: $ZSH_CUSTOM is +# ~/.oh-my-zsh/custom (see .zshrc), and oh-my-zsh's own custom/.gitignore +# excludes everything but example.zsh, so the nested clones do not dirty the +# parent working tree on refresh. +# +# --depth=1 on clone is what each of these projects recommends; a full +# oh-my-zsh history is ~50 MB of bootstrap nobody reads. `pull --ff-only` keeps +# a refresh from ever creating a merge commit in someone else's repo. + +[".tmux"] + type = "git-repo" + url = "https://github.com/gpakosz/.tmux.git" + refreshPeriod = "168h" + [".tmux".pull] + args = ["--ff-only"] + +[".oh-my-zsh"] + type = "git-repo" + url = "https://github.com/ohmyzsh/ohmyzsh.git" + refreshPeriod = "168h" + [".oh-my-zsh".clone] + args = ["--depth=1"] + [".oh-my-zsh".pull] + args = ["--ff-only"] + +[".oh-my-zsh/custom/themes/powerlevel10k"] + type = "git-repo" + url = "https://github.com/romkatv/powerlevel10k.git" + refreshPeriod = "168h" + [".oh-my-zsh/custom/themes/powerlevel10k".clone] + args = ["--depth=1"] + [".oh-my-zsh/custom/themes/powerlevel10k".pull] + args = ["--ff-only"] + +[".oh-my-zsh/custom/plugins/zsh-autosuggestions"] + type = "git-repo" + url = "https://github.com/zsh-users/zsh-autosuggestions.git" + refreshPeriod = "168h" + [".oh-my-zsh/custom/plugins/zsh-autosuggestions".clone] + args = ["--depth=1"] + [".oh-my-zsh/custom/plugins/zsh-autosuggestions".pull] + args = ["--ff-only"] + +# .zshrc adds this to the plugins array only when this directory exists, and +# only here — it used to be in the array on every platform AND separately +# sourced from /opt/homebrew on macOS, so it loaded twice on mac and not at all +# on Linux. Default branch is `main`, not `master`. +[".oh-my-zsh/custom/plugins/zsh-ai"] + type = "git-repo" + url = "https://github.com/matheusml/zsh-ai.git" + refreshPeriod = "168h" + [".oh-my-zsh/custom/plugins/zsh-ai".clone] + args = ["--depth=1"] + [".oh-my-zsh/custom/plugins/zsh-ai".pull] + args = ["--ff-only"] + +# Was a checked-in TPM checkout under .config/tmux/plugins. It is a clone of +# someone else's repo; it belongs here, not in git history. +[".config/tmux/plugins/tpm"] + type = "git-repo" + url = "https://github.com/tmux-plugins/tpm.git" + refreshPeriod = "168h" + [".config/tmux/plugins/tpm".clone] + args = ["--depth=1"] + [".config/tmux/plugins/tpm".pull] + args = ["--ff-only"] diff --git a/.chezmoiignore b/.chezmoiignore new file mode 100644 index 0000000..92150cb --- /dev/null +++ b/.chezmoiignore @@ -0,0 +1,42 @@ +# .chezmoiignore — PUBLIC tier. +# +# This is gitignore syntax. A `#` in the middle of a line becomes part of the +# pattern, silently producing an entry that matches nothing, so every comment +# in this file is on its own line. That mistake was caught by inspection once +# already; it does not announce itself. +# +# Ignoring changes what chezmoi manages. It never removes or modifies a file on +# any machine. + +# Repo documentation, not a dotfile. Without this it lands as ~/README.md. +README.md + +# Auto-generated by lazy.nvim on every plugin update. +.config/nvim/lazy-lock.json + +# chezmoi's own config directory. Never manage the thing that configures the +# manager. +.config/chezmoi +.config/chezmoi/** + +# Nested git checkouts. Configs that are repos get their own repo. +**/.git +**/.git/** + +# App-managed state. Each of these is rewritten by the application that owns +# it, so a tracked copy can never match the live one and `chezmoi status` is +# never empty. Carried in the source as a starting point, applied never. +.config/btop/btop.conf +.config/herdr/config.toml + +# Belt and braces on the tier boundary. The public repo does not manage any of +# these and must never start: a stray `chezmoi add ~/.config/gh` would +# otherwise sweep up an OAuth token, and the same for the bws token and the +# generated key file. These are the exact paths the phase 3 negative test +# asserts are absent on a public-only machine. +.config/gh/hosts.yml +.config/zsh/secrets.zsh +.config/bitwarden +.config/bitwarden/** +.claude.json +.claude/settings.local.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2d6455 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# This repo is public and holds no credential. These patterns are not the +# control that keeps it that way -- `gitleaks detect` over the full history is, +# and it runs with zero allowlist entries. These are here so an accident has to +# be deliberate. + +# Anything shaped like a credential +*.age +*.key +*.pem +*_key +*.secret +*_secret +.env +.env.* +!.env.example + +# Files the tier boundary says belong elsewhere +private_dot_config/bitwarden/ +private_dot_config/gh/hosts.yml +private_dot_config/zsh/secrets.zsh +.chezmoidata/bws.toml + +# chezmoi runtime state, never the source +.chezmoistate.boltdb +.chezmoi.toml +!.chezmoi.toml.tmpl + +# Picker state and its cached fzf, both machine-local by design +.tests/state/ +.tests/fzfver + +# OS junk +.DS_Store +**/dot_DS_Store diff --git a/.tests/fakebin/_log b/.tests/fakebin/_log new file mode 100755 index 0000000..43ae177 --- /dev/null +++ b/.tests/fakebin/_log @@ -0,0 +1,32 @@ +#!/bin/sh +# Every fake in this directory is a symlink to this file. It records the call +# and succeeds, so the installer can be driven end to end without a single +# package being installed, on this machine or in a container. +# +# `sudo` is faked too, and deliberately does NOT exec its argument: a run as +# root in a container would otherwise reach the real `rm -rf /opt/nvim`. +printf '%s %s\n' "${0##*/}" "$*" >> "${DOTUP_TEST_LOG:?DOTUP_TEST_LOG unset}" + +case ${0##*/} in +apt-cache) + # Stand in for a real apt cache: everything is known except `gh`, which is + # genuinely absent from stock Ubuntu and is the manifest's real example of a + # name that has to fall through to brew. + case $2 in gh) exit 100 ;; esac + printf 'Package: %s\n' "$2"; exit 0 ;; +curl) + # Enough shape for the two bespoke handlers that parse a response. + for a in "$@"; do + case $a in + *go.dev/VERSION*) echo go1.99.0; exit 0 ;; + *api.github.com*) echo ' "browser_download_url": "https://example.invalid/fake_amd64.deb"'; exit 0 ;; + esac + done + prev= + for a in "$@"; do + [ "$prev" = "-o" ] && { : > "$a"; exit 0; } + prev=$a + done + exit 0 ;; +esac +exit 0 diff --git a/.tests/fakebin/apt-cache b/.tests/fakebin/apt-cache new file mode 120000 index 0000000..46acdae --- /dev/null +++ b/.tests/fakebin/apt-cache @@ -0,0 +1 @@ +_log \ No newline at end of file diff --git a/.tests/fakebin/apt-get b/.tests/fakebin/apt-get new file mode 120000 index 0000000..46acdae --- /dev/null +++ b/.tests/fakebin/apt-get @@ -0,0 +1 @@ +_log \ No newline at end of file diff --git a/.tests/fakebin/brew b/.tests/fakebin/brew new file mode 120000 index 0000000..46acdae --- /dev/null +++ b/.tests/fakebin/brew @@ -0,0 +1 @@ +_log \ No newline at end of file diff --git a/.tests/fakebin/curl b/.tests/fakebin/curl new file mode 120000 index 0000000..46acdae --- /dev/null +++ b/.tests/fakebin/curl @@ -0,0 +1 @@ +_log \ No newline at end of file diff --git a/.tests/fakebin/flatpak b/.tests/fakebin/flatpak new file mode 120000 index 0000000..46acdae --- /dev/null +++ b/.tests/fakebin/flatpak @@ -0,0 +1 @@ +_log \ No newline at end of file diff --git a/.tests/fakebin/npm b/.tests/fakebin/npm new file mode 120000 index 0000000..46acdae --- /dev/null +++ b/.tests/fakebin/npm @@ -0,0 +1 @@ +_log \ No newline at end of file diff --git a/.tests/fakebin/snap b/.tests/fakebin/snap new file mode 120000 index 0000000..46acdae --- /dev/null +++ b/.tests/fakebin/snap @@ -0,0 +1 @@ +_log \ No newline at end of file diff --git a/.tests/fakebin/sudo b/.tests/fakebin/sudo new file mode 120000 index 0000000..46acdae --- /dev/null +++ b/.tests/fakebin/sudo @@ -0,0 +1 @@ +_log \ No newline at end of file diff --git a/.tests/fakebin/tar b/.tests/fakebin/tar new file mode 120000 index 0000000..46acdae --- /dev/null +++ b/.tests/fakebin/tar @@ -0,0 +1 @@ +_log \ No newline at end of file diff --git a/.tests/fakebin/uv b/.tests/fakebin/uv new file mode 120000 index 0000000..46acdae --- /dev/null +++ b/.tests/fakebin/uv @@ -0,0 +1 @@ +_log \ No newline at end of file diff --git a/.tests/fakebin/xcode-select b/.tests/fakebin/xcode-select new file mode 120000 index 0000000..46acdae --- /dev/null +++ b/.tests/fakebin/xcode-select @@ -0,0 +1 @@ +_log \ No newline at end of file diff --git a/.tests/fakeuname/uname b/.tests/fakeuname/uname new file mode 100755 index 0000000..cebf914 --- /dev/null +++ b/.tests/fakeuname/uname @@ -0,0 +1,4 @@ +#!/bin/sh +# Stands in for a Mac. The macOS half of the manifest has no CI anywhere, and +# resolution is pure data, so it can be checked from here. +case ${1:-} in -m) echo arm64 ;; *) echo Darwin ;; esac diff --git a/.tests/listen-test.sh b/.tests/listen-test.sh new file mode 100755 index 0000000..b8e968b --- /dev/null +++ b/.tests/listen-test.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# Drive the real picker over fzf's --listen API and assert the cursor does not +# jump on reload. Without that property, ticking four packages in a row means +# four scroll-backs and the tree design is unusable. +# +# fzf needs a terminal, so the work happens inside `script`, which re-enters +# this file with --inner rather than serialising a shell function (declare -f +# is bash-only, and the suite runs under dash). +set -eu +cd "$(dirname "$0")" +ROOT=$(CDPATH= cd -- .. && pwd) +export DOTUP_STATE=$PWD/state +export DOTUP_MANIFEST=$ROOT/dot_local/share/dotup/packages.tsv +S=$ROOT/dot_local/bin/executable_dotup +PORT=${DOTUP_TEST_PORT:-$((20000 + $$ % 20000))} + +if [ "${1:-}" != "--inner" ]; then + DOTUP_TEST_PORT=$PORT; export DOTUP_TEST_PORT + script -qec "sh $0 --inner" /dev/null 2>&1 \ + | tr -d '\r' | grep -oE 'CURSOR_(HELD|MOVED)[^ ]*|ROW_FLIPPED' || true + exit 0 +fi + +export FZF_DEFAULT_OPTS="--listen $PORT" +( + sleep 1.3 + curl -s -XPOST "localhost:$PORT" -d 'pos(4)' >/dev/null; sleep 0.3 + before=$(curl -s "localhost:$PORT" | grep -o '"position":[0-9]*') + row1=$(curl -s "localhost:$PORT" | grep -o '"current":{[^}]*}') + curl -s -XPOST "localhost:$PORT" \ + -d "execute-silent($S toggle {2})+reload($S render)" >/dev/null + sleep 0.8 + after=$(curl -s "localhost:$PORT" | grep -o '"position":[0-9]*') + row2=$(curl -s "localhost:$PORT" | grep -o '"current":{[^}]*}') + if [ "$before" = "$after" ]; then echo "CURSOR_HELD"; else echo "CURSOR_MOVED"; fi + # same row, opposite tick state + case "$row1|$row2" in + *'[x]'*'|'*'[ ]'*|*'[ ]'*'|'*'[x]'*) echo "ROW_FLIPPED" ;; + esac + curl -s -XPOST "localhost:$PORT" -d 'abort' >/dev/null +) & + +sh "$S" preset defaults +sh "$S" expand-all >/dev/null +sh "$S" pick >/dev/null 2>&1 || true +wait diff --git a/.tests/test.sh b/.tests/test.sh new file mode 100755 index 0000000..503190c --- /dev/null +++ b/.tests/test.sh @@ -0,0 +1,347 @@ +#!/bin/sh +# dotup test suite. +# +# Everything here is pty-free and runs unchanged in a container: the toggle +# rule is pure state-file logic, the match-confinement test uses +# `fzf --filter`, which is non-interactive, and the installer is driven against +# a directory of fake package managers that record what they were asked to do +# and install nothing. The one test that genuinely needs a terminal is fenced +# off at the bottom and skips cleanly without one. +# +# sh .tests/test.sh on the host +# sh .tests/test.sh --docker in a clean ubuntu:24.04 container +# +# NOTHING IN THIS FILE INSTALLS A PACKAGE. If you add a case that could, it +# belongs in a container and behind an explicit opt-in, not here. + +set -eu +cd "$(dirname "$0")" + +if [ "${1:-}" = "--docker" ]; then + # curl and script are here so the pty test runs too: the container has + # apt's fzf 0.44.1, not whatever the dev box has, and the cursor-on-reload + # behaviour is the one property that must hold on the older release. + exec docker run --rm -v "$(cd .. && pwd):/w:ro" "${DOTUP_TEST_IMAGE:-ubuntu:24.04}" sh -c ' + apt-get update -qq + apt-get install -y -qq fzf curl ca-certificates >/dev/null 2>&1 + cp -r /w /work && cd /work/.tests && rm -rf state fzfver + echo " distro fzf: $(fzf --version 2>/dev/null || echo none)" + sh ../dot_local/bin/executable_dotup preflight # exactly what a real run does first + exec sh test.sh' +fi + +ROOT=$(CDPATH= cd -- .. && pwd) +D=$ROOT/dot_local/bin/executable_dotup +M=$ROOT/dot_local/share/dotup/packages.tsv +export DOTUP_STATE=$PWD/state +export DOTUP_MANIFEST=$M + +pass=0; fail=0 +ok() { pass=$((pass+1)); printf ' \033[32mok\033[0m %s\n' "$1"; } +no() { fail=$((fail+1)); printf ' \033[31mFAIL\033[0m %s\n' "$1"; } +is() { # is + if [ "$2" = "$3" ]; then ok "$1"; else no "$1 — want [$2] got [$3]"; fi +} +has() { # has + case $3 in *"$2"*) ok "$1" ;; *) no "$1 — [$2] not in output" ;; esac +} +hasnt(){ # hasnt + case $3 in *"$2"*) no "$1 — [$2] IS in output" ;; *) ok "$1" ;; esac +} +# Normalise before parsing: "[x]" is one awk field but "[ ]" is two, so column +# positions shift with tick state. Collapse the box to a single token first. +rows() { sh $D render | sed 's/\x1b\[[0-9;]*m//g; s/\[[x~ ]\]/BOX/'; } +grp() { rows | awk -v g="$1" '$1!="BOX" && $3==g {print $4}'; } +# Every selected key matching a flag. Trailing ":" keeps a failed grep on the +# last item from tripping set -e via the loop's exit status. +picked(){ awk -F'\t' -v f="$1" '!/^#/ && !/^@/ && NF>=3 && $3==f {print $1"/"$2}' "$M" \ + | while read -r k; do grep -qxF "$k" state/selected && echo "$k"; done; :; } +missed(){ awk -F'\t' -v f="$1" '!/^#/ && !/^@/ && NF>=3 && $3==f {print $1"/"$2}' "$M" \ + | while read -r k; do grep -qxF "$k" state/selected || echo "$k"; done; :; } +reset() { rm -rf state; sh $D preset defaults; } + +printf '\n\033[1mtoggle rule\033[0m\n' +reset +is "package off -> group goes partial" "2/3" \ + "$(sh $D toggle p:media/sox >/dev/null; grp media)" +is "group partial -> all on" "3/3" \ + "$(sh $D toggle g:media >/dev/null; grp media)" +is "group full -> all off" "0/3" \ + "$(sh $D toggle g:media >/dev/null; grp media)" +is "group empty -> all on" "3/3" \ + "$(sh $D toggle g:media >/dev/null; grp media)" +is "bulk, mixed set -> all on" "3/3" \ + "$(sh $D toggle p:gpu/cuda-toolkit >/dev/null + sh $D toggle p:gpu/nvidia-driver p:gpu/cuda-toolkit p:gpu/container-toolkit >/dev/null + grp gpu)" +is "bulk, all on -> all off" "0/3" \ + "$(sh $D toggle p:gpu/nvidia-driver p:gpu/cuda-toolkit p:gpu/container-toolkit >/dev/null + grp gpu)" + +printf '\n\033[1mdependencies — @needs auto-ticks, both directions\033[0m\n' +on() { grep -qxF "$1" state/selected && echo on || echo off; } +reset; sh $D toggle p:networking/xrdp >/dev/null +is "ticking xrdp pulls in the desktop group" "2/2" "$(grp desktop)" +sh $D toggle g:desktop >/dev/null +is "unticking desktop drops xrdp" "off" "$(on networking/xrdp)" +is "unticking desktop drops xorgxrdp" "off" "$(on networking/xorgxrdp)" +reset; sh $D toggle p:gpu/container-toolkit >/dev/null +is "container-toolkit pulls in docker" "3/3" "$(grp docker)" +reset; sh $D toggle p:core/node >/dev/null +is "dropping node drops codex" "off" "$(on agents/codex)" +is "dropping node drops mermaid-cli" "off" "$(on core/mermaid-cli)" +is "…and transitively drops neovim" "off" "$(on core/neovim)" +is "but leaves unrelated packages be" "on" "$(on core/ripgrep)" +# The directives must not leak into package parsing. +is "@needs lines are not packages" "" \ + "$(rows | awk '$1!="BOX" && $3 ~ /@/ {print $3}')" +is "group count unchanged by directives" "11" \ + "$(awk -F'\t' '!/^[#@]/ && NF>=3 {print $1}' "$M" | uniq | grep -c .)" +# @spec is the second directive kind and must be as invisible as the first. +is "@spec lines are not packages" "0" \ + "$(sh $D render | cut -f2 | grep -c 'spec' || true)" +reset; sh $D toggle p:core/uv >/dev/null +is "dropping uv drops specify-cli" "off" "$(on agents/specify-cli)" + +printf '\n\033[1mrisk model — the invariants that matter\033[0m\n' +reset +is "defaults tick nothing invasive" "" "$(picked invasive)" +is "defaults tick nothing private" "" "$(picked private)" +is "defaults tick every safe package" "" "$(missed safe)" + +# A group must never render a flag milder than something inside it. +bad=$(rows | awk '$1!="BOX" && $2=="BOX" {print $3, $5}' \ + | while read -r g f; do + worst=$(awk -F'\t' -v g="$g" '!/^#/ && !/^@/ && NF>=3 && $1==g { + s=($3=="invasive")?3:($3=="private")?2:($3=="gui")?1:0 + if (s>m) m=s } END{ print (m==3)?"invasive":(m==2)?"private":(m==1)?"gui":"safe" }' "$M") + [ "$f" = "$worst" ] || echo "$g shows $f, worst child is $worst" + done; :) +is "no group looks safer than its contents" "" "$bad" + +printf '\n\033[1mmatch confinement — regression for the ^t bug\033[0m\n' +if command -v fzf >/dev/null; then + sh $D expand-all >/dev/null # every package row visible + grep -q gpu/nvidia-driver <<-EOF || sh $D expand-all >/dev/null + $(sh $D render | cut -f2) + EOF + rows=$(sh $D render | sed 's/\x1b\[[0-9;]*m//g') + hit=$(printf '%s\n' "$rows" | fzf --exact --filter=nvidia --no-sort | cut -f2 | sort) + is "--exact: 'nvidia' hits only gpu packages" \ + "p:gpu/container-toolkit +p:gpu/cuda-toolkit +p:gpu/nvidia-driver" "$hit" + # The bug: without --exact this also matches docker, networking and desktop, + # and ^t silently switched all three on. + loose=$(printf '%s\n' "$rows" | fzf --filter=nvidia --no-sort | cut -f2 \ + | grep -cv '^p:gpu/' || true) + if [ "$loose" -gt 0 ]; then + ok "fuzzy would over-match $loose non-gpu rows (why --exact is required)" + else + no "fuzzy no longer over-matches — re-check whether --exact is still load-bearing" + fi + is "the fzf invocation carries --exact" "1" \ + "$(grep -c '\--ansi --exact' $D)" + + # Every action the picker binds must exist on THIS fzf. Ubuntu 24.04 ships + # 0.44.1; a dev box may be 28 releases ahead. fzf validates binds at + # startup, and empty stdin makes it exit without needing a terminal. + badbind=$(sed -n 's/.*--bind "\([^"]*\)".*/\1/p; s/.*--bind '"'"'\([^'"'"']*\)'"'"'.*/\1/p' $D \ + | sed "s|\$SELF|$D|g" \ + | while read -r b; do + out=$(fzf --bind "$b" &1 || true) + case $out in *nvalid*|*nknown*|*nsupported*) echo "$b" ;; esac + done; :) + is "every binding parses on fzf $(fzf --version | cut -d' ' -f1)" "" "$badbind" +else + printf ' \033[33mskip\033[0m fzf not installed\n' +fi + +printf '\n\033[1mpreflight — the picker brings its own fzf\033[0m\n' +vc() { sh $D vercmp "$1" "$2" && echo ge || echo lt; } +is "0.44.1 clears the floor" "ge" "$(vc 0.44.1 0.44.0)" +is "0.29 (jammy) does not" "lt" "$(vc 0.29 0.44.0)" +is "0.38 (bookworm) does not" "lt" "$(vc 0.38.0 0.44.0)" +is "compare is numeric, not lexical" "ge" "$(vc 0.100.0 0.44.0)" +is "0.9.5 is not above 0.44.0" "lt" "$(vc 0.9.5 0.44.0)" +is "preflight resolves a fzf above the floor" "ge" \ + "$(vc "$(sh $D fzf-path | xargs -r -I{} sh -c '{} --version' | awk '{print $1}')" 0.44.0)" +# The picker must call its resolved binary, never whatever PATH happens to hold. +is "picker invokes the resolved binary" "1" \ + "$(grep -c 'cmd_render | "\$FZF"' $D)" +is "picker never calls bare fzf" "0" \ + "$(grep -c 'render | fzf ' $D)" +# The whole point of the cache: PATH and ~/.local/bin are left alone. +is "preflight writes nothing to ~/.local/bin" "absent" \ + "$(sh $D preflight >/dev/null 2>&1; [ -e "$HOME/.local/bin/fzf" ] && echo present || echo absent)" +is "preflight does not export PATH" "0" \ + "$(grep -c 'PATH=.*export PATH' $D)" +# fzf is still offered as a normal package — for your shell, not for the picker. +is "manifest still offers fzf to you" "core/fzf" \ + "$(awk -F'\t' '!/^#/ && !/^@/ && NF>=3 && $2=="fzf" {print $1"/"$2}' "$M")" + +printf '\n\033[1mplatform resolution\033[0m\n' +is "brew-only pkg resolves on linux (omp)" "" \ + "$(sh $D expand g:agents >/dev/null + sh $D render | sed 's/\x1b\[[0-9;]*m//g' | grep 'omp' | grep -o 'unavailable' || true)" + +# One place decides what installs a package; everything downstream reads it. +rs() { sh $D resolve "$1" | tr '\t' ' '; } +is "plain apt name resolves to apt" "apt sox" "$(rs media/sox)" +is "no apt package falls through to brew" "brew lazygit" "$(rs core/lazygit)" +is "tap-only pkg falls through to brew" "brew can1357/tap/omp" "$(rs agents/omp)" +is "special channel carries its @spec" "npm @openai/codex" "$(rs agents/codex)" +is "…and the scoped mermaid name" "npm @mermaid-js/mermaid-cli" "$(rs core/mermaid-cli)" +is "deb channel carries a source" "deb gh:mkasberg/ghostty-ubuntu:_amd64.deb" "$(rs apps/ghostty)" +is "snap channel renames to the binary" "snap bw" "$(rs core/bitwarden-cli)" +is "tarball is dispatched, not named" "tarball neovim" "$(rs core/neovim)" +is "an unknown key says so" "missing" "$(rs nope/nope | tr -d ' ')" +# The macOS half of the manifest is exercised nowhere else. Resolution is pure +# data, so a fake `uname` is enough to check it — and it caught a real bug: the +# apt/brew fallback used to run both ways, so a Linux-only package resolved to +# `apt install davfs2` on a machine that has never had apt. +DARWIN=$PWD/fakeuname +drs() { PATH="$DARWIN:$PATH" sh $D resolve "$1" | tr '\t' ' '; } +is "darwin: tap-only pkg is a brew tap" "brew can1357/tap/omp" "$(drs agents/omp)" +is "darwin: zsh is built in" "builtin zsh" "$(drs core/zsh)" +is "darwin: no apt package is not a reason to try apt" "unavailable" \ + "$(drs networking/davfs2 | tr -d ' ')" + +# Nothing in the default set may be unresolvable — that is a manifest bug, and +# it is silent until someone runs the installer on a fresh machine. +reset +is "every default package resolves" "" \ + "$(sh $D plan 2>/dev/null | sed -n '/no source on this platform/,$p' | grep -v 'no source' || true)" + +printf '\n\033[1mthe installer — driven against fake package managers\033[0m\n' +FAKE=$PWD/fakebin +export DOTUP_TEST_LOG=$PWD/state/calls.log + +# --print resolves everything and must call nothing at all. The fakes are first +# on PATH, so any call whatsoever leaves a trace. +reset +: > "$DOTUP_TEST_LOG" +out=$(PATH="$FAKE:$PATH" sh $D --unattended --print 2>&1 || true) +is "--print calls no package manager" "0" "$(grep -c . "$DOTUP_TEST_LOG" || true)" +has "--print still shows the apt batch" "apt-get install -y" "$out" +has "--print shows the npm batch" "npm install -g" "$out" +hasnt "--print never reaches the private stage" "Bootstrap URL:" "$out" + +# Now the real engine, over a subset chosen to exercise every non-destructive +# channel. Deliberately not the whole default set: the tarball and script +# handlers write outside $HOME, and a test suite has no business doing that. +reset +sh $D preset none +cat > state/selected <<'EOF' +agents/omp +agents/specify-cli +apps/obsidian +core/bitwarden-cli +core/gh +core/lazygit +core/mermaid-cli +core/ripgrep +EOF +: > "$DOTUP_TEST_LOG" +PATH="$FAKE:$PATH" sh $D install >/dev/null 2>&1 || true +log=$(cat "$DOTUP_TEST_LOG") +has "apt batches its packages in one call" "apt-get install -y ripgrep" "$log" +has "brew gets the tap-only package" "brew install can1357/tap/omp" "$log" +has "brew gets the apt-less package" "brew install lazygit" "$log" +has "npm gets the scoped spec" "@mermaid-js/mermaid-cli" "$log" +has "uv installs the tool, not the package" "uv tool install specify-cli" "$log" +has "snap gets the renamed binary" "snap install bw" "$log" +has "flatpak gets the app id" "flatpak install" "$log" +has "…with the real obsidian app id" "md.obsidian.Obsidian" "$log" +# The one that matters on Linux: apt not knowing a name is not a dead end. +has "apt probes before it batches" "apt-cache show gh" "$log" +has "an apt name apt rejects moves to brew" "brew install gh" "$log" +hasnt "…and is not left in the apt batch" "apt-get install -y gh" "$log" +# npm cannot run before node, uv cannot run before uv. The pipeline is fixed +# rather than sorted, so assert the order it actually produces. +is "apt runs before npm" "yes" \ + "$(a=$(grep -n 'apt-get install' "$DOTUP_TEST_LOG" | head -1 | cut -d: -f1) + b=$(grep -n 'npm install' "$DOTUP_TEST_LOG" | head -1 | cut -d: -f1) + [ -n "$a" ] && [ -n "$b" ] && [ "$a" -lt "$b" ] && echo yes || echo no)" +# Nothing runs as root that does not have to. +hasnt "brew is never run through sudo" "sudo brew" "$log" + +printf '\n\033[1munattended — the boundary holds because of what is missing\033[0m\n' +# A stale state file is the adversary here: it ticks the two things an +# unattended run must never act on, and the run has to refuse both anyway. +rm -rf state; mkdir -p state +cat > state/selected <<'EOF' +core/ripgrep +docker/docker-ce +networking/openssh-server +private/bws-secrets +private/private-repo +EOF +: > "$DOTUP_TEST_LOG" +out=$(PATH="$FAKE:$PATH" sh $D --unattended --print 2>&1 || true) +hasnt "unattended never installs a private row" "bws-secrets" "$out" +hasnt "unattended never prompts for a password" "Password:" "$out" +hasnt "unattended refuses a stale invasive tick" "docker-ce" "$out" +hasnt "…including a listening ssh port" "openssh-server" "$out" +has "…but still installs the safe defaults" "ripgrep" "$out" +# Determinism: the same command twice, on the same machine, means the same +# thing. A state file left by an interactive run must not change it. +a=$(PATH="$FAKE:$PATH" sh $D --unattended --print 2>/dev/null || true) +sh $D preset none +b=$(PATH="$FAKE:$PATH" sh $D --unattended --print 2>/dev/null || true) +is "unattended is computed, not inherited" "same" \ + "$([ "$a" = "$b" ] && echo same || echo different)" +# `set -e` would kill the subshell at the failing command, so the status is +# captured through a || branch rather than read from $? afterwards. +rc() { c=0; "$@" >/dev/null 2>&1 || c=$?; echo "$c"; } +is "unattended exits clean when nothing fails" "0" \ + "$(PATH="$FAKE:$PATH" rc sh $D --unattended --print)" +is "an unknown flag is refused" "2" "$(rc sh $D --nonsense)" +# The negative test in PLAN.md phase 3 runs `zsh -ic exit` after an unattended +# run. That only passes if zsh is in the unattended set. +has "unattended installs the shell it configures" "zsh" \ + "$(reset; PATH="$FAKE:$PATH" sh $D --unattended --print 2>&1 | grep 'apt-get install' || true)" + +printf '\n\033[1mruns where it has to run\033[0m\n' +# A machine with no sudo and no root is a real case -- a locked-down work box, a +# rootless container. `A || { B && C; }` is one || list, so when `command -v +# sudo` failed the whole list failed and `set -e` killed dotup at load, before +# it printed anything. A minimal PATH is the only way to see that. +# Not under state/ -- reset() removes that whole directory, and a PATH that +# vanishes mid-test looks exactly like the bug being tested for. +MIN=${TMPDIR:-/tmp}/dotup-minbin.$$ +rm -rf "$MIN"; mkdir -p "$MIN" +# Resolved by looking, not by `command -v`: an interactive shell can report an +# alias or a bare name for these, and a symlink to a bare name is a loop. +for t in awk grep sed sort cut tr id uname mkdir cp mv rm cat head sh dirname basename; do + for bd in /usr/bin /bin /usr/local/bin; do + [ -x "$bd/$t" ] && { ln -sf "$bd/$t" "$MIN/$t"; break; } + done +done +reset >/dev/null 2>&1 +is "runs as a non-root user with no sudo on PATH" "0" \ + "$(c=0; env -i HOME="$HOME" PATH="$MIN" DOTUP_STATE="$DOTUP_STATE" DOTUP_MANIFEST="$M" \ + "$MIN/sh" $D --unattended --print >/dev/null 2>&1 || c=$? + echo "$c")" +is "…and still resolves the whole default set" "1" \ + "$(env -i HOME="$HOME" PATH="$MIN" DOTUP_STATE="$DOTUP_STATE" DOTUP_MANIFEST="$M" \ + "$MIN/sh" $D --unattended --print 2>/dev/null | grep -c 'apt-get install -y ')" +rm -rf "$MIN" +# `sudo` must not be glued onto anything that is not a system package manager. +is "sudo is only ever used for apt, snap and dpkg" "" \ + "$(grep -oE '\$\{SUDO:\+\$SUDO \}[a-z-]+' $D | sed 's/.*}//' | sort -u \ + | grep -vE '^(apt-get|snap|rm|mkdir|tar|ln)$' || true)" + +printf '\n\033[1minteractive loop (needs a pty)\033[0m\n' +if command -v fzf >/dev/null && command -v curl >/dev/null && command -v script >/dev/null; then + out=$(sh ./listen-test.sh 2>/dev/null | tr -d '\r' || true) + case $out in + *CURSOR_HELD*) ok "cursor survives toggle+reload" ;; + *) no "cursor position lost across reload — rapid ticking breaks" ;; + esac +else + printf ' \033[33mskip\033[0m needs fzf, curl and script\n' +fi + +printf '\n%d passed, %d failed\n\n' "$pass" "$fail" +[ "$fail" -eq 0 ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..30ca75b --- /dev/null +++ b/README.md @@ -0,0 +1,218 @@ +# dotfiles — public tier + +Everything a machine needs to become a working machine, and nothing that makes +it *mine*. Clone it anonymously, apply it to a throwaway VM, and you get the +editor, the shell, the terminal and the tools. You do not get my name, my email, +my hosts, my agent config, or any credential — because none of that is in here. + +The other half lives in a private repo you cannot clone without a password. That +split is the whole design, and it is structural rather than a policy: the gate +is *which repo a machine can obtain*, not a `.chezmoiignore` rule that could be +misconfigured once and leak. + +Why the repo is shaped this way: **`DESIGN.md`**. How it got here: **`PLAN.md`**. +Both live in the working repo, not this one. + +--- + +## Install + +```sh +sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply +~/.local/bin/dotup +``` + +Two commands. The first lays down the files and clones the six externals; the +second picks and installs the software. There is no third step, and nothing +here prompts for anything. + +For a machine with nobody at the keyboard: + +```sh +dotup --unattended # safe defaults, no UI, no prompt, no private tier +dotup --print # resolve everything and print the commands, install nothing +``` + +## What lands where + +| Path | What | +|---|---| +| `~/.zshrc` | Plain file, no template, no secrets. One guarded `source` is the entire secret surface. | +| `~/.zshenv`, `~/.p10k.zsh` | Shell environment and prompt | +| `~/.config/nvim/**` | LazyVim, 36 files. `lua/plugins/image.lua` is the fragile one — see DESIGN §3 before touching it. | +| `~/.config/{gh,ghostty,git,btop,herdr}` | Terminal and tooling config | +| `~/.tmux.conf` → `~/.tmux/.tmux.conf` | Symlink into the gpakosz external | +| `~/.gitconfig` | No `[user]`. See "the seams" below. | +| `~/.local/bin/{dotup,sysjournal}` | The installer and the journal CLI | +| `~/.local/share/dotup/packages.tsv` | The manifest `dotup` reads | + +## The seams + +Three files this tier *reads* and never writes. Each is absent on a public-only +machine, and each absence is a silent no-op rather than an error. + +| File | Written by | Carries | +|---|---|---| +| `~/.config/git/config.local` | private tier | `[user]`, signing key, Gitea host rewrites | +| `~/.config/zsh/local.zsh` | private tier | aliases naming hosts you own | +| `~/.config/zsh/secrets.zsh` | `dotsecrets`, from bws | the seven API keys, mode 600, in no repo | + +If `git commit` says it does not know who you are, the private tier has not been +applied. That is the correct complaint, not a bug. + +## Externals + +Six `git-repo` externals, none of which were declared anywhere before this repo +existed. That omission is why a clean bootstrap used to produce a broken login +shell: `.zshrc` sourced `$ZSH/oh-my-zsh.sh` with nothing having installed it. + +`.tmux`, oh-my-zsh, powerlevel10k, zsh-autosuggestions, zsh-ai, tpm. The theme +and both plugins are nested under `.oh-my-zsh/custom/`, which is what +`$ZSH_CUSTOM` resolves to; chezmoi processes externals in path order, so the +parent is cloned first. + +## dotup + +One screen. Groups and packages are the same kind of row, both carry a tick box, +and every group opens into the packages underneath it. Group boxes are a +tri-state summary of their children, never an independent switch. + +``` + ▸ [x] core 31/31 safe + ▾ [~] media 2/3 safe + [x] ffmpeg apt ffmpeg + [ ] sox apt sox + [x] p7zip apt p7zip-full + ▸ [ ] gpu 0/3 invasive kernel modules; a bad driver can bre… + ▸ [ ] private 0/2 private one password, typed after the install… + + space tick tab open ^t tick all shown ^a defaults ^x none enter install +``` + +**One toggle rule, everywhere.** Expand the row to the packages it covers; if +every one is on, turn them all off, otherwise turn them all on. That single rule +covers a group row, a package row, and a bulk toggle over a filtered set — type +`nvidia`, press `^t`, and exactly the three rows you can see flip. + +**The risk model.** `safe` is pre-ticked; `gui` is pre-ticked only where there +is a display; `invasive` is never ticked for you, and its reason is on the row; +`private` needs a password you type after the install finishes. A group's flag +is derived as the worst flag among its children, so a group can never look +safer than something inside it. + +**Dependencies tick themselves.** `@needs` closure runs in both directions: +ticking `xrdp` ticks the `desktop` group it is useless without, and unticking +`core/node` drops `codex`, `pi`, `mermaid-cli` and then `neovim`. The closure is +shown, not described — the counts move on the same keystroke. + +### The manifest + +Five tab-separated columns, greppable by hand. Anything that would have been a +sixth column is an `@` directive instead: + +```tsv +@needs core/neovim core/imagemagick core/mermaid-cli +@spec agents/codex @openai/codex +core neovim safe -tarball neovim apt ships 0.9.5 — tarball to /opt/nvim +core fd safe fd-find fd binary is fdfind on ubuntu +``` + +A leading `-` in the apt or brew column means "not from this package manager": +`-tarball -npm -script -snap -deb -flatpak -uv -builtin -xcode`. A bare `-` +means unavailable there — and **Linux resolves apt-then-brew, not +apt-instead-of-brew**, because `omp`, `herdr` and `lazygit` have no apt package +at all. The installer goes further and probes `apt-cache` before it batches: a +name apt does not know moves to the brew column rather than failing the whole +batch. + +`@spec` carries the argument a channel needs but the package name cannot supply +— the scoped npm name, the flatpak app id, the `.deb` source. It defaults to the +package name. + +### Where fzf comes from + +The picker cannot get fzf from the manifest: it needs fzf to *draw* the +manifest. So it brings its own copy into `~/.cache/dotup/` and invokes it by +absolute path. **`PATH` is never modified and `~/.local/bin` is never written** +— a tool that refuses to tick `invasive` for you has no business quietly +shadowing your distro's fzf for Ctrl-R and the oh-my-zsh plugin. Your own fzf +wins whenever it clears the floor; nothing is replaced merely for being old. + +Floor is 0.44.0, which is exactly where verification stops rather than where +compatibility breaks: every release from 0.29 up parses every binding used here, +but cursor-on-reload can only be *measured* from 0.44.1, where fzf's `--listen` +API began reporting state. + +### The private tier + +Ticking a `private` row schedules work, it does not do it — `git`, `chezmoi` and +`bws` have to exist first, and a password typed at picker time would sit in +memory through ten minutes of package downloads. So it happens at the end, and +never without a human: + +``` +prompt: URL · username · password the address is in no repo + │ + ├── blank ─▶ done. public-only machine. nothing was asked for. + ├── 401 ─▶ say so, leave the machine public-only + └── 200 ─▶ two-line blob ─┬─ private repo → chezmoi init --apply + └─ bws token → ~/.config/bitwarden/bws-token (600) +``` + +The endpoint returns **data, never a script**: two `KEY=VALUE` lines, +`PRIVATE_REPO_URL=` and `BWS_ACCESS_TOKEN=`. Compromising it discloses two +revocable read-only credentials rather than executing arbitrary code as you on +every machine you ever build. The password is spent, never stored, and never +reaches `argv` — `curl -K -` reads it from stdin, so it never reaches `ps` +either. + +`dotup --unattended` cannot reach any of this. It has nobody to type a password, +and it refuses `private` and `invasive` rows outright even if a stale state file +ticks them. + +## Tests + +```sh +sh .tests/test.sh # here +sh .tests/test.sh --docker # clean ubuntu:24.04 +DOTUP_TEST_IMAGE=ubuntu:22.04 sh .tests/test.sh --docker +DOTUP_TEST_IMAGE=debian:12 sh .tests/test.sh --docker +``` + +81 assertions covering the toggle rule, the `@needs` closure both ways, the risk +model as invariants rather than prose, match confinement (two-sided: `--exact` +confines `nvidia` to three rows **and** fuzzy still over-matches, so removing +`--exact` fails loudly), the fzf preflight, channel resolution on both +platforms, the installer, and that dotup runs at all as a non-root user with no +`sudo` on `PATH`. + +The installer is driven against a directory of fake package managers that record +what they were asked to do and install nothing, so the engine is exercised end +to end without a single package landing on the machine running the suite. + +The version spread is the point of the containers — this box runs fzf 0.72, +24.04 ships 0.44.1, 22.04 ships 0.29 and bookworm 0.38. + +The macOS half of the manifest has no CI anywhere, so a fake `uname` stands in +for it. That is not theatre: it caught the apt/brew fallback running in both +directions, which resolved Linux-only packages to `apt install davfs2` on a +machine that has never had apt. The fallback is one-directional now. + +## Rules this repo lives by + +- **Zero credentials, forever.** `gitleaks detect` runs over the full history + with no allowlist. The private repo gets exactly one allowlisted path; this one + gets none, and that asymmetry is the point. +- **Grep the files this repo owns, not `$HOME`.** A wholesale scan of a finished + machine is not a test of this repo: `~/.npm-global` ends up holding an AWS SDK + whose type definitions contain a literal `AKIA…` and an Anthropic SDK whose + docs contain `ghp_…`. Scope the scan to `chezmoi managed`, and tighten `sk-` to + `sk-[A-Za-z0-9_-]{20,}` — the bare substring matches `ask-`, `task-`, `risk-` + and `disk-`, and one of this manifest's own npm packages is called + `rpiv-ask-user-question`. +- **No identity.** No name, email, hostname, private IP or absolute home path. + If something needs one, it belongs behind a seam. +- **`.chezmoiignore` is gitignore syntax.** A `#` mid-line becomes part of the + pattern and silently matches nothing. Every comment goes on its own line. +- **Nothing invasive without an action of yours.** Not "never", since a `@needs` + edge can pull one in — but never silently, and never unattended. diff --git a/dot_gitconfig.tmpl b/dot_gitconfig.tmpl new file mode 100644 index 0000000..653b8bd --- /dev/null +++ b/dot_gitconfig.tmpl @@ -0,0 +1,26 @@ +; ~/.gitconfig — PUBLIC tier. +; +; Nothing here identifies you. Your name, your email, your signing key and the +; Gitea host rewrites are identity, not configuration, so they live in the +; private tier — which writes ~/.config/git/config.local. The include below is +; the seam between the two. Git treats a missing include as a no-op, so a +; public-only machine reads this file and stops. +; +; Do not add [user] here. If `git commit` complains that it does not know who +; you are, the private tier has not been applied, and that is the correct +; complaint. + +[init] + defaultBranch = main + +[filter "lfs"] + clean = git-lfs clean -- %f + smudge = git-lfs smudge -- %f + process = git-lfs filter-process + required = true +{{ if eq .chezmoi.os "darwin" }} +[credential] + helper = manager +{{ end }} +[include] + path = ~/.config/git/config.local diff --git a/dot_local/bin/executable_dotup b/dot_local/bin/executable_dotup new file mode 100755 index 0000000..735b49b --- /dev/null +++ b/dot_local/bin/executable_dotup @@ -0,0 +1,893 @@ +#!/bin/sh +# dotup — the front door. One command from a bare machine to a finished one. +# +# It is a picker and an installer, not a picker next to an installer. Tick boxes +# on groups AND on individual packages; one toggle rule everywhere: expand the +# row to its leaves, and if every leaf is on turn them all off, otherwise turn +# them all on. That single rule covers a group row, a package row, and a bulk +# toggle over a filtered set. +# +# dotup the picker, then install what you ticked +# dotup --unattended no UI: safe defaults, never prompts, never private +# dotup --print resolve and print every command, install nothing +# +# POSIX sh on purpose: this runs on a bare box before anything is installed, +# and macOS still ships bash 3.2 (no associative arrays). + +set -eu + +SELF=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/$(basename -- "$0") +HERE=$(dirname -- "$SELF") +STATE=${DOTUP_STATE:-${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles} +SEL=$STATE/selected +EXP=$STATE/expanded + +# The manifest is data, not a script, so it does not live in bin/. Checked in +# the development layout first so the repo's own test suite and a checkout both +# work without setting anything. +if [ -n "${DOTUP_MANIFEST:-}" ]; then MANIFEST=$DOTUP_MANIFEST +elif [ -f "$HERE/packages.tsv" ]; then MANIFEST=$HERE/packages.tsv +else MANIFEST=${XDG_DATA_HOME:-$HOME/.local/share}/dotup/packages.tsv +fi + +[ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] && HAS_DISPLAY=1 || HAS_DISPLAY=0 +case $(uname -s) in Darwin) PLAT=brew ;; *) PLAT=apt ;; esac + +UNATTENDED=0 +DRYRUN=0 +ASSUME_YES=0 + +mkdir -p "$STATE" +[ -f "$SEL" ] || : > "$SEL" +[ -f "$EXP" ] || : > "$EXP" + +R=''; DIM=''; B=''; GRN=''; YEL=''; RED='' +if [ -t 1 ]; then + R=$(printf '\033[0m'); DIM=$(printf '\033[2m'); B=$(printf '\033[1m') + GRN=$(printf '\033[32m'); YEL=$(printf '\033[33m'); RED=$(printf '\033[31m') +fi +say() { printf '%s\n' "$*" >&2; } +warn() { printf '%s!%s %s\n' "$YEL" "$R" "$*" >&2; } +err() { printf '%s✗%s %s\n' "$RED" "$R" "$*" >&2; } +head_() { printf '\n%s%s%s\n' "$B" "$*" "$R" >&2; } + +# ---------------------------------------------------------------- leaves ---- +# Expand a row key to the package keys it covers. +# g:agents -> agents/codex agents/pi ... +# p:agents/pi -> agents/pi +leaves() { + for key in "$@"; do + case $key in + g:*) grp=${key#g:} + awk -F'\t' -v g="$grp" '!/^[#@]/ && NF>=3 && $1==g {print $1"/"$2}' "$MANIFEST" ;; + p:*) printf '%s\n' "${key#p:}" ;; + esac + done +} + +# ------------------------------------------------------------------ deps ---- +# Dependencies are @needs directives, not a sixth column: the TSV stays five +# wide and greppable, and a package with no dependencies costs nothing to read. +# +# @needs [dep ...] dep is group/pkg or a whole group +# +# There is no "requires" state to display. Ticking a box ticks what it needs; +# unticking one unticks what needed it. The counts on screen move as it happens, +# so the closure is visible rather than described. + +# stdin: keys -> stdout: those keys plus everything they need, transitively. +expand_deps() { + work=$(sort -u); prev= + while [ "$work" != "$prev" ]; do + prev=$work + add=$(printf '%s\n' "$work" | while read -r k; do + [ -n "$k" ] || continue + awk -F'\t' -v k="$k" '$1=="@needs" && $2==k {print $3}' "$MANIFEST" + done | tr ' ' '\n' | while read -r d; do + [ -n "$d" ] || continue + case $d in + */*) printf '%s\n' "$d" ;; + *) awk -F'\t' -v g="$d" '!/^[#@]/ && NF>=3 && $1==g {print $1"/"$2}' "$MANIFEST" ;; + esac + done) + work=$(printf '%s\n%s\n' "$work" "$add" | grep . | sort -u) + done + printf '%s\n' "$work" +} + +# stdin: keys -> stdout: those keys plus everything that needs them. +expand_rdeps() { + work=$(sort -u); prev= + while [ "$work" != "$prev" ]; do + prev=$work + add=$(printf '%s\n' "$work" | while read -r k; do + [ -n "$k" ] || continue + awk -F'\t' -v k="$k" -v g="${k%%/*}" '$1=="@needs"{ + n=split($3, d, " ") + for(i=1;i<=n;i++) if (d[i]==k || d[i]==g) print $2 }' "$MANIFEST" + done) + work=$(printf '%s\n%s\n' "$work" "$add" | grep . | sort -u) + done + printf '%s\n' "$work" +} + +# ---------------------------------------------------------------- toggle ---- +# All on -> all off. Anything else -> all on. +cmd_toggle() { + want=$(leaves "$@" | sort -u) + [ -n "$want" ] || return 0 + all_on=1 + for k in $want; do grep -qxF "$k" "$SEL" || { all_on=0; break; }; done + # Decide direction on what you touched, then widen along the dependency + # edges: switching on pulls in what it needs, switching off drops what + # needed it. Either way the ticks never describe a broken machine. + if [ "$all_on" -eq 1 ]; then want=$(printf '%s\n' "$want" | expand_rdeps) + else want=$(printf '%s\n' "$want" | expand_deps); fi + tmp=$STATE/.sel.$$ + grep -vxF -f - "$SEL" > "$tmp" <<-EOF || : + $want + EOF + if [ "$all_on" -eq 0 ]; then printf '%s\n' "$want" >> "$tmp"; fi + sort -u "$tmp" > "$SEL" && rm -f "$tmp" +} + +cmd_expand() { + for key in "$@"; do + case $key in g:*) g=${key#g:} ;; p:*) g=${key#p:}; g=${g%%/*} ;; *) continue ;; esac + tmp=$STATE/.exp.$$ + if grep -qxF "$g" "$EXP"; then grep -vxF "$g" "$EXP" > "$tmp" || : + else cp "$EXP" "$tmp"; printf '%s\n' "$g" >> "$tmp"; fi + mv "$tmp" "$EXP" + done +} + +cmd_expand_all() { + if [ -s "$EXP" ]; then : > "$EXP" + else awk -F'\t' '!/^[#@]/ && NF>=3 {print $1}' "$MANIFEST" | awk '!seen[$0]++' > "$EXP"; fi +} + +# --------------------------------------------------------------- presets ---- +cmd_preset() { + case ${1:-defaults} in + none) : > "$SEL" ;; + safe) awk -F'\t' '!/^[#@]/ && NF>=3 && $3=="safe" {print $1"/"$2}' "$MANIFEST" > "$SEL" ;; + defaults) + awk -F'\t' -v d="$HAS_DISPLAY" \ + '!/^[#@]/ && NF>=3 && ($3=="safe" || ($3=="gui" && d==1)) {print $1"/"$2}' \ + "$MANIFEST" > "$SEL" ;; + esac +} + +# ---------------------------------------------------------------- render ---- +cmd_render() { + awk -F'\t' -v selfile="$SEL" -v expfile="$EXP" -v plat="$PLAT" -v disp="$HAS_DISPLAY" ' + function sev(f){ return f=="invasive"?3 : f=="private"?2 : f=="gui"?1 : 0 } + function lbl(s){ return s==3?"invasive" : s==2?"private" : s==1?"gui" : "safe" } + function col(s){ return s==3?RED : s==2?MAG : s==1?CYA : GRN } + # Linux is apt-then-brew, not apt-instead-of-brew: all three machines run + # linuxbrew, and omp/herdr/lazygit exist only as taps. + # The fallback runs one way only. Linux is apt-then-brew because all three + # machines run linuxbrew and omp/herdr/lazygit exist only as taps. There is + # no apt on a Mac, so a bare `-` in the brew column means unavailable — the + # other direction would offer to `apt install davfs2` on macOS. + function src(a,b, v,p){ if (plat=="brew") { v=b; p="brew" } + else { v=a; p="apt"; if (v=="-") { v=b; p="brew" } } + if (v=="-") return "unavailable" + if (substr(v,1,1)=="-") return substr(v,2) + return p" "v } + function cut(s,n){ return length(s)>n ? substr(s,1,n-1) "\342\200\246" : s } + BEGIN{ + R="\033[0m"; DIM="\033[2m"; B="\033[1m" + GRN="\033[32m"; YEL="\033[33m"; RED="\033[31m"; CYA="\033[36m"; MAG="\033[35m" + while((getline l < selfile) > 0) sel[l]=1 + while((getline l < expfile) > 0) expd[l]=1 + } + # @groupnote — a group whose members fail the safe test for several + # different reasons needs its own line. Deriving it from the first child + # would show "opens port 22" on a group that also mounts setuid helpers. + $1=="@needs" || $1=="@spec" { next } + /^@/ { gnote[substr($1,2)] = $2; next } + /^#/ || NF<3 { next } + { + g=$1; p=$2; f=$3; key=g"/"p + n++; G[n]=g; P[n]=p; F[n]=f; A[n]=$4; Bc[n]=$5; NT[n]=$6 + if (!(g in seen)) { seen[g]=1; order[++ng]=g } + tot[g]++ + if (key in sel) { selc[g]++; on[n]=1 } + if (sev(f) > worst[g]) worst[g]=sev(f) + if (f=="invasive" || f=="private") why[g]=($6!="" && why[g]=="") ? $6 : why[g] + } + END{ + for (i=1; i<=ng; i++) { + g=order[i]; s=worst[g]; c=selc[g]+0; t=tot[g] + mark = (c==t) ? "x" : (c>0 ? "~" : " ") + mc = (c==t) ? GRN : (c>0 ? YEL : DIM) + arrow = (g in expd) ? "\342\226\276" : "\342\226\270" + reason = (g in gnote) ? gnote[g] : why[g] + note = (s>=2 && reason!="") ? " " col(s) reason R : "" + printf "%s %s[%s]%s %s%-13s%s %s%5s%s %s%-8s%s%s\t%s\n", + arrow, mc, mark, R, B, g, R, DIM, c"/"t, R, col(s), lbl(s), R, cut(note,44), "g:" g + if (!(g in expd)) continue + for (j=1; j<=n; j++) { + if (G[j]!=g) continue + m = (j in on) ? "x" : " " + mc = (j in on) ? GRN : DIM + d = src(A[j], Bc[j]) + if (NT[j]!="") d = d " \302\267 " NT[j] + printf " %s[%s]%s %-18s %s%s%s\t%s\n", + mc, m, R, P[j], DIM, cut(d,46), R, "p:" g "/" P[j] + } + } + }' "$MANIFEST" +} + +# --------------------------------------------------------------- explain ---- +cmd_explain() { + key=${1:-} + awk -F'\t' -v key="$key" -v selfile="$SEL" -v plat="$PLAT" ' + function src(a,b, v,p){ if (plat=="brew") { v=b; p="brew" } + else { v=a; p="apt"; if (v=="-") { v=b; p="brew" } } + if (v=="-") return "not available" + if (substr(v,1,1)=="-") return substr(v,2) + return p " install " v } + BEGIN{ + R="\033[0m"; DIM="\033[2m"; B="\033[1m"; RED="\033[31m"; GRN="\033[32m" + while((getline l < selfile) > 0) sel[l]=1 + split(key, kk, ":"); kind=kk[1]; rest=substr(key, index(key,":")+1) + if (kind=="g") { wantg=rest } else { split(rest, pp, "/"); wantg=pp[1]; wantp=pp[2] } + } + # A key may carry several @needs lines; keep them all, not just the last. + $1=="@needs" { if ($2==rest) needs = needs (needs==""?"":" ") $3; next } + $1=="@spec" { if ($2==rest) spec=$3; next } + /^@/ { if (substr($1,2)==wantg) gnote=$2; next } + /^#/ || NF<3 { next } + $1==wantg { + if (wantp=="" ) { + t++; if (($1"/"$2) in sel) c++ + list = list sprintf(" %s %s\n", (($1"/"$2) in sel)?GRN "\342\234\223" R:DIM "\342\227\246" R, $2) + if ($6!="" && flagnote=="") flagnote=$6 + f=$3 + } else if ($2==wantp) { + printf "%s%s%s\n\n", B, $2, R + printf "%sgroup%s %s\n", DIM, R, $1 + printf "%srisk%s %s\n", DIM, R, $3 + printf "%sapt%s %s\n", DIM, R, ($4=="-"?"not available":$4) + printf "%sbrew%s %s\n", DIM, R, ($5=="-"?"not available":$5) + printf "%sinstall%s %s\n", DIM, R, src($4,$5) + if ($6!="") printf "\n%s\n", $6 + done=1 + } + } + END{ + if (done) { + if (spec!="") printf "\n%sspec%s %s\n", DIM, R, spec + if (needs!="") printf "\n%sneeds%s %s%s\n", DIM, R, needs, DIM " (ticked automatically)" R + printf "\n%sstate%s %s\n", DIM, R, (key_in_sel()) ? GRN "selected" R : DIM "not selected" R + exit + } + printf "%s%s%s %s%d of %d selected%s\n\n", B, wantg, R, DIM, c, t, R + printf "%s\n", list + if (f=="invasive") printf "%sinvasive%s %s\n", RED, R, (gnote!="" ? gnote : flagnote) + if (f=="private") printf "%s\n", (gnote!="" ? gnote : "one password, typed after the install finishes") + } + function key_in_sel(){ return (rest in sel) } + ' "$MANIFEST" +} + +# ------------------------------------------------------------- resolution ---- +# One place decides, for a selected package on this platform, what actually +# installs it. Everything downstream — the plan, the installer, the dry run — +# reads this and nothing else, so they cannot disagree. +# +# Output: +# apt|brew argument is the package name +# npm|uv|snap|deb|flatpak argument is the @spec, defaulting to the pkg name +# tarball|script|xcode|builtin bespoke by nature; dispatched on the key +# unavailable neither column offers it here +resolve() { + awk -F'\t' -v key="$1" -v plat="$PLAT" ' + $1=="@spec" { if ($2==key) spec=$3; next } + /^[#@]/ || NF<3 { next } + ($1"/"$2)==key { pkg=$2; apt=$4; brw=$5; found=1 } + END{ + if (!found) { print "missing\t"; exit } + # apt-then-brew, not apt-instead-of-brew: omp, herdr and lazygit have no + # apt package at all and install perfectly well from linuxbrew. The + # fallback is one-directional on purpose — there is no apt on a Mac, so + # a bare `-` in the brew column is the end of the road, not a reason to + # go looking in a column that names Debian packages. + if (plat=="brew") { v=brw; p="brew" } + else { v=apt; p="apt"; if (v=="-") { v=brw; p="brew" } } + if (v=="-") { print "unavailable\t"; exit } + if (substr(v,1,1)=="-") { print substr(v,2) "\t" (spec!="" ? spec : pkg); exit } + print p "\t" v + }' "$MANIFEST" +} + +# Every selected key, minus the private rows — those are not packages. +selected_packages() { + awk -F'\t' -v selfile="$SEL" ' + BEGIN{ while((getline l < selfile)>0) sel[l]=1 } + /^[#@]/ || NF<3 { next } + $3=="private" { next } + ($1"/"$2) in sel { print $1"/"$2 }' "$MANIFEST" +} + +selected_private() { + awk -F'\t' -v selfile="$SEL" ' + BEGIN{ while((getline l < selfile)>0) sel[l]=1 } + /^[#@]/ || NF<3 { next } + $3=="private" && (($1"/"$2) in sel) { print $1"/"$2 }' "$MANIFEST" +} + +# Build for everything selected. +plan_table() { + selected_packages | while read -r k; do + [ -n "$k" ] || continue + printf '%s\t%s\n' "$(resolve "$k")" "$k" + done +} + +cmd_plan() { + head_ "plan" + plan_table | sort | awk -F'\t' ' + BEGIN{ R="\033[0m"; B="\033[1m"; DIM="\033[2m"; YEL="\033[33m" } + { ch=$1; arg=$2; key=$3 + if (ch=="unavailable" || ch=="missing") { bad = bad " " key "\n"; nb++; next } + line[ch] = line[ch] " " sprintf("%-28s %s", key, arg) "\n"; n++ } + END{ + for (c in line) printf " %s%s%s\n%s", B, c, R, line[c] + printf "\n %d packages\n", n + if (nb) printf "\n %sno source on this platform (%d):%s\n%s", YEL, nb, R, bad + }' +} + +# --------------------------------------------------------------- installer --- +# Nothing here is clever. It batches what can be batched, refuses what it cannot +# reach, and never lets one bad package sink the other thirty. +# Written as an `if`, not `A || { B && C; }`: that form is one || list, so when +# `command -v sudo` fails the whole list fails and `set -e` exits the script at +# load. On a non-root machine with no sudo, dotup died before printing anything. +SUDO= +if [ "$(id -u)" != 0 ] && command -v sudo >/dev/null 2>&1; then SUDO=sudo; fi +FAILED=$STATE/.failed.$$ +APT_UPDATED=0 + +have() { find_tool "$1" >/dev/null 2>&1; } + +# A tool installed a moment ago is not on this process's PATH: the astral +# installer drops uv in ~/.local/bin, npm -g honours the ~/.npmrc prefix, the go +# tarball lands in /usr/local/go, and linuxbrew lives outside a non-login PATH. +# The fix is to look in the places we just wrote to -- NOT to export a modified +# PATH. A tool that refuses to shadow your fzf has no business rewriting PATH +# for its own convenience either, and a child process's PATH would be a lie the +# moment dotup exits. +find_tool() { + command -v "$1" 2>/dev/null && return 0 + for ft_c in "$HOME/.local/bin/$1" "$HOME/.npm-global/bin/$1" \ + "/usr/local/bin/$1" "/usr/local/go/bin/$1" \ + "/home/linuxbrew/.linuxbrew/bin/$1" "/opt/homebrew/bin/$1"; do + [ -x "$ft_c" ] && { printf '%s\n' "$ft_c"; return 0; } + done + return 1 +} +# Collapse a space-separated list to canonical form; an all-blank list becomes +# empty, so a channel with nothing in it prints no header. +norm() { printf '%s\n' "$*" | tr ' ' '\n' | grep . | tr '\n' ' ' | sed 's/ $//'; } + +run() { + if [ "$DRYRUN" -eq 1 ]; then printf ' + %s\n' "$*"; return 0; fi + printf '%s + %s%s\n' "$DIM" "$*" "$R" >&2 + "$@" +} +run_sh() { + if [ "$DRYRUN" -eq 1 ]; then printf ' + %s\n' "$1"; return 0; fi + printf '%s + %s%s\n' "$DIM" "$1" "$R" >&2 + sh -c "$1" +} +note_fail() { printf '%s\t%s\n' "$1" "$2" >> "$FAILED"; err "$1: $2"; } + +apt_update_once() { + [ "$APT_UPDATED" -eq 0 ] || return 0 + APT_UPDATED=1 + run_sh "${SUDO:+$SUDO }apt-get update -qq" || warn "apt-get update failed; continuing with stale lists" +} + +# apt refuses the whole batch when one name is unknown, so ask first. A name apt +# does not know is not a dead end: if the brew column offers it, it moves there. +# That is what "apt-then-brew" has to mean in practice, and it is the difference +# between 34 packages installed and 0. +apt_known() { apt-cache show "$1" >/dev/null 2>&1; } + +install_apt() { + pkgs=$(norm "$1") + [ -n "$pkgs" ] || return 0 + head_ "apt" + apt_update_once + keep=; moved=; unknown= + for p in $pkgs; do + if [ "$DRYRUN" -eq 1 ] || ! have apt-cache || apt_known "$p"; then keep="$keep $p" + else unknown="$unknown $p"; fi + done + for p in $unknown; do + k=$(awk -F'\t' -v p="$p" '!/^[#@]/ && NF>=3 && index(" "$4" "," "p" ") {print $1"/"$2; exit}' "$MANIFEST") + b=$(awk -F'\t' -v p="$p" '!/^[#@]/ && NF>=3 && index(" "$4" "," "p" ") {print $5; exit}' "$MANIFEST") + case $b in + -|-*) note_fail "${k:-$p}" "apt does not know '$p' and there is no brew fallback" ;; + *) warn "apt does not know '$p' — falling back to brew '$b'"; moved="$moved $b" ;; + esac + done + if [ -n "$keep" ]; then + # shellcheck disable=SC2086 + if ! run_sh "${SUDO:+$SUDO }DEBIAN_FRONTEND=noninteractive apt-get install -y$(printf ' %s' $keep)"; then + warn "batch install failed; retrying one at a time so one bad package does not sink the rest" + for p in $keep; do + run_sh "${SUDO:+$SUDO }DEBIAN_FRONTEND=noninteractive apt-get install -y $p" \ + || note_fail "$p" "apt install failed" + done + fi + fi + BREW_EXTRA=$moved +} + +install_brew() { + pkgs=$(norm "$1") + [ -n "$pkgs" ] || return 0 + head_ "brew" + BREW=$(find_tool brew || echo brew) + if ! have brew && [ "$DRYRUN" -eq 0 ]; then + # Installing a second package manager is exactly the kind of thing the + # `invasive` flag exists to refuse doing on your behalf. Say what to run. + warn "brew is not installed; skipping:$pkgs" + warn " install it first: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" + for p in $pkgs; do note_fail "$p" "brew missing"; done + return 0 + fi + for p in $pkgs; do run_sh "$BREW install $p" || note_fail "$p" "brew install failed"; done +} + +install_npm() { + specs=$(norm "$1") + [ -n "$specs" ] || return 0 + head_ "npm" + NPM=$(find_tool npm || echo npm) + if ! have npm && [ "$DRYRUN" -eq 0 ]; then + # Ubuntu's `nodejs` package ships node WITHOUT npm; the manifest asks + # apt for both. If this fires anyway, node itself did not land. + for p in $specs; do note_fail "$p" "npm missing — core/node did not install"; done + return 0 + fi + # shellcheck disable=SC2086 + run_sh "$NPM install -g$(printf ' %s' $specs)" || { + warn "batch npm install failed; retrying one at a time" + for p in $specs; do run_sh "$NPM install -g $p" || note_fail "$p" "npm install failed"; done + } +} + +install_uv() { + tools=$(norm "$1") + [ -n "$tools" ] || return 0 + head_ "uv" + UV=$(find_tool uv || echo uv) + if ! have uv && [ "$DRYRUN" -eq 0 ]; then + for t in $tools; do note_fail "$t" "uv missing — core/uv did not install"; done + return 0 + fi + for t in $tools; do run_sh "$UV tool install $t" || note_fail "$t" "uv tool install failed"; done +} + +install_snap() { + names=$(norm "$1") + [ -n "$names" ] || return 0 + head_ "snap" + if ! have snap && [ "$DRYRUN" -eq 0 ]; then + for n in $names; do note_fail "$n" "snapd is not present on this machine"; done + return 0 + fi + for n in $names; do run_sh "${SUDO:+$SUDO }snap install $n" || note_fail "$n" "snap install failed"; done +} + +install_flatpak() { + ids=$(norm "$1") + [ -n "$ids" ] || return 0 + head_ "flatpak" + if ! have flatpak && [ "$DRYRUN" -eq 0 ]; then + for i in $ids; do note_fail "$i" "flatpak is not present on this machine"; done + return 0 + fi + for i in $ids; do + run_sh "flatpak install -y --noninteractive flathub $i" || note_fail "$i" "flatpak install failed" + done +} + +# Two forms, because two real packages need two different things: +# https://…/x.deb a stable vendor URL (chrome) +# gh:/: latest release asset (ghostty) +install_deb() { + srcs=$(norm "$1") + [ -n "$srcs" ] || return 0 + head_ "deb" + for s in $srcs; do + url=$s + case $s in + gh:*) + spec=${s#gh:}; repo=${spec%%:*}; match=${spec#*:} + if [ "$DRYRUN" -eq 1 ]; then + printf ' + resolve latest %s asset matching *%s*\n' "$repo" "$match" + url="https://github.com/$repo/releases/latest/" + else + url=$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null \ + | awk -F'"' -v m="$match" '/browser_download_url/ && index($4,m) {print $4; exit}') + [ -n "$url" ] || { note_fail "$repo" "no release asset matching *$match*"; continue; } + fi ;; + esac + f=${TMPDIR:-/tmp}/dotup-$$.deb + run_sh "curl -fsSL '$url' -o '$f'" || { note_fail "$url" "download failed"; continue; } + run_sh "${SUDO:+$SUDO }apt-get install -y '$f'" || note_fail "$url" "dpkg install failed" + run_sh "rm -f '$f'" + done +} + +# Bespoke by nature: each of these needs arch detection, a destination outside +# $HOME, and a symlink. A data column cannot express that honestly, so the +# dispatch is on the key and the code says what it does. +install_bespoke() { + bsp=$(norm "$2") + [ -n "$bsp" ] || return 0 + head_ "$1" + for key in $bsp; do + case $key in + core/neovim) + # The one place the channel is prescribed rather than "whatever the + # package manager has": 24.04's apt candidate is 0.9.5 and LazyVim + # needs 0.12 for the kitty graphics protocol. + if have nvim && [ "$DRYRUN" -eq 0 ]; then + v=$(nvim --version 2>/dev/null | awk 'NR==1{print $2}' | tr -d 'v') + case $v in 0.1[2-9]*|0.[2-9]*|[1-9]*) say " nvim $v already above 0.12 — leaving it"; continue ;; esac + fi + case $(uname -m) in + x86_64|amd64) a=x86_64 ;; + aarch64|arm64) a=arm64 ;; + *) note_fail "$key" "no neovim tarball for $(uname -m)"; continue ;; + esac + b=https://github.com/neovim/neovim/releases/latest/download + run_sh "curl -fsSL '$b/nvim-linux-$a.tar.gz' -o /tmp/nvim.tgz || curl -fsSL '$b/nvim-linux64.tar.gz' -o /tmp/nvim.tgz" \ + || { note_fail "$key" "tarball download failed"; continue; } + run_sh "${SUDO:+$SUDO }rm -rf /opt/nvim && ${SUDO:+$SUDO }mkdir -p /opt/nvim && ${SUDO:+$SUDO }tar -xzf /tmp/nvim.tgz -C /opt/nvim --strip-components=1" \ + || { note_fail "$key" "tarball extract failed"; continue; } + run_sh "${SUDO:+$SUDO }ln -sf /opt/nvim/bin/nvim /usr/local/bin/nvim" + run_sh "rm -f /tmp/nvim.tgz" ;; + core/go) + if have go && [ "$DRYRUN" -eq 0 ]; then say " go already present — leaving it"; continue; fi + case $(uname -s) in Darwin) o=darwin ;; *) o=linux ;; esac + case $(uname -m) in x86_64|amd64) a=amd64 ;; aarch64|arm64) a=arm64 ;; + *) note_fail "$key" "no go tarball for $(uname -m)"; continue ;; esac + if [ "$DRYRUN" -eq 1 ]; then v='go1.X.Y' + else v=$(curl -fsSL 'https://go.dev/VERSION?m=text' 2>/dev/null | head -1); fi + [ -n "$v" ] || { note_fail "$key" "could not resolve the current go version"; continue; } + run_sh "curl -fsSL 'https://go.dev/dl/$v.$o-$a.tar.gz' -o /tmp/go.tgz" \ + || { note_fail "$key" "tarball download failed"; continue; } + run_sh "${SUDO:+$SUDO }rm -rf /usr/local/go && ${SUDO:+$SUDO }tar -xzf /tmp/go.tgz -C /usr/local" \ + || { note_fail "$key" "tarball extract failed"; continue; } + run_sh "rm -f /tmp/go.tgz" ;; + core/chezmoi) + # Circular by nature: dotup arrives *via* chezmoi. Present already + # in every case that matters; here for the one where it is not. + if have chezmoi && [ "$DRYRUN" -eq 0 ]; then say " chezmoi already present — leaving it"; continue; fi + run_sh "sh -c \"\$(curl -fsLS get.chezmoi.io)\" -- -b \"\$HOME/.local/bin\"" \ + || note_fail "$key" "installer failed" ;; + core/uv) + if have uv && [ "$DRYRUN" -eq 0 ]; then say " uv already present — leaving it"; continue; fi + run_sh "curl -LsSf https://astral.sh/uv/install.sh | sh" || note_fail "$key" "installer failed" ;; + core/build-tools) + # macOS only — the Linux side is build-essential through apt. + if [ "$DRYRUN" -eq 0 ] && xcode-select -p >/dev/null 2>&1; then + say " Xcode command line tools already installed"; continue + fi + run_sh "xcode-select --install" || note_fail "$key" "xcode-select --install failed" ;; + core/zsh|networking/openssh-server) + say " $key: built into macOS, nothing to install" ;; + *) note_fail "$key" "no handler for channel '$1'" ;; + esac + done +} + +cmd_install() { + : > "$FAILED" + tbl=$STATE/.plan.$$ + plan_table > "$tbl" + + # Two filters, and they are the reason this is safe to run unattended. + # `private` is never a package: it needs a password nobody is there to type. + # `invasive` is never installed by a run with nobody at the keyboard, even + # if a stale state file says otherwise — the boundary holds because of what + # this refuses, not because of what it was asked. + if [ "$UNATTENDED" -eq 1 ]; then + inv=$(awk -F'\t' -v selfile="$SEL" ' + BEGIN{ while((getline l < selfile)>0) sel[l]=1 } + !/^[#@]/ && NF>=3 && $3=="invasive" && (($1"/"$2) in sel) {print $1"/"$2}' "$MANIFEST") + if [ -n "$inv" ]; then + warn "unattended: refusing invasive packages$(printf ' %s' $inv)" + for k in $inv; do + awk -F'\t' -v k="$k" '$3!=k' "$tbl" > "$tbl.f"; mv "$tbl.f" "$tbl" + done + fi + fi + + col() { awk -F'\t' -v c="$1" '$1==c {print $2}' "$tbl" | sort -u | tr '\n' ' '; } + keys() { awk -F'\t' -v c="$1" '$1==c {print $3}' "$tbl" | sort -u | tr '\n' ' '; } + + bad=$(awk -F'\t' '$1=="unavailable" || $1=="missing" {print $3}' "$tbl" | tr '\n' ' ') + [ -z "$bad" ] || warn "no source on this platform:$bad" + + # Order is a fixed pipeline, not a topological sort, because the real + # manifest has exactly two ordering constraints and both are channel-level: + # npm needs node (apt/brew), and uv tools need uv (script). + BREW_EXTRA= + install_apt "$(col apt)" + install_brew "$(col brew) $BREW_EXTRA" + install_bespoke script "$(keys script)" + install_bespoke tarball "$(keys tarball)" + install_bespoke builtin "$(keys builtin)" + install_bespoke xcode "$(keys xcode)" + install_snap "$(col snap)" + install_deb "$(col deb)" + install_flatpak "$(col flatpak)" + install_npm "$(col npm)" + install_uv "$(col uv)" + + rm -f "$tbl" + if [ -s "$FAILED" ]; then + head_ "did not install" + while IFS=" " read -r what why; do printf ' %s%-32s%s %s\n' "$RED" "$what" "$R" "$why" >&2; done < "$FAILED" + n=$(grep -c . "$FAILED") + rm -f "$FAILED" + printf '\n%s%d package(s) did not install.%s Everything else did.\n' "$YEL" "$n" "$R" >&2 + return 1 + fi + rm -f "$FAILED" + head_ "done" + return 0 +} + +# ----------------------------------------------------------------- private --- +# The tick does not do the work, it schedules it. git, chezmoi and bws have to +# exist before either row can act, and a password typed at picker time would sit +# in memory for the ten minutes of package downloads in between. So it happens +# here, immediately before it is used, and never at all without a human. +PRIV_SRC=${DOTUP_PRIVATE_SRC:-${XDG_DATA_HOME:-$HOME/.local/share}/dotfiles-private} +BWS_TOKEN=${DOTUP_BWS_TOKEN:-${XDG_CONFIG_HOME:-$HOME/.config}/bitwarden/bws-token} + +cmd_private() { + rows=$(selected_private) + [ -n "$rows" ] || return 0 + + # The §1 boundary is structural: it holds because there is nobody to type a + # password, not because of a policy this is obeying. + if [ "$UNATTENDED" -eq 1 ]; then + warn "unattended: the private tier needs a password nobody is here to type — skipped" + return 0 + fi + [ "$DRYRUN" -eq 0 ] || { head_ "private"; printf ' + prompt for URL, username, password (interactive only)\n'; return 0; } + [ -t 0 ] || { warn "no terminal: the private tier needs a password — skipped"; return 0; } + + want_repo=0; want_bws=0 + case $rows in *private/private-repo*) want_repo=1 ;; esac + case $rows in *private/bws-secrets*) want_bws=1 ;; esac + # A satisfied row is silent. You only see the prompt for something absent. + [ ! -d "$PRIV_SRC" ] || { say " private repo already present at $PRIV_SRC"; want_repo=0; } + [ ! -r "$BWS_TOKEN" ] || { say " bws token already present"; want_bws=0; } + [ "$want_repo" -eq 1 ] || [ "$want_bws" -eq 1 ] || return 0 + + head_ "private tier" + say " The address is in no repository. Leave it blank to stay public-only." + # Read into variables: nothing reaches argv, so nothing reaches `ps`. + P_URL=''; P_USER=''; P_PW='' + printf ' Bootstrap URL: ' >&2; IFS= read -r P_URL || : + [ -n "$P_URL" ] || { say " public-only machine. Nothing was asked for."; return 0; } + printf ' Username: ' >&2; IFS= read -r P_USER || : + printf ' Password: ' >&2 + stty -echo 2>/dev/null || :; IFS= read -r P_PW || :; stty echo 2>/dev/null || :; printf '\n' >&2 + # shellcheck disable=SC2064 + trap 'unset P_URL P_USER P_PW P_BLOB 2>/dev/null || :' EXIT INT TERM + + # curl -K - reads its config, credentials included, from stdin rather than + # the command line. --fail matters too: without it a 401 body is parsed as + # if it were the blob. + P_BLOB=$(printf 'user = "%s:%s"\nsilent\nfail\n' "$P_USER" "$P_PW" \ + | curl -K - "${P_URL%/}/bootstrap.env" 2>/dev/null) || { + err "endpoint refused the credentials. The machine stays public-only." + unset P_PW; return 1; } + unset P_PW # spent. only the fetched credentials exist now. + + # Contract with the endpoint (phase 4 writes the file this parses): + # two KEY=VALUE lines, no quoting, no shell + # PRIVATE_REPO_URL=https://:@host/path/dotfiles-private.git + # BWS_ACCESS_TOKEN= + p_repo=$(printf '%s\n' "$P_BLOB" | sed -n 's/^PRIVATE_REPO_URL=//p' | head -1) + p_tok=$(printf '%s\n' "$P_BLOB" | sed -n 's/^BWS_ACCESS_TOKEN=//p' | head -1) + unset P_BLOB + + if [ "$want_bws" -eq 1 ]; then + if [ -n "$p_tok" ]; then + mkdir -p "$(dirname "$BWS_TOKEN")" + ( umask 077; printf '%s\n' "$p_tok" > "$BWS_TOKEN" ) + chmod 600 "$BWS_TOKEN" + say " bws token written, mode 600" + else err "the blob carried no BWS_ACCESS_TOKEN"; fi + fi + unset p_tok + + if [ "$want_repo" -eq 1 ]; then + if [ -n "$p_repo" ]; then + run chezmoi init --apply --source "$PRIV_SRC" "$p_repo" \ + || err "private repo init failed" + else err "the blob carried no PRIVATE_REPO_URL"; fi + fi + unset p_repo + trap - EXIT INT TERM +} + +# --------------------------------------------------------------- preflight --- +# fzf cannot come from the manifest: the picker needs it to draw the list that +# installs it. Same bootstrap problem chezmoi has. So it is fetched here, before +# any UI exists. +# +# FLOOR is exactly where verification stops, not a guess. Every release from +# 0.29 up parses every option and binding the picker uses, but cursor-on-reload +# — the property that makes ticking a list bearable — can only be measured from +# 0.44.1, where fzf's --listen API began reporting state. Below that it is +# unverifiable rather than known-broken, so we decline to rely on it. +# +# The binary is dotup's own, not yours. It lands in a cache directory and is +# invoked by absolute path — PATH is never touched. Dropping a newer fzf into +# ~/.local/bin would shadow the distro's copy for Ctrl-R, the oh-my-zsh plugin +# and every other script, which is precisely the kind of silent change the +# `invasive` flag exists to refuse. Not the repo either: a vendored binary means +# either megabytes in git or a .gitignore rule, and the public repo stays clean. +FZF_FLOOR=0.44.0 +FZF_PIN=0.74.2 +FZF_CACHE=${XDG_CACHE_HOME:-$HOME/.cache}/dotup +FZF= + +# $1 >= $2, dotted numeric. Not `sort -V`: BSD sort on older macOS lacks it. +ver_ge() { + awk -v a="$1" -v b="$2" 'BEGIN{ + na=split(a,A,"."); nb=split(b,B,".") + for(i=1;i<=3;i++){ x=(i<=na)?A[i]+0:0; y=(i<=nb)?B[i]+0:0 + if(x>y) exit 0; if(x/dev/null | awk '{print $1}'; } + +install_fzf() { + case $(uname -s) in Darwin) os=darwin ;; *) os=linux ;; esac + case $(uname -m) in + x86_64|amd64) arch=amd64 ;; + aarch64|arm64) arch=arm64 ;; + armv7l) arch=armv7 ;; + *) echo "dotup: unsupported arch $(uname -m); install fzf yourself" >&2; return 1 ;; + esac + # The release tag gained a leading v between 0.53.0 and 0.55.0. Anything we + # would pin today is above that; the fallback keeps an older pin working. + base=https://github.com/junegunn/fzf/releases/download + mkdir -p "$FZF_CACHE" + tmp=$FZF_CACHE/.fzf.$$ + echo "dotup: fetching fzf $FZF_PIN ($os/$arch) for its own use" >&2 + if curl -sfL "$base/v$FZF_PIN/fzf-$FZF_PIN-${os}_${arch}.tar.gz" | tar xz -O fzf > "$tmp" 2>/dev/null && [ -s "$tmp" ]; then : + elif curl -sfL "$base/$FZF_PIN/fzf-$FZF_PIN-${os}_${arch}.tar.gz" | tar xz -O fzf > "$tmp" 2>/dev/null && [ -s "$tmp" ]; then : + else rm -f "$tmp"; echo "dotup: could not fetch fzf $FZF_PIN" >&2; return 1; fi + chmod +x "$tmp" && mv "$tmp" "$FZF_CACHE/fzf" +} + +# Resolution order, cheapest first. The machine's own fzf wins when it clears +# the floor — nothing is replaced merely for being old. +ensure_fzf() { + if [ -x "$FZF_CACHE/fzf" ] && ver_ge "$("$FZF_CACHE/fzf" --version 2>/dev/null | awk '{print $1}')" "$FZF_FLOOR"; then + FZF=$FZF_CACHE/fzf; return 0 + fi + if command -v fzf >/dev/null 2>&1; then + cur=$(fzf_version) + if [ -n "$cur" ] && ver_ge "$cur" "$FZF_FLOOR"; then + FZF=$(command -v fzf); return 0 + fi + echo "dotup: system fzf $cur is below the verified floor $FZF_FLOOR" >&2 + fi + install_fzf || return 1 + FZF=$FZF_CACHE/fzf +} + +# ------------------------------------------------------------------- pick ---- +cmd_pick() { + ensure_fzf || { echo "dotup: no usable fzf; use the numbered prompt" >&2; return 2; } + [ -s "$SEL" ] || cmd_preset defaults + # --exact is a safety property, not a preference. ^t toggles every row the + # filter is showing, so the filter must mean exactly what it looks like. + # Fuzzy-matching "nvidia" also matches docker, tailscale and desktop. + cmd_render | "$FZF" --ansi --exact --no-sort --cycle --multi --layout=reverse --height=100% \ + --delimiter='\t' --with-nth=1 --pointer='>' --marker=' ' \ + --info=inline --border=none \ + --header=$'space tick tab open ^t tick all shown ^a defaults ^x none ^o open all enter install\n' \ + --preview "$SELF explain {2}" --preview-window='right,46%,wrap,border-left' \ + --bind "space:execute-silent($SELF toggle {2})+reload($SELF render)" \ + --bind "tab:execute-silent($SELF expand {2})+reload($SELF render)" \ + --bind "ctrl-t:select-all+execute-silent($SELF toggle {+2})+clear-selection+reload($SELF render)" \ + --bind "ctrl-a:execute-silent($SELF preset defaults)+reload($SELF render)" \ + --bind "ctrl-x:execute-silent($SELF preset none)+reload($SELF render)" \ + --bind "ctrl-o:execute-silent($SELF expand-all)+reload($SELF render)" \ + --bind 'enter:accept' > /dev/null || return 1 +} + +confirm() { + [ "$ASSUME_YES" -eq 0 ] || return 0 + [ -t 0 ] || return 0 + printf '\n install? [y/N] ' >&2 + a=''; IFS= read -r a || a='' + case $a in y|Y|yes|YES) return 0 ;; *) say " nothing installed."; return 1 ;; esac +} + +cmd_run() { + if [ "$UNATTENDED" -eq 1 ]; then + # Deterministic by construction: computed fresh from the manifest, never + # inherited from whatever a previous interactive run left in the state + # file. "What a VM or CI run gets" has to mean the same thing twice. + cmd_preset defaults + cmd_plan + else + cmd_pick || return $? + cmd_plan + confirm || return 0 + fi + rc=0 + cmd_install || rc=$? + cmd_private || : + return $rc +} + +# ------------------------------------------------------------------ usage ---- +usage() { + cat >&2 <<-EOF + usage: dotup [--unattended] [--print] [--yes] + + (no flags) the picker, then install what you ticked + --unattended no UI: safe defaults, never prompts, never private + --print, -n resolve everything and print the commands, install nothing + --yes, -y skip the confirmation after the picker + + plumbing, called by the fzf bindings: + render toggle expand expand-all preset explain plan preflight + EOF +} + +CMD= +while [ $# -gt 0 ]; do + case $1 in + --unattended) UNATTENDED=1; ASSUME_YES=1 ;; + --print|-n) DRYRUN=1 ;; + --yes|-y) ASSUME_YES=1 ;; + -h|--help) usage; exit 0 ;; + --*) err "unknown flag: $1"; usage; exit 2 ;; + *) CMD=$1; shift; break ;; + esac + shift +done + +case ${CMD:-run} in +run) cmd_run ;; +pick) cmd_pick && cmd_plan ;; +install) cmd_install ;; +private) cmd_private ;; +render) cmd_render ;; +toggle) cmd_toggle "$@" ;; +expand) cmd_expand "$@" ;; +expand-all) cmd_expand_all ;; +preset) cmd_preset "${1:-defaults}" ;; +explain) cmd_explain "${1:-}" ;; +plan) cmd_plan ;; +resolve) resolve "${1:-}" ;; # test hook +preflight) ensure_fzf && echo "using $FZF ($("$FZF" --version | awk '{print $1}'), floor $FZF_FLOOR)" ;; +fzf-path) ensure_fzf >/dev/null 2>&1 && echo "$FZF" ;; # test hook +vercmp) ver_ge "$1" "$2" ;; # test hook +*) usage; exit 2 ;; +esac diff --git a/dot_local/bin/executable_sysjournal b/dot_local/bin/executable_sysjournal new file mode 100755 index 0000000..0ccd3f6 --- /dev/null +++ b/dot_local/bin/executable_sysjournal @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# sysjournal — shared system-knowledge journal for the Obsidian vault. +# +# One tool, used by every coding agent (Claude Code, Codex, Pi), so host/ +# environment changes get recorded in ONE consistent, greppable place with +# the frontmatter schema that "Tech/Infrastructure/System Log MOC.md" indexes +# via Dataview (date, machine, type, status, tags, review-by). +# +# Journal HOST/ENVIRONMENT changes — system config, services, networking, +# VMs, drivers, build toolchains, host-wiring of an app. NOT ordinary work +# inside a code repo. See `sysjournal help`. +# +# Portable bash (invoked by agents in varied environments), not zsh. + +set -euo pipefail + +VAULT="${SYSJOURNAL_VAULT:-$HOME/Documents/Obsidian25}" +SUBDIR="${SYSJOURNAL_SUBDIR:-Tech/Infrastructure}" +INFRA="$VAULT/$SUBDIR" + +die() { printf 'sysjournal: %s\n' "$*" >&2; exit 1; } + +ensure_dir() { + [ -d "$VAULT" ] || die "vault not found: $VAULT (set SYSJOURNAL_VAULT)" + mkdir -p "$INFRA" +} + +usage() { + cat <<'EOF' +sysjournal — record & recall host/environment changes in the Obsidian vault. + +USAGE + sysjournal search Recall: grep the journal first (case-insensitive) + sysjournal new "" [options] Scaffold a new note, print its path + sysjournal list [N] List the N most-recent notes (default 20) + sysjournal path Print the infrastructure folder path + sysjournal help This help + +`new` OPTIONS + --type <t> change | setup | debug | fix | incident | note (default: change) + --status <s> deployed | resolved | unresolved | workaround | planned (default: deployed) + --tags a,b,c comma-separated tags + --machine <m> host the change was made on (default: `hostname -s`) + --review-by <YYYY-MM-DD> optional follow-up/expiry date + --summary "<one line>" optional lead line + +WHAT TO JOURNAL + YES system config, services/daemons (systemd/launchd/cron), networking/DNS/ + VPN/firewall, VMs & host containers, drivers/kernel/boot, disks/mounts, + build toolchains & global package installs, wiring an app into the host. + NO feature work, bug fixes, refactors, tests INSIDE a project repo. + Discriminator: does it change state outside the repo, on the host? If no, skip. + Straddle (build an app AND install it as a service): journal only the + host-wiring part (the unit/cron/port), not the app code. + +EXAMPLES + sysjournal search systemd relay port + sysjournal new "WireGuard VPN to homelab" --type setup --tags wireguard,vpn,network + sysjournal new "DNS resolution flaky after netplan change" --type debug --status unresolved +EOF +} + +cmd_search() { + ensure_dir + [ "$#" -ge 1 ] || die "search needs at least one keyword" + # OR-match the keywords so a few loosely-related terms still surface notes. + local pattern + pattern=$(printf '%s|' "$@"); pattern="${pattern%|}" + echo "# Journal matches in $SUBDIR for: $*" + echo + if ! rg -i --no-heading -n -C1 --color never -e "$pattern" "$INFRA" 2>/dev/null; then + echo "(no matches — nothing journaled on this yet)" + fi +} + +cmd_list() { + ensure_dir + local n="${1:-20}" + # Newest first by mtime; strip the vault prefix for readability. + find "$INFRA" -maxdepth 1 -name '*.md' -printf '%T@ %p\n' 2>/dev/null \ + | sort -rn | head -n "$n" | sed "s#[0-9.]* $INFRA/##" +} + +cmd_path() { echo "$INFRA"; } + +cmd_new() { + ensure_dir + local title="" type="change" status="deployed" tags="" machine review_by="" summary="" + machine="$(hostname -s 2>/dev/null || hostname)" + + # First non-flag arg is the title. + while [ "$#" -gt 0 ]; do + case "$1" in + --type) type="${2:?--type needs a value}"; shift 2;; + --status) status="${2:?--status needs a value}"; shift 2;; + --tags) tags="${2:?--tags needs a value}"; shift 2;; + --machine) machine="${2:?--machine needs a value}"; shift 2;; + --review-by) review_by="${2:?--review-by needs a value}"; shift 2;; + --summary) summary="${2:?--summary needs a value}"; shift 2;; + --*) die "unknown option: $1";; + *) [ -z "$title" ] && title="$1" || die "unexpected arg: $1"; shift;; + esac + done + [ -n "$title" ] || die 'new needs a "<Title>"' + + # Filename: keep the human title (Obsidian-friendly), drop only path-illegal chars. + local fname; fname=$(printf '%s' "$title" | tr '/\\' '--' | sed 's/[[:cntrl:]]//g; s/ */ /g; s/^ *//; s/ *$//') + local file="$INFRA/$fname.md" + if [ -e "$file" ]; then + echo "$file" # already exists — recall, don't clobber; edit/append this note. + echo "sysjournal: note already exists — edit it instead of creating a duplicate." >&2 + return 0 + fi + + # YAML frontmatter — inline tag list, matching the MOC's own `tags: [moc]` style. + local yaml_tags="[]" + if [ -n "$tags" ]; then + yaml_tags="[$(printf '%s' "$tags" | sed 's/ *, */, /g')]" + fi + local date_today; date_today="$(date +%F)" + + { + echo "---" + echo "date: $date_today" + echo "machine: $machine" + echo "type: $type" + echo "status: $status" + echo "tags: $yaml_tags" + [ -n "$review_by" ] && echo "review-by: $review_by" + echo "---" + echo + echo "# $title" + echo + [ -n "$summary" ] && { echo "$summary"; echo; } + echo "## Why" + echo + echo "## What changed" + echo + echo "## Design decisions / gotchas" + echo + echo "## Verify" + echo + echo '```' + echo '# command run + observed result' + echo '```' + echo + echo "## Status / follow-ups" + echo + echo "Related: " + } > "$file" + + echo "$file" +} + +main() { + local sub="${1:-help}"; shift || true + case "$sub" in + search|recall|grep) cmd_search "$@";; + new|add) cmd_new "$@";; + list|ls) cmd_list "$@";; + path|dir) cmd_path;; + help|-h|--help) usage;; + *) usage; die "unknown command: $sub";; + esac +} + +main "$@" diff --git a/dot_local/share/dotup/packages.tsv b/dot_local/share/dotup/packages.tsv new file mode 100644 index 0000000..abd622d --- /dev/null +++ b/dot_local/share/dotup/packages.tsv @@ -0,0 +1,119 @@ +#group pkg flag apt brew note +# +# Five columns, tab separated, greppable by hand. Anything that would have been +# a sixth column is an @ directive instead, so a package with no dependency and +# no install argument -- most of them -- costs nothing to read. +# +# @needs <group/pkg> <dep>... dep is group/pkg or a whole group. +# Closure is transitive in both directions. +# @spec <group/pkg> <arg>... Argument for a channel that cannot be named +# by the package: the npm spec, the flatpak +# app id, the .deb source. Defaults to the +# package name when absent. +# @<group> <note> Group note. Only needed where the members +# fail the safe test for different reasons; +# otherwise the worst child's note is right. +# +# A leading `-` in the apt or brew column means "not from this package manager": +# -tarball -npm -script -snap -deb -flatpak -uv -builtin -xcode. +# A bare `-` means unavailable there. Linux falls back to the brew column before +# giving up -- omp, herdr and lazygit have no apt package at all. The fallback is +# one-directional: there is no apt on a Mac, so a `-` in the brew column is the +# end of the road rather than a reason to read a column of Debian package names. +# +@needs networking/xrdp desktop +@needs networking/xorgxrdp desktop +@needs gpu/container-toolkit docker +@needs agents/codex core/node +@needs agents/pi core/node +@needs agents/pi-plugins core/node +@needs agents/pi-plugins agents/pi +@needs agents/specify-cli core/uv +@needs core/mermaid-cli core/node +@needs core/neovim core/imagemagick core/mermaid-cli +# +# npm names carry scopes that the plugin's short name does not. `npm i -g +# rpiv-btw` installs somebody else's package. +@spec agents/codex @openai/codex +@spec agents/pi @earendil-works/pi-coding-agent +@spec agents/pi-plugins @juicesharp/rpiv-ask-user-question @juicesharp/rpiv-btw @juicesharp/rpiv-todo @samfp/pi-memory @tmustier/pi-ralph-wiggum pi-markdown-preview pi-powerline-footer pi-simplify pi-subagents pi-web-access +@spec agents/specify-cli specify-cli +@spec core/mermaid-cli @mermaid-js/mermaid-cli +@spec core/bitwarden-cli bw +@spec apps/obsidian md.obsidian.Obsidian +@spec apps/chrome https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb +@spec apps/ghostty gh:mkasberg/ghostty-ubuntu:_amd64.deb +core neovim safe -tarball neovim apt ships 0.9.5 — tarball to /opt/nvim +core ripgrep safe ripgrep ripgrep binary is rg +core fd safe fd-find fd binary is fdfind on ubuntu +core bat safe bat bat binary is batcat on ubuntu +core fzf safe fzf fzf for your shell (ctrl-r); the picker uses its own pinned copy +core eza safe eza eza +core zsh safe zsh -builtin login shell everywhere +core tmux safe tmux tmux +core git-lfs safe git-lfs git-lfs +core lazygit safe - lazygit no apt package +core gh safe gh gh +core chezmoi safe -script chezmoi bootstrap cannot come from the manifest +core go safe -tarball go /usr/local/go on linux +core uv safe -script uv astral installer to ~/.local/bin +core node safe nodejs npm node apt's nodejs ships node WITHOUT npm — both names needed +core imagemagick safe imagemagick imagemagick required by the nvim markdown render path +core mermaid-cli safe -npm -npm mmdc — nvim renders mermaid fences with it +core btop safe btop btop +core htop safe htop htop +core ncdu safe ncdu ncdu +core tree safe tree tree +core cmake safe cmake cmake +core ninja safe ninja-build ninja package name differs from binary +core build-tools safe build-essential -xcode +core magic-wormhole safe magic-wormhole magic-wormhole snap wormhole on dev +core bitwarden-cli safe -snap bitwarden-cli snap bw on ubuntu +core mosh safe mosh mosh +core nmap safe nmap nmap +core binwalk safe binwalk binwalk +core pipx safe pipx pipx +core age safe age age general purpose only now +agents codex safe -npm -npm @openai/codex — unpinned, always latest +agents pi safe -npm -npm @earendil-works/pi-coding-agent +agents pi-plugins safe -npm -npm 10 plugins: rpiv-*, pi-memory, pi-subagents, ... +agents omp safe - can1357/tap/omp oh my pi — tap only, pulls linuxbrew on linux +agents herdr safe - herdr terminal workspace manager for agents +agents specify-cli safe -uv -uv uv tool install +fonts hack-nerd-font safe fonts-powerline font-hack-nerd-font +fonts iosevka-nerd-font safe fonts-powerline font-iosevka-nerd-font +media ffmpeg safe ffmpeg ffmpeg +media sox safe sox sox +media p7zip safe p7zip-full p7zip +apps obsidian gui -flatpak obsidian +apps ghostty gui -deb ghostty +apps chrome gui -deb google-chrome +apps firefox gui firefox firefox +apps vlc gui vlc vlc +@networking daemons, listening ports, and setuid mount helpers +networking openssh-server invasive openssh-server -builtin opens port 22 on every network this box can reach +networking tailscale invasive tailscale tailscale daemon; joins a private network and rewrites DNS +networking avahi-daemon invasive avahi-daemon - daemon; broadcasts this host on the LAN +networking xrdp invasive xrdp - opens port 3389 · useless without the desktop group +networking xorgxrdp invasive xorgxrdp - xrdp's X backend +networking nfs-common invasive nfs-common - setuid mount helper +networking cifs-utils invasive cifs-utils - setuid mount helper +networking davfs2 invasive davfs2 - setuid mount helper +@docker daemon; membership in the docker group is root-equivalent +docker docker-ce invasive docker-ce - daemon; docker group is root-equivalent +docker docker-buildx invasive docker-buildx-plugin - +docker docker-compose invasive docker-compose-plugin - +@desktop changes the display manager — can leave you at a black screen +desktop xfce4 invasive xfce4 - changes the display manager +desktop lightdm invasive lightdm - CAN LEAVE YOU AT A BLACK SCREEN +@gpu kernel modules; a bad driver can break boot +gpu nvidia-driver invasive nvidia-driver-570 - kernel modules; can break boot +gpu cuda-toolkit invasive nvidia-cuda-toolkit - +gpu container-toolkit invasive nvidia-container-toolkit - requires docker +@virt daemon, bridges, and group membership +virt qemu invasive qemu-kvm - +virt libvirt invasive libvirt-daemon-system - daemon + group membership +virt virt-manager invasive virt-manager - +@private one password, typed after the install finishes +private private-repo private - - ~/.local/share/dotfiles-private — agent config, ssh config +private bws-secrets private - - 7 API keys into ~/.config/zsh/secrets.zsh diff --git a/dot_tmux.conf.local b/dot_tmux.conf.local new file mode 100644 index 0000000..39261e3 --- /dev/null +++ b/dot_tmux.conf.local @@ -0,0 +1,531 @@ +# --- clipboard: allow OSC 52 passthrough from nvim --- +set -g allow-passthrough on #!important +set -s set-clipboard on #!important + +# --- claude-managed: session persistence --- +set -g @continuum-restore 'on' +set -g @resurrect-capture-pane-contents 'on' +run-shell ~/.tmux/plugins/tmux-resurrect/resurrect.tmux +run-shell ~/.tmux/plugins/tmux-continuum/continuum.tmux + +# : << 'EOF' +# Oh my tmux! +# 💛🩷💙🖤❤️🤍 +# https://github.com/gpakosz/.tmux +# (‑●‑●)> dual licensed under the WTFPL v2 license and the MIT license, +# without any warranty. +# Copyright 2012— Gregory Pakosz (@gpakosz). + + +# -- bindings ------------------------------------------------------------------ + +# preserve tmux stock bindings, +# while adding bindings that don't conflict with these stock bindings +# /!\ this disables some of Oh my tmux! bindings described in README.md +# - true +# - false (default) +tmux_conf_preserve_stock_bindings=false + + +# -- session creation ---------------------------------------------------------- + +# prompt for session name when creating a new session, possible values are: +# - true +# - false (default) +# - disabled (do not modify new-session bindings) +tmux_conf_new_session_prompt=false + +# new session retains current path, possible values are: +# - true +# - false (default) +# - disabled (do not modify new-session bindings) +tmux_conf_new_session_retain_current_path=false + + +# -- windows & pane creation --------------------------------------------------- + +# new window retains current path, possible values are: +# - true +# - false (default) +# - disabled (do not modify new-window bindings) +tmux_conf_new_window_retain_current_path=false + +# new window tries to reconnect ssh sessions, possible values are: +# - true +# - false (default) +# - disabled (do not modify new-window bindings) +tmux_conf_new_window_reconnect_ssh=false + +# new pane retains current path, possible values are: +# - true (default) +# - false +# - disabled (do not modify split-window bindings) +tmux_conf_new_pane_retain_current_path=true + +# new pane tries to reconnect ssh sessions, possible values are: +# - true +# - false (default) +# - disabled (do not modify split-window bindings) +tmux_conf_new_pane_reconnect_ssh=false + + +# -- display ------------------------------------------------------------------- + +# RGB 24-bit colour support, possible values are: +# - true +# - false +# - auto (default) +# +# automatic detection relies on the COLORTERM environment variable being defined +# to 'truecolor' or '24bit' or '$ tput colors' answering '16777216' +# see https://github.com/termstandard/colors +tmux_conf_24b_colour=auto + + +# -- theming ------------------------------------------------------------------- + +# enable or disable theming: +# - enabled (default) +# - disabled +# when disabled, all tmux_conf_theme_xxx variables are ignored except: +# - tmux_conf_theme_pairing +# - tmux_conf_theme_prefix +# - tmux_conf_theme_mouse +# - tmux_conf_theme_root +# - tmux_conf_theme_synchronized +tmux_conf_theme=enabled + +# default theme +tmux_conf_theme_colour_1="#080808" # dark gray +tmux_conf_theme_colour_2="#303030" # gray +tmux_conf_theme_colour_3="#8a8a8a" # light gray +tmux_conf_theme_colour_4="#00afff" # light blue +tmux_conf_theme_colour_5="#ffff00" # yellow +tmux_conf_theme_colour_6="#080808" # dark gray +tmux_conf_theme_colour_7="#e4e4e4" # white +tmux_conf_theme_colour_8="#080808" # dark gray +tmux_conf_theme_colour_9="#ffff00" # yellow +tmux_conf_theme_colour_10="#ff00af" # pink +tmux_conf_theme_colour_11="#5fff00" # green +tmux_conf_theme_colour_12="#8a8a8a" # light gray +tmux_conf_theme_colour_13="#e4e4e4" # white +tmux_conf_theme_colour_14="#080808" # dark gray +tmux_conf_theme_colour_15="#080808" # dark gray +tmux_conf_theme_colour_16="#d70000" # red +tmux_conf_theme_colour_17="#e4e4e4" # white + +# default theme (ansi) +#tmux_conf_theme_colour_1="colour0" +#tmux_conf_theme_colour_2="colour8" +#tmux_conf_theme_colour_3="colour8" +#tmux_conf_theme_colour_4="colour14" +#tmux_conf_theme_colour_5="colour11" +#tmux_conf_theme_colour_6="colour0" +#tmux_conf_theme_colour_7="colour15" +#tmux_conf_theme_colour_8="colour0" +#tmux_conf_theme_colour_9="colour11" +#tmux_conf_theme_colour_10="colour13" +#tmux_conf_theme_colour_11="colour10" +#tmux_conf_theme_colour_12="colour8" +#tmux_conf_theme_colour_13="colour15" +#tmux_conf_theme_colour_14="colour0" +#tmux_conf_theme_colour_15="colour0" +#tmux_conf_theme_colour_16="colour1" +#tmux_conf_theme_colour_17="colour15" + +# window style +tmux_conf_theme_window_fg="default" +tmux_conf_theme_window_bg="default" + +# highlight focused pane, possible values are: +# - true +# - false (default) +tmux_conf_theme_highlight_focused_pane=false + +# focused pane colours: +tmux_conf_theme_focused_pane_bg="$tmux_conf_theme_colour_2" + +# pane border style, possible values are: +# - thin (default) +# - fat +tmux_conf_theme_pane_border_style=thin + +# pane borders colours: +tmux_conf_theme_pane_border="$tmux_conf_theme_colour_2" +tmux_conf_theme_pane_active_border="$tmux_conf_theme_colour_4" +%if #{>=:#{version},3.2} +tmux_conf_theme_pane_active_border="#{?pane_in_mode,$tmux_conf_theme_colour_9,#{?synchronize-panes,$tmux_conf_theme_colour_16,$tmux_conf_theme_colour_4}}" +%endif + +# pane indicator colours (when you hit <prefix> + q) +tmux_conf_theme_pane_indicator="$tmux_conf_theme_colour_4" +tmux_conf_theme_pane_active_indicator="$tmux_conf_theme_colour_4" + +# status line style +tmux_conf_theme_message_fg="$tmux_conf_theme_colour_1" +tmux_conf_theme_message_bg="$tmux_conf_theme_colour_5" +tmux_conf_theme_message_attr="bold" + +# status line command style (<prefix> : Escape) +tmux_conf_theme_message_command_fg="$tmux_conf_theme_colour_5" +tmux_conf_theme_message_command_bg="$tmux_conf_theme_colour_1" +tmux_conf_theme_message_command_attr="bold" + +# window modes style +tmux_conf_theme_mode_fg="$tmux_conf_theme_colour_1" +tmux_conf_theme_mode_bg="$tmux_conf_theme_colour_5" +tmux_conf_theme_mode_attr="bold" + +# status line style +tmux_conf_theme_status_fg="$tmux_conf_theme_colour_3" +tmux_conf_theme_status_bg="$tmux_conf_theme_colour_1" +tmux_conf_theme_status_attr="none" + +# terminal title +# - built-in variables are: +# - #{circled_window_index} +# - #{circled_session_name} +# - #{hostname} +# - #{hostname_ssh} +# - #{hostname_full} +# - #{hostname_full_ssh} +# - #{username} +# - #{username_ssh} +tmux_conf_theme_terminal_title="#h ❐ #S ● #I #W" + +# window status style +# - built-in variables are: +# - #{circled_window_index} +# - #{circled_session_name} +# - #{hostname} +# - #{hostname_ssh} +# - #{hostname_full} +# - #{hostname_full_ssh} +# - #{username} +# - #{username_ssh} +tmux_conf_theme_window_status_fg="$tmux_conf_theme_colour_3" +tmux_conf_theme_window_status_bg="$tmux_conf_theme_colour_1" +tmux_conf_theme_window_status_attr="none" +tmux_conf_theme_window_status_format="#I #W#{?#{||:#{window_bell_flag},#{window_zoomed_flag}}, ,}#{?window_bell_flag,!,}#{?window_zoomed_flag,Z,}" +#tmux_conf_theme_window_status_format="#{circled_window_index} #W#{?#{||:#{window_bell_flag},#{window_zoomed_flag}}, ,}#{?window_bell_flag,!,}#{?window_zoomed_flag,Z,}" +#tmux_conf_theme_window_status_format="#I #W#{?#{||:#{window_bell_flag},#{window_zoomed_flag}}, ,}#{?window_bell_flag,🔔,}#{?window_zoomed_flag,🔍,}" + +# window current status style +# - built-in variables are: +# - #{circled_window_index} +# - #{circled_session_name} +# - #{hostname} +# - #{hostname_ssh} +# - #{hostname_full} +# - #{hostname_full_ssh} +# - #{username} +# - #{username_ssh} +tmux_conf_theme_window_status_current_fg="$tmux_conf_theme_colour_1" +tmux_conf_theme_window_status_current_bg="$tmux_conf_theme_colour_4" +tmux_conf_theme_window_status_current_attr="bold" +tmux_conf_theme_window_status_current_format="#I #W#{?#{||:#{window_bell_flag},#{window_zoomed_flag}}, ,}#{?window_bell_flag,!,}#{?window_zoomed_flag,Z,}" +#tmux_conf_theme_window_status_current_format="#{circled_window_index} #W#{?#{||:#{window_bell_flag},#{window_zoomed_flag}}, ,}#{?window_bell_flag,!,}#{?window_zoomed_flag,Z,}" +#tmux_conf_theme_window_status_current_format="#I #W#{?#{||:#{window_bell_flag},#{window_zoomed_flag}}, ,}#{?window_bell_flag,🔔,}#{?window_zoomed_flag,🔍,}" + +# window activity status style +tmux_conf_theme_window_status_activity_fg="default" +tmux_conf_theme_window_status_activity_bg="default" +tmux_conf_theme_window_status_activity_attr="underscore" + +# window bell status style +tmux_conf_theme_window_status_bell_fg="$tmux_conf_theme_colour_5" +tmux_conf_theme_window_status_bell_bg="default" +tmux_conf_theme_window_status_bell_attr="blink,bold" + +# window last status style +tmux_conf_theme_window_status_last_fg="$tmux_conf_theme_colour_4" +tmux_conf_theme_window_status_last_bg="$tmux_conf_theme_colour_2" +tmux_conf_theme_window_status_last_attr="none" + +# status left/right sections separators +#tmux_conf_theme_left_separator_main="" +#tmux_conf_theme_left_separator_sub="|" +#tmux_conf_theme_right_separator_main="" +#tmux_conf_theme_right_separator_sub="|" +tmux_conf_theme_left_separator_main='\uE0B0' # /!\ you don't need to install Powerline +tmux_conf_theme_left_separator_sub='\uE0B1' # you only need fonts patched with +tmux_conf_theme_right_separator_main='\uE0B2' # Powerline symbols or the standalone +tmux_conf_theme_right_separator_sub='\uE0B3' # PowerlineSymbols.otf font, see README.md + +# status left/right content: +# - separate main sections with "|" +# - separate subsections with "," +# - built-in variables are: +# - #{battery_bar} +# - #{battery_hbar} +# - #{battery_percentage} +# - #{battery_status} +# - #{battery_vbar} +# - #{circled_session_name} +# - #{hostname_ssh} +# - #{hostname} +# - #{hostname_full} +# - #{hostname_full_ssh} +# - #{loadavg} +# - #{mouse} +# - #{pairing} +# - #{prefix} +# - #{root} +# - #{synchronized} +# - #{uptime_y} +# - #{uptime_d} (modulo 365 when #{uptime_y} is used) +# - #{uptime_h} +# - #{uptime_m} +# - #{uptime_s} +# - #{username} +# - #{username_ssh} +tmux_conf_theme_status_left=" ❐ #S | ↑#{?uptime_y, #{uptime_y}y,}#{?uptime_d, #{uptime_d}d,}#{?uptime_h, #{uptime_h}h,}#{?uptime_m, #{uptime_m}m,} " +#tmux_conf_theme_status_left=" ❐ #S | ↑#{?uptime_y, #{uptime_y}y,}#{?uptime_d, #{uptime_d}d,}#{?uptime_h, #{uptime_h}h,}#{?uptime_m, #{uptime_m}m,} | #{pretty_pane_current_path} " +tmux_conf_theme_status_right=" #{prefix}#{mouse}#{pairing}#{synchronized}#{?battery_status,#{battery_status},}#{?battery_bar, #{battery_bar},}#{?battery_percentage, #{battery_percentage},} , %R , %d %b | #{username}#{root} | #{hostname} " + +# status left style +tmux_conf_theme_status_left_fg="$tmux_conf_theme_colour_6,$tmux_conf_theme_colour_7,$tmux_conf_theme_colour_8" +tmux_conf_theme_status_left_bg="$tmux_conf_theme_colour_9,$tmux_conf_theme_colour_10,$tmux_conf_theme_colour_11" +tmux_conf_theme_status_left_attr="bold,none,none" + +# status right style +tmux_conf_theme_status_right_fg="$tmux_conf_theme_colour_12,$tmux_conf_theme_colour_13,$tmux_conf_theme_colour_14" +tmux_conf_theme_status_right_bg="$tmux_conf_theme_colour_15,$tmux_conf_theme_colour_16,$tmux_conf_theme_colour_17" +tmux_conf_theme_status_right_attr="none,none,bold" + +# pairing indicator +tmux_conf_theme_pairing="⚇" # U+2687 +tmux_conf_theme_pairing_fg="none" +tmux_conf_theme_pairing_bg="none" +tmux_conf_theme_pairing_attr="none" + +# prefix indicator +tmux_conf_theme_prefix="⌨" # U+2328 +tmux_conf_theme_prefix_fg="none" +tmux_conf_theme_prefix_bg="none" +tmux_conf_theme_prefix_attr="none" + +# mouse indicator +tmux_conf_theme_mouse="↗" # U+2197 +tmux_conf_theme_mouse_fg="none" +tmux_conf_theme_mouse_bg="none" +tmux_conf_theme_mouse_attr="none" + +# root indicator +tmux_conf_theme_root="!" +tmux_conf_theme_root_fg="none" +tmux_conf_theme_root_bg="none" +tmux_conf_theme_root_attr="bold,blink" + +# synchronized indicator +tmux_conf_theme_synchronized="⚏" # U+268F +tmux_conf_theme_synchronized_fg="none" +tmux_conf_theme_synchronized_bg="none" +tmux_conf_theme_synchronized_attr="none" + +# battery bar symbols +tmux_conf_battery_bar_symbol_full="◼" +tmux_conf_battery_bar_symbol_empty="◻" +#tmux_conf_battery_bar_symbol_full="♥" +#tmux_conf_battery_bar_symbol_empty="·" + +# battery bar length (in number of symbols), possible values are: +# - auto +# - a number, e.g. 5 +tmux_conf_battery_bar_length="auto" + +# battery bar palette, possible values are: +# - gradient (default) +# - heat +# - "colour_full_fg,colour_empty_fg,colour_bg" +# - gradient(colour_fg_1,colour_fg_2,...,colour_fg_n) +tmux_conf_battery_bar_palette="gradient" +#tmux_conf_battery_bar_palette="#d70000,#e4e4e4,#000000" # red, white, black +#tmux_conf_battery_bar_palette="gradient(#00afff,#47a2ff,#7c91ff,#ac7afb,#d65be2,#e163df,#eb6cdd,#f475db,#ec9ff1,#eac3fe,#efe2ff,#ffffff)" + +# battery hbar palette, possible values are: +# - gradient (default) +# - heat +# - "colour_low,colour_half,colour_full" +# - gradient(colour_fg_1,colour_fg_2,...,colour_fg_n) +tmux_conf_battery_hbar_palette="gradient" +#tmux_conf_battery_hbar_palette="#d70000,#ff5f00,#5fff00" # red, orange, green +#tmux_conf_battery_hbar_palette="gradient(#00afff,#47a2ff,#7c91ff,#ac7afb,#d65be2,#e163df,#eb6cdd,#f475db,#ec9ff1,#eac3fe,#efe2ff,#ffffff)" + +# battery vbar palette, possible values are: +# - gradient (default) +# - heat +# - "colour_low,colour_half,colour_full" +# - gradient(colour_fg_1,colour_fg_2,...,colour_fg_n) +tmux_conf_battery_vbar_palette="gradient" +#tmux_conf_battery_vbar_palette="#d70000,#ff5f00,#5fff00" # red, orange, green +#tmux_conf_battery_vbar_palette="gradient(#00afff,#47a2ff,#7c91ff,#ac7afb,#d65be2,#e163df,#eb6cdd,#f475db,#ec9ff1,#eac3fe,#efe2ff,#ffffff)" + +# symbols used to indicate whether battery is charging or discharging +tmux_conf_battery_status_charging="↑" # U+2191 +tmux_conf_battery_status_discharging="↓" # U+2193 +#tmux_conf_battery_status_charging="🔌" # U+1F50C +#tmux_conf_battery_status_discharging="🔋" # U+1F50B + +# clock style (when you hit <prefix> + t) +# you may want to use %I:%M %p in place of %R in tmux_conf_theme_status_right +tmux_conf_theme_clock_colour="$tmux_conf_theme_colour_4" +tmux_conf_theme_clock_style="24" + + +# -- clipboard ----------------------------------------------------------------- + +# in copy mode, copying selection also copies to the OS clipboard +# - true +# - false (default) +# - disabled +# on Linux, this requires xsel, xclip or wl-copy +tmux_conf_copy_to_os_clipboard=false + + +# -- urlscan ------------------------------------------------------------------- + +# options passed to urlscan +tmux_conf_urlscan_options="--compact --dedupe" + + +# -- user customizations ------------------------------------------------------- + +# this is the place to override or undo settings + +# increase history size +#set -g history-limit 10000 + +# start with mouse mode enabled +set -g mouse on + +# force Vi mode +# really you should export VISUAL or EDITOR environment variable, see manual +#set -g status-keys vi +#set -g mode-keys vi + +# replace C-b by C-a instead of using both prefixes +# set -gu prefix2 +# unbind C-a +# unbind C-b +# set -g prefix C-a +# bind C-a send-prefix + +# if you don't want Oh my tmux! to alter a binding or a setting, use #!important +# bind c new-window -c '#{pane_current_path}' #!important + +# display a message after toggling mouse support +bind m run "cut -c3- '#{TMUX_CONF}' | sh -s _toggle_mouse" \; display 'mouse #{?#{mouse},on,off}' + +# cycle through windows/tabs with Shift+Left/Right keys (no prefix needed) +bind -n S-Left previous-window +bind -n S-Right next-window + +# cycle through sessions/workspaces with Shift+Up/Down keys (no prefix needed) +bind -n S-Up switch-client -p +bind -n S-Down switch-client -n + +# move status line to top +#set -g status-position top + +# cleaner pane borders (VS Code-like) +set -g pane-border-lines single +set -g pane-border-style fg=brightblack +set -g pane-active-border-style fg=blue + + +# -- tpm ----------------------------------------------------------------------- + +# while I don't use tpm myself, many people requested official support so here +# is a seamless integration that automatically installs plugins in parallel + +# whenever a plugin introduces a variable to be used in 'status-left' or +# 'status-right', you can use it in 'tmux_conf_theme_status_left' and +# 'tmux_conf_theme_status_right' variables. + +# by default, launching tmux will update tpm and all plugins +# - true (default) +# - false +tmux_conf_update_plugins_on_launch=true + +# by default, reloading the configuration will update tpm and all plugins +# - true (default) +# - false +tmux_conf_update_plugins_on_reload=true + +# by default, reloading the configuration will uninstall tpm and plugins when no +# plugins are enabled +# - true (default) +# - false +tmux_conf_uninstall_plugins_on_reload=true + +# /!\ the tpm bindings differ slightly from upstream: +# - installing plugins: <prefix> + I +# - uninstalling plugins: <prefix> + Alt + u +# - updating plugins: <prefix> + u + +# /!\ do not add set -g @plugin 'tmux-plugins/tpm' +# /!\ do not add run '~/.tmux/plugins/tpm/tpm' + +# to enable a plugin, use the 'set -g @plugin' syntax: +# visit https://github.com/tmux-plugins for available plugins +#set -g @plugin 'tmux-plugins/tmux-copycat' +#set -g @plugin 'tmux-plugins/tmux-cpu' +#set -g @plugin 'tmux-plugins/tmux-resurrect' +#set -g @plugin 'tmux-plugins/tmux-continuum' +#set -g @continuum-restore 'on' + + +# -- custom variables ---------------------------------------------------------- + +# to define a custom #{foo} variable, define a POSIX shell function between the +# '# EOF' and the '# "$@"' lines. Please note that the opening brace { character +# must be on the same line as the function name otherwise the parse won't detect +# it. +# +# then, use #{foo} in e.g. the 'tmux_conf_theme_status_left' or the +# 'tmux_conf_theme_status_right' variables. + +# ------------------------------------------------------------------------------ + +# # /!\ do not remove the following line +# EOF +# +# # /!\ do not "uncomment" the functions: the leading "# " characters are needed +# +# # usage: #{weather} +# weather() { # see https://github.com/chubin/wttr.in#one-line-output +# curl -f -s -m 2 'wttr.in?format=3' || printf '\n' # /!\ make sure curl is installed +# sleep 900 # sleep for 15 minutes, throttle network requests whatever the value of status-interval +# } +# +# # usage: #{online} +# online() { +# ping -c 1 1.1.1.1 >/dev/null 2>&1 && printf '✔' || printf '✘' +# } +# +# # usage: #{wan_ip_v4} +# wan_ip_v4() { +# curl -f -s -m 2 -4 ifconfig.me +# sleep 300 # sleep for 5 minutes, throttle network requests whatever the value of status-interval +# } +# +# # usage: #{wan_ip_v6} +# wan_ip_v6() { +# curl -f -s -m 2 -6 ifconfig.me +# sleep 300 # sleep for 5 minutes, throttle network requests whatever the value of status-interval +# } +# +# # usage: #{github_stars}, #{github_stars tmux/tmux}, ... +# github_stars() { +# repository=${1##*https://github.com/} +# repository=${repository%% *} +# repository=${repository%%.git} +# url="https://api.github.com/repos/${repository:-gpakosz/.tmux}" +# curl -s "$url" | perl -MJSON::PP=decode_json -CO -0777 -E '$response = decode_json(readline *STDIN); say ($response->{stargazers_count})' +# sleep 300 # sleep for 5 minutes, throttle network requests whatever the value of status-interval +# } +# +# "$@" +# # /!\ do not remove the previous line +# # do not write below this line diff --git a/private_dot_config/btop/btop.conf.tmpl b/private_dot_config/btop/btop.conf.tmpl new file mode 100644 index 0000000..f313183 --- /dev/null +++ b/private_dot_config/btop/btop.conf.tmpl @@ -0,0 +1,505 @@ +{{- if eq .chezmoi.os "darwin" -}} +#? Config file for btop v. 1.4.5 + +#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes. +#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes" +color_theme = "TTY" + +#* If the theme set background should be shown, set to False if you want terminal background transparency. +theme_background = True + +#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false. +truecolor = True + +#* Set to true to force tty mode regardless if a real tty has been detected or not. +#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols. +force_tty = False + +#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets. +#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box. +#* Use whitespace " " as separator between different presets. +#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty" +presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty" + +#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists. +#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift. +vim_keys = False + +#* Rounded corners on boxes, is ignored if TTY mode is ON. +rounded_corners = True + +#* Default symbols to use for graph creation, "braille", "block" or "tty". +#* "braille" offers the highest resolution but might not be included in all fonts. +#* "block" has half the resolution of braille but uses more common characters. +#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY. +#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view. +graph_symbol = "braille" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_cpu = "default" + +# Graph symbol to use for graphs in gpu box, "default", "braille", "block" or "tty". +graph_symbol_gpu = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_mem = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_net = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_proc = "default" + +#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace. +shown_boxes = "cpu mem net proc" + +#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs. +update_ms = 2000 + +#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct", +#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly. +proc_sorting = "cpu lazy" + +#* Reverse sorting order, True or False. +proc_reversed = False + +#* Show processes as a tree. +proc_tree = False + +#* Use the cpu graph colors in the process list. +proc_colors = True + +#* Use a darkening gradient in the process list. +proc_gradient = True + +#* If process cpu usage should be of the core it's running on or usage of the total available cpu power. +proc_per_core = True + +#* Show process memory as bytes instead of percent. +proc_mem_bytes = True + +#* Show cpu graph for each process. +proc_cpu_graphs = True + +#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate) +proc_info_smaps = False + +#* Show proc box on left side of screen instead of right. +proc_left = False + +#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop). +proc_filter_kernel = False + +#* In tree-view, always accumulate child process resources in the parent process. +proc_aggregate = False + +#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available. +#* Select from a list of detected attributes from the options menu. +cpu_graph_upper = "Auto" + +#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available. +#* Select from a list of detected attributes from the options menu. +cpu_graph_lower = "iowait" + +#* If gpu info should be shown in the cpu box. Available values = "Auto", "On" and "Off". +show_gpu_info = "Auto" + +#* Toggles if the lower CPU graph should be inverted. +cpu_invert_lower = True + +#* Set to True to completely disable the lower CPU graph. +cpu_single_graph = False + +#* Show cpu box at bottom of screen instead of top. +cpu_bottom = False + +#* Shows the system uptime in the CPU box. +show_uptime = True + +#* Shows the CPU package current power consumption in watts. Requires running `make setcap` or `make setuid` or running with sudo. +show_cpu_watts = True + +#* Show cpu temperature. +check_temp = True + +#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors. +cpu_sensor = "Auto" + +#* Show temperatures for cpu cores also if check_temp is True and sensors has been found. +show_coretemp = True + +#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core. +#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine. +#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries. +#* Example: "4:0 5:1 6:3" +cpu_core_map = "" + +#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine". +temp_scale = "celsius" + +#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024. +base_10_sizes = False + +#* Show CPU frequency. +show_cpu_freq = True + +#* Draw a clock at top of screen, formatting according to strftime, empty string to disable. +#* Special formatting: /host = hostname | /user = username | /uptime = system uptime +clock_format = "%X" + +#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort. +background_update = True + +#* Custom cpu model name, empty string to disable. +custom_cpu_name = "" + +#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ". +#* Only disks matching the filter will be shown. Prepend exclude= to only show disks not matching the filter. Examples: disk_filter="/boot /home/user", disks_filter="exclude=/boot /home/user" +disks_filter = "" + +#* Show graphs instead of meters for memory values. +mem_graphs = True + +#* Show mem box below net box instead of above. +mem_below_net = False + +#* Count ZFS ARC in cached and available memory. +zfs_arc_cached = True + +#* If swap memory should be shown in memory box. +show_swap = True + +#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk. +swap_disk = True + +#* If mem box should be split to also show disks info. +show_disks = True + +#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar. +only_physical = False + +#* Read disks list from /etc/fstab. This also disables only_physical. +use_fstab = True + +#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool) +zfs_hide_datasets = False + +#* Set to true to show available disk space for privileged users. +disk_free_priv = True + +#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view. +show_io_stat = True + +#* Toggles io mode for disks, showing big graphs for disk read/write speeds. +io_mode = False + +#* Set to True to show combined read/write io graphs in io mode. +io_graph_combined = False + +#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ". +#* Example: "/mnt/media:100 /:20 /boot:1". +io_graph_speeds = "" + +#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False. +net_download = 100 + +net_upload = 100 + +#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest. +net_auto = True + +#* Sync the auto scaling for download and upload to whichever currently has the highest scale. +net_sync = True + +#* Starts with the Network Interface specified here. +net_iface = "" + +#* "True" shows bitrates in base 10 (Kbps, Mbps). "False" shows bitrates in binary sizes (Kibps, Mibps, etc.). "Auto" uses base_10_sizes. +base_10_bitrate = "Auto" + +#* Show battery stats in top right if battery is present. +show_battery = True + +#* Which battery to use if multiple are present. "Auto" for auto detection. +selected_battery = "Auto" + +#* Show power stats of battery next to charge indicator. +show_battery_watts = True + +#* Set loglevel for "~/.config/btop/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG". +#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info. +log_level = "WARNING" + +#* Measure PCIe throughput on NVIDIA cards, may impact performance on certain cards. +nvml_measure_pcie_speeds = True + +#* Measure PCIe throughput on AMD cards, may impact performance on certain cards. +rsmi_measure_pcie_speeds = True + +#* Horizontally mirror the GPU graph. +gpu_mirror_graph = True + +#* Custom gpu0 model name, empty string to disable. +custom_gpu_name0 = "" + +#* Custom gpu1 model name, empty string to disable. +custom_gpu_name1 = "" + +#* Custom gpu2 model name, empty string to disable. +custom_gpu_name2 = "" + +#* Custom gpu3 model name, empty string to disable. +custom_gpu_name3 = "" + +#* Custom gpu4 model name, empty string to disable. +custom_gpu_name4 = "" + +#* Custom gpu5 model name, empty string to disable. +custom_gpu_name5 = "" +{{- else if eq .chezmoi.os "linux" -}} +#? Config file for btop v. 1.3.0 + +#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes. +#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes" +color_theme = "TTY" + +#* If the theme set background should be shown, set to False if you want terminal background transparency. +theme_background = True + +#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false. +truecolor = True + +#* Set to true to force tty mode regardless if a real tty has been detected or not. +#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols. +force_tty = False + +#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets. +#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box. +#* Use whitespace " " as separator between different presets. +#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty" +presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty" + +#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists. +#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift. +vim_keys = False + +#* Rounded corners on boxes, is ignored if TTY mode is ON. +rounded_corners = True + +#* Default symbols to use for graph creation, "braille", "block" or "tty". +#* "braille" offers the highest resolution but might not be included in all fonts. +#* "block" has half the resolution of braille but uses more common characters. +#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY. +#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view. +graph_symbol = "braille" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_cpu = "default" + +# Graph symbol to use for graphs in gpu box, "default", "braille", "block" or "tty". +graph_symbol_gpu = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_mem = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_net = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_proc = "default" + +#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace. +shown_boxes = "cpu mem net proc" + +#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs. +update_ms = 2000 + +#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct", +#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly. +proc_sorting = "cpu direct" + +#* Reverse sorting order, True or False. +proc_reversed = False + +#* Show processes as a tree. +proc_tree = False + +#* Use the cpu graph colors in the process list. +proc_colors = True + +#* Use a darkening gradient in the process list. +proc_gradient = True + +#* If process cpu usage should be of the core it's running on or usage of the total available cpu power. +proc_per_core = True + +#* Show process memory as bytes instead of percent. +proc_mem_bytes = True + +#* Show cpu graph for each process. +proc_cpu_graphs = True + +#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate) +proc_info_smaps = False + +#* Show proc box on left side of screen instead of right. +proc_left = False + +#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop). +proc_filter_kernel = False + +#* In tree-view, always accumulate child process resources in the parent process. +proc_aggregate = False + +#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available. +#* Select from a list of detected attributes from the options menu. +cpu_graph_upper = "Auto" + +#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available. +#* Select from a list of detected attributes from the options menu. +cpu_graph_lower = "iowait" + +#* If gpu info should be shown in the cpu box. Available values = "Auto", "On" and "Off". +show_gpu_info = "Auto" + +#* Toggles if the lower CPU graph should be inverted. +cpu_invert_lower = True + +#* Set to True to completely disable the lower CPU graph. +cpu_single_graph = False + +#* Show cpu box at bottom of screen instead of top. +cpu_bottom = False + +#* Shows the system uptime in the CPU box. +show_uptime = True + +#* Show cpu temperature. +check_temp = True + +#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors. +cpu_sensor = "Auto" + +#* Show temperatures for cpu cores also if check_temp is True and sensors has been found. +show_coretemp = True + +#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core. +#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine. +#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries. +#* Example: "4:0 5:1 6:3" +cpu_core_map = "" + +#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine". +temp_scale = "celsius" + +#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024. +base_10_sizes = False + +#* Show CPU frequency. +show_cpu_freq = True + +#* Draw a clock at top of screen, formatting according to strftime, empty string to disable. +#* Special formatting: /host = hostname | /user = username | /uptime = system uptime +clock_format = "%X" + +#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort. +background_update = True + +#* Custom cpu model name, empty string to disable. +custom_cpu_name = "" + +#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ". +#* Begin line with "exclude=" to change to exclude filter, otherwise defaults to "most include" filter. Example: disks_filter="exclude=/boot /home/user". +disks_filter = "" + +#* Show graphs instead of meters for memory values. +mem_graphs = True + +#* Show mem box below net box instead of above. +mem_below_net = False + +#* Count ZFS ARC in cached and available memory. +zfs_arc_cached = True + +#* If swap memory should be shown in memory box. +show_swap = True + +#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk. +swap_disk = True + +#* If mem box should be split to also show disks info. +show_disks = True + +#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar. +only_physical = False + +#* Read disks list from /etc/fstab. This also disables only_physical. +use_fstab = True + +#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool) +zfs_hide_datasets = False + +#* Set to true to show available disk space for privileged users. +disk_free_priv = True + +#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view. +show_io_stat = True + +#* Toggles io mode for disks, showing big graphs for disk read/write speeds. +io_mode = False + +#* Set to True to show combined read/write io graphs in io mode. +io_graph_combined = False + +#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ". +#* Example: "/mnt/media:100 /:20 /boot:1". +io_graph_speeds = "" + +#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False. +net_download = 100 + +net_upload = 100 + +#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest. +net_auto = False + +#* Sync the auto scaling for download and upload to whichever currently has the highest scale. +net_sync = True + +#* Starts with the Network Interface specified here. +net_iface = "" + +#* Show battery stats in top right if battery is present. +show_battery = True + +#* Which battery to use if multiple are present. "Auto" for auto detection. +selected_battery = "Auto" + +#* Set loglevel for "~/.config/btop/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG". +#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info. +log_level = "WARNING" + +#* Measure PCIe throughput on NVIDIA cards, may impact performance on certain cards. +nvml_measure_pcie_speeds = True + +#* Horizontally mirror the GPU graph. +gpu_mirror_graph = True + +#* Custom gpu0 model name, empty string to disable. +custom_gpu_name0 = "" + +#* Custom gpu1 model name, empty string to disable. +custom_gpu_name1 = "" + +#* Custom gpu2 model name, empty string to disable. +custom_gpu_name2 = "" + +#* Custom gpu3 model name, empty string to disable. +custom_gpu_name3 = "" + +#* Custom gpu4 model name, empty string to disable. +custom_gpu_name4 = "" + +#* Custom gpu5 model name, empty string to disable. +custom_gpu_name5 = "" +{{- end }} diff --git a/private_dot_config/gh/config.yml b/private_dot_config/gh/config.yml new file mode 100644 index 0000000..8b25011 --- /dev/null +++ b/private_dot_config/gh/config.yml @@ -0,0 +1,27 @@ +# The current version of the config schema +version: 1 +# What protocol to use when performing git operations. Supported values: ssh, https +git_protocol: https +# What editor gh should run when creating issues, pull requests, etc. If blank, will refer to environment. +editor: +# When to interactively prompt. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled +prompt: enabled +# Preference for editor-based interactive prompting. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled +prefer_editor_prompt: disabled +# A pager program to send command output to, e.g. "less". If blank, will refer to environment. Set the value to "cat" to disable the pager. +pager: +# Aliases allow you to create nicknames for gh commands +aliases: + co: pr checkout +# The path to a unix socket through which to send HTTP connections. If blank, HTTP traffic will be handled by net/http.DefaultTransport. +http_unix_socket: +# What web browser gh should use when opening URLs. If blank, will refer to environment. +browser: +# Whether to display labels using their RGB hex color codes in terminals that support truecolor. Supported values: enabled, disabled +color_labels: disabled +# Whether customizable, 4-bit accessible colors should be used. Supported values: enabled, disabled +accessible_colors: disabled +# Whether an accessible prompter should be used. Supported values: enabled, disabled +accessible_prompter: disabled +# Whether to use a animated spinner as a progress indicator. If disabled, a textual progress indicator is used instead. Supported values: enabled, disabled +spinner: enabled diff --git a/private_dot_config/ghostty/config.tmpl b/private_dot_config/ghostty/config.tmpl new file mode 100644 index 0000000..505f6f4 --- /dev/null +++ b/private_dot_config/ghostty/config.tmpl @@ -0,0 +1,56 @@ +# This is the configuration file for Ghostty. +# +# The template does not set any default options, since Ghostty ships +# with sensible defaults for all options. Users should only need to set +# options that they want to change from the default. +# +# Run `ghostty +show-config --default --docs` to view a list of +# all available config options and their default values. +# +# Additionally, each config option is also explained in detail +# on Ghostty's website, at https://ghostty.org/docs/config. + +# Config syntax crash course +# ========================== +# # The config file consists of simple key-value pairs, +# # separated by equals signs. +# font-family = Iosevka +# window-padding-x = 2 +# +# # Spacing around the equals sign does not matter. +# # All of these are identical: +# key=value +# key= value +# key =value +# key = value +# +# # Any line beginning with a # is a comment. It's not possible to put +# # a comment after a config option, since it would be interpreted as a +# # part of the value. For example, this will have a value of "#123abc": +# background = #123abc +# +# # Empty values are used to reset config keys to default. +# key = +# +# # Some config options have unique syntaxes for their value, +# # which is explained in the docs for that config option. +# # Just for example: +# resize-overlay-duration = 4s 200ms +shell-integration-features = sudo +keybind = shift+enter=text:\n +#font-family = "CaskaydiaMono Nerd Font Mono ExtraLight" +font-family = "JetBrainsMono NFM Thin" +font-family-italic = "JetBrainsMono NFM Thin" +font-family-bold = "JetBrainsMono NFM Light" +font-family-bold-italic = "JetBrainsMono NFM Light" + +# VS Code-like visual improvements +font-size = 13 +adjust-cell-height = 15% +window-padding-x = 12 +window-padding-y = 8 +cursor-style = bar +cursor-style-blink = true +{{ if eq .chezmoi.os "darwin" }} +background-opacity = 1.00 +{{- end }} diff --git a/private_dot_config/git/ignore b/private_dot_config/git/ignore new file mode 100644 index 0000000..66d62f8 --- /dev/null +++ b/private_dot_config/git/ignore @@ -0,0 +1 @@ +**/.claude/settings.local.json diff --git a/private_dot_config/herdr/config.toml b/private_dot_config/herdr/config.toml new file mode 100644 index 0000000..22a4a46 --- /dev/null +++ b/private_dot_config/herdr/config.toml @@ -0,0 +1,35 @@ +[ui] +agent_panel_sort = "spaces" + +[ui.sound] +enabled = false + +[ui.toast] +delivery = "herdr" + +[experimental] +kitty_graphics = true + +[keys] +next_tab = "shift+right" +previous_tab = "shift+left" +next_workspace = "shift+down" +previous_workspace = "shift+up" + +[[keys.command]] +key = "prefix+p" +type = "plugin_action" +command = "jt.command-palette.open" +description = "Command palette" + +[[keys.command]] +key = "cmd+r" +type = "plugin_action" +command = "persiyanov.reviewr.toggle" +description = "reviewr: toggle sidebar" + +[[keys.command]] +key = "prefix+shift+a" +type = "plugin_action" +command = "herdr-spreader.apply" +description = "Spreader: apply layout" diff --git a/private_dot_config/nvim/LICENSE b/private_dot_config/nvim/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/private_dot_config/nvim/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/private_dot_config/nvim/PLUGIN_CHEATSHEET.md b/private_dot_config/nvim/PLUGIN_CHEATSHEET.md new file mode 100644 index 0000000..97ae31f --- /dev/null +++ b/private_dot_config/nvim/PLUGIN_CHEATSHEET.md @@ -0,0 +1,210 @@ +# LazyVim Plugin Cheatsheet + +Quick reference for the custom plugins in your configuration. + +--- + +## Git Tools (`lua/plugins/git.lua`) + +### git-blame.nvim +Shows git blame information at the end of each line. + +**Commands:** +- `:GitBlameToggle` - Toggle git blame on/off +- `:GitBlameEnable` - Enable git blame +- `:GitBlameDisable` - Disable git blame +- `:GitBlameOpenCommitURL` - Open commit in browser +- `:GitBlameCopySHA` - Copy commit SHA to clipboard + +**Default:** Enabled on startup + +--- + +### diffview.nvim +Advanced git diff and history viewer. + +**Key Bindings:** +- `<leader>gd` - Open diff view of current changes +- `<leader>gh` - View current file's git history +- `<leader>gH` - View entire repo's git history + +**Commands:** +- `:DiffviewOpen` - Open diff view +- `:DiffviewClose` - Close diff view +- `:DiffviewFileHistory` - View repo history +- `:DiffviewFileHistory %` - View current file history +- `:DiffviewRefresh` - Refresh the diff view + +**In Diffview:** +- `]c` / `[c` - Jump to next/previous change +- `<tab>` / `<S-tab>` - Select next/previous file +- `gf` - Open file in new tab +- `q` - Close diffview + +--- + +## UI Enhancements (`lua/plugins/ui-enhancements.lua`) + +### render-markdown.nvim +Renders markdown with better formatting while editing. + +**Features:** +- Automatically styles headers, lists, code blocks +- Works only in markdown files +- No commands needed - always active in `.md` files + +**Toggle:** Disable by setting `enabled = false` in config + +--- + +### hlargs.nvim +Highlights function arguments with different colors. + +**Features:** +- Automatically highlights function parameters +- Helps distinguish arguments visually +- Works across all supported languages + +**No commands needed** - always active + +--- + +### twilight.nvim +Dims inactive code to focus on current block. + +**Key Bindings:** +- `<leader>ut` - Toggle Twilight mode + +**Commands:** +- `:Twilight` - Toggle twilight +- `:TwilightEnable` - Enable twilight +- `:TwilightDisable` - Disable twilight + +**Best for:** Focusing on specific functions or code blocks + +--- + +### theme-hub.nvim +Browse and switch colorschemes with live preview. + +**Key Bindings:** +- `<leader>uT` - Open Theme Hub + +**Commands:** +- `:ThemeHub` - Open theme browser + +**In Theme Hub:** +- `j` / `k` - Navigate themes +- `<CR>` - Apply selected theme +- `<Esc>` / `q` - Close without applying + +--- + +## Search Tools (`lua/plugins/search.lua`) + +### nvim-hlslens +Enhanced search highlighting with match counts. + +**Key Bindings:** +- `n` - Next search result (enhanced) +- `N` - Previous search result (enhanced) +- `*` - Search word under cursor (enhanced) +- `#` - Search word backward (enhanced) +- `g*` / `g#` - Search without word boundaries + +**Features:** +- Shows match count (e.g., "3/15") +- Floating window near cursor +- Automatically clears after cursor movement + +--- + +### search-replace.nvim +Advanced multi-buffer search and replace. + +**Single Buffer Operations:** +- `<leader>rs` - Search/replace in visual selection +- `<leader>ro` - Search/replace (open prompt) +- `<leader>rw` - Search/replace word under cursor +- `<leader>rW` - Search/replace WORD under cursor +- `<leader>re` - Search/replace expression +- `<leader>rf` - Search/replace filename + +**Multi-Buffer Operations:** +- `<leader>rbs` - Search/replace selections (all buffers) +- `<leader>rbo` - Search/replace open (all buffers) +- `<leader>rbw` - Search/replace word (all buffers) +- `<leader>rbW` - Search/replace WORD (all buffers) +- `<leader>rbe` - Search/replace expression (all buffers) +- `<leader>rbf` - Search/replace filename (all buffers) + +**Commands:** +- `:SearchReplaceSingleBuffer` - Single buffer mode +- `:SearchReplaceMultiBuffer` - Multi-buffer mode +- `:SearchReplaceWithinVisualSelection` - Within selection + +--- + +## File Type Tools (`lua/plugins/filetypes.lua`) + +### csvview.nvim +Formats and aligns CSV/TSV files for better readability. + +**Key Bindings:** +- `<leader>cv` - Toggle CSV view (only in CSV/TSV files) + +**Commands:** +- `:CsvViewEnable` - Enable CSV formatting +- `:CsvViewDisable` - Disable CSV formatting +- `:CsvViewToggle` - Toggle CSV view + +**File Types:** Automatically works with `.csv` and `.tsv` files + +--- + +## Built-in LazyVim Extras + +These are enabled via `lazyvim.json` and have their own keybindings: + +### Telescope +- `<leader>ff` - Find files +- `<leader>fg` - Live grep +- `<leader>fb` - Browse buffers +- `<leader>fh` - Help tags + +### Neo-tree +- `<leader>e` - Toggle file explorer +- `<leader>E` - Toggle file explorer (current file) + +### Harpoon2 +- `<leader>h` - Harpoon menu +- `<leader>a` - Add file to harpoon + +### Aerial (Code Outline) +- `<leader>cs` - Open symbols outline + +### Project Management +- `<leader>fp` - Find project + +--- + +## Tips + +1. **Enable/Disable Plugins:** Edit the respective file in `lua/plugins/` and set `enabled = false` +2. **View All Keymaps:** Press `<leader>sk` (search keymaps) in Telescope +3. **Check Plugin Status:** Run `:Lazy` to see installed plugins +4. **Update Plugins:** Run `:Lazy update` +5. **View Help:** Most plugins have `:help <plugin-name>` documentation + +--- + +## Plugin Files Location + +``` +~/.config/nvim/lua/plugins/ +├── git.lua # Git blame & diffview +├── ui-enhancements.lua # Render-markdown, hlargs, twilight, theme-hub +├── search.lua # Hlslens, search-replace +├── filetypes.lua # CSV viewer +└── markdown.lua # Markdown-specific config +``` diff --git a/private_dot_config/nvim/README.md b/private_dot_config/nvim/README.md new file mode 100644 index 0000000..185280b --- /dev/null +++ b/private_dot_config/nvim/README.md @@ -0,0 +1,4 @@ +# 💤 LazyVim + +A starter template for [LazyVim](https://github.com/LazyVim/LazyVim). +Refer to the [documentation](https://lazyvim.github.io/installation) to get started. diff --git a/private_dot_config/nvim/dot_gitignore b/private_dot_config/nvim/dot_gitignore new file mode 100644 index 0000000..f11e9dd --- /dev/null +++ b/private_dot_config/nvim/dot_gitignore @@ -0,0 +1,9 @@ +tt.* +.tests +doc/tags +debug +.repro +foo.* +*.log +data +.claude diff --git a/private_dot_config/nvim/dot_neoconf.json b/private_dot_config/nvim/dot_neoconf.json new file mode 100644 index 0000000..7c48087 --- /dev/null +++ b/private_dot_config/nvim/dot_neoconf.json @@ -0,0 +1,15 @@ +{ + "neodev": { + "library": { + "enabled": true, + "plugins": true + } + }, + "neoconf": { + "plugins": { + "lua_ls": { + "enabled": true + } + } + } +} diff --git a/private_dot_config/nvim/init.lua b/private_dot_config/nvim/init.lua new file mode 100644 index 0000000..2514f9e --- /dev/null +++ b/private_dot_config/nvim/init.lua @@ -0,0 +1,2 @@ +-- bootstrap lazy.nvim, LazyVim and your plugins +require("config.lazy") diff --git a/private_dot_config/nvim/lazyvim.json b/private_dot_config/nvim/lazyvim.json new file mode 100644 index 0000000..67fe419 --- /dev/null +++ b/private_dot_config/nvim/lazyvim.json @@ -0,0 +1,28 @@ +{ + "extras": [ + "lazyvim.plugins.extras.ai.claudecode", + "lazyvim.plugins.extras.coding.yanky", + "lazyvim.plugins.extras.editor.aerial", + "lazyvim.plugins.extras.editor.harpoon2", + "lazyvim.plugins.extras.editor.illuminate", + "lazyvim.plugins.extras.editor.mini-diff", + "lazyvim.plugins.extras.editor.neo-tree", + "lazyvim.plugins.extras.editor.refactoring", + "lazyvim.plugins.extras.editor.telescope", + "lazyvim.plugins.extras.lang.docker", + "lazyvim.plugins.extras.lang.go", + "lazyvim.plugins.extras.lang.json", + "lazyvim.plugins.extras.lang.python", + "lazyvim.plugins.extras.lang.yaml", + "lazyvim.plugins.extras.ui.smear-cursor", + "lazyvim.plugins.extras.ui.treesitter-context", + "lazyvim.plugins.extras.util.chezmoi", + "lazyvim.plugins.extras.util.dot", + "lazyvim.plugins.extras.util.project" + ], + "install_version": 8, + "news": { + "NEWS.md": "11866" + }, + "version": 8 +} \ No newline at end of file diff --git a/private_dot_config/nvim/lua/config/autocmds.lua b/private_dot_config/nvim/lua/config/autocmds.lua new file mode 100644 index 0000000..4221e75 --- /dev/null +++ b/private_dot_config/nvim/lua/config/autocmds.lua @@ -0,0 +1,8 @@ +-- Autocmds are automatically loaded on the VeryLazy event +-- Default autocmds that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/autocmds.lua +-- +-- Add any additional autocmds here +-- with `vim.api.nvim_create_autocmd` +-- +-- Or remove existing autocmds by their group name (which is prefixed with `lazyvim_` for the defaults) +-- e.g. vim.api.nvim_del_augroup_by_name("lazyvim_wrap_spell") diff --git a/private_dot_config/nvim/lua/config/keymaps.lua b/private_dot_config/nvim/lua/config/keymaps.lua new file mode 100644 index 0000000..e0598a9 --- /dev/null +++ b/private_dot_config/nvim/lua/config/keymaps.lua @@ -0,0 +1,15 @@ +-- Keymaps are automatically loaded on the VeryLazy event +-- Default keymaps that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/keymaps.lua +-- Add any additional keymaps here +-- +-- -- Copy relative path +vim.keymap.set("n", "<leader>cy", function() + vim.fn.setreg("+", vim.fn.expand("%")) + print("Copied relative path: " .. vim.fn.expand("%")) +end, { desc = "Copy relative path" }) + +-- Copy absolute path +vim.keymap.set("n", "<leader>cY", function() + vim.fn.setreg("+", vim.fn.expand("%:p")) + print("Copied absolute path: " .. vim.fn.expand("%:p")) +end, { desc = "Copy absolute path" }) diff --git a/private_dot_config/nvim/lua/config/lazy.lua b/private_dot_config/nvim/lua/config/lazy.lua new file mode 100644 index 0000000..d73bfa1 --- /dev/null +++ b/private_dot_config/nvim/lua/config/lazy.lua @@ -0,0 +1,53 @@ +local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" +if not (vim.uv or vim.loop).fs_stat(lazypath) then + local lazyrepo = "https://github.com/folke/lazy.nvim.git" + local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath }) + if vim.v.shell_error ~= 0 then + vim.api.nvim_echo({ + { "Failed to clone lazy.nvim:\n", "ErrorMsg" }, + { out, "WarningMsg" }, + { "\nPress any key to exit..." }, + }, true, {}) + vim.fn.getchar() + os.exit(1) + end +end +vim.opt.rtp:prepend(lazypath) + +require("lazy").setup({ + spec = { + -- add LazyVim and import its plugins + { "LazyVim/LazyVim", import = "lazyvim.plugins" }, + -- import/override with your plugins + { import = "plugins" }, + }, + defaults = { + -- By default, only LazyVim plugins will be lazy-loaded. Your custom plugins will load during startup. + -- If you know what you're doing, you can set this to `true` to have all your custom plugins lazy-loaded by default. + lazy = false, + -- It's recommended to leave version=false for now, since a lot the plugin that support versioning, + -- have outdated releases, which may break your Neovim install. + version = false, -- always use the latest git commit + -- version = "*", -- try installing the latest stable version for plugins that support semver + }, + install = { colorscheme = { "tokyonight", "habamax" } }, + checker = { + enabled = true, -- check for plugin updates periodically + notify = false, -- notify on update + }, -- automatically check for plugin updates + performance = { + rtp = { + -- disable some rtp plugins + disabled_plugins = { + "gzip", + -- "matchit", + -- "matchparen", + -- "netrwPlugin", + "tarPlugin", + "tohtml", + "tutor", + "zipPlugin", + }, + }, + }, +}) diff --git a/private_dot_config/nvim/lua/config/options.lua b/private_dot_config/nvim/lua/config/options.lua new file mode 100644 index 0000000..861dc80 --- /dev/null +++ b/private_dot_config/nvim/lua/config/options.lua @@ -0,0 +1,82 @@ +-- Options are automatically loaded before lazy.nvim startup +-- Default options that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/options.lua +-- Add any additional options here + +-- Clipboard provider. LazyVim disables `clipboard=unnamedplus` under SSH, so +-- select the right provider and explicitly re-enable clipboard-linked yanks: +-- * tmux -> tmux's own clipboard buffer (readable) +-- * SSH/remote -> OSC 52 copy-only (see note below) +-- * local macOS -> native pbcopy/pbpaste. +-- Do NOT use OSC 52 locally: yanky's `sync_with_ring` reads the system +-- clipboard, and OSC 52 reads depend on the terminal answering a query, which +-- Ghostty refuses by default -> OSC 52 error + yanky stack trace. Neovim 0.12 +-- also auto-prefers OSC 52 over pbcopy, so the macOS provider must be explicit. +if vim.env.TMUX then + vim.g.clipboard = "tmux" +elseif vim.env.SSH_CONNECTION then + -- Remote session (bare SSH, or a multiplexer like herdr that exports no env + -- var of its own). Copy via OSC 52 so yanks reach the OUTER terminal's + -- clipboard, but NEVER read the clipboard back: OSC 52 reads require the + -- terminal to answer a query, which Warp/herdr don't, so a read returns stale + -- data (or hangs) and blows up yanky's ring sync. Trade-off: pasting FROM the + -- local host INTO nvim over SSH won't work here -- use the terminal's own + -- paste in insert mode. + -- + -- The paste function below must NOT call vim.fn.getreg('"')/'*'/'+' itself: + -- Neovim's clipboard provider (autoload/provider/clipboard.vim) guards + -- against re-entrant provider calls (`s:here`, see nvim#7184), so a getreg() + -- of a clipboard-linked register issued from *inside* a registered paste() + -- callback is swallowed and silently returns empty -- not the register's + -- real content. Cache what copy() was last given instead, and have paste() + -- return that cached value directly. + local osc52 = require("vim.ui.clipboard.osc52") + local last = { lines = {}, regtype = "" } + local function make_copy(reg) + local osc52_copy = osc52.copy(reg) + return function(lines, regtype) + last.lines, last.regtype = lines, regtype + osc52_copy(lines, regtype) + end + end + local function paste_from_cache() + return { last.lines, last.regtype } + end + vim.g.clipboard = { + name = "osc52-copyonly", + copy = { ["+"] = make_copy("+"), ["*"] = make_copy("*") }, + paste = { ["+"] = paste_from_cache, ["*"] = paste_from_cache }, + cache_enabled = 0, + } +elseif vim.fn.has("mac") == 1 then + vim.g.clipboard = { + name = "pbcopy", + copy = { ["+"] = "pbcopy", ["*"] = "pbcopy" }, + paste = { ["+"] = "pbpaste", ["*"] = "pbpaste" }, + cache_enabled = 0, + } +end + +-- Make ordinary y/Y use the selected provider. LazyVim applies its SSH default +-- after this file is read, so restore the option once startup has settled. The +-- schedule keeps this callback after LazyVim's own VeryLazy clipboard callback. +vim.api.nvim_create_autocmd("User", { + pattern = "VeryLazy", + once = true, + callback = function() + vim.schedule(function() + vim.opt.clipboard = "unnamedplus" + end) + end, +}) + +-- VS Code-like visual improvements +vim.opt.cursorline = true -- Highlight current line +-- vim.opt.colorcolumn = "80,120" -- Disabled: renders poorly with some themes +vim.opt.scrolloff = 8 -- Keep 8 lines visible above/below cursor +vim.opt.sidescrolloff = 8 -- Keep 8 columns visible left/right +vim.opt.smoothscroll = true -- Smooth scrolling + +-- Make Shift+movement and mouse selections behave like a conventional editor. +-- Keep `cmd` out of selectmode so v/V continue to enter Vim's Visual mode. +vim.opt.keymodel = { "startsel", "stopsel" } +vim.opt.selectmode = { "key", "mouse" } diff --git a/private_dot_config/nvim/lua/custom/keymaps.lua b/private_dot_config/nvim/lua/custom/keymaps.lua new file mode 100644 index 0000000..d08a0dd --- /dev/null +++ b/private_dot_config/nvim/lua/custom/keymaps.lua @@ -0,0 +1,37 @@ +local completion_state = { + enabled = true, -- current state + backup = { -- store the original values so we can restore them + omnifunc = nil, + completefunc = nil, + completeopt = nil, + }, +} + +vim.keymap.set("n", "<leader>co", function() + if completion_state.enabled then + -- ----- DISABLE ----- + completion_state.backup.omnifunc = vim.opt_local.omnifunc:get() + completion_state.backup.completefunc = vim.opt_local.completefunc:get() + completion_state.backup.completeopt = vim.opt_local.completeopt:get() + + -- Clear the sources + vim.opt_local.omnifunc = "" + vim.opt_local.completefunc = "" + + -- Hide the menu even if something tries to show it + vim.opt_local.completeopt = { "noinsert", "noselect" } + + completion_state.enabled = false + print("⛔ Autocompletion disabled") + else + -- ----- ENABLE ----- + vim.opt_local.omnifunc = completion_state.backup.omnifunc + vim.opt_local.completefunc = completion_state.backup.completefunc + + -- Restore the previous `completeopt` (or a safe default) + vim.opt_local.completeopt = completion_state.backup.completeopt or { "menu", "preview", "noinsert", "noselect" } + + completion_state.enabled = true + print("✅ Autocompletion re‑enabled") + end +end, { desc = "Toggle all autocompletion (omnifunc, completefunc, menu)" }) diff --git a/private_dot_config/nvim/lua/plugins/ai_assist.lua b/private_dot_config/nvim/lua/plugins/ai_assist.lua new file mode 100644 index 0000000..d61092c --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/ai_assist.lua @@ -0,0 +1,35 @@ +return { + dir = vim.fn.expand("~/code/nvim-ai-assist"), + name = "nvim_ai_assist", + lazy = false, + cmd = { "AI", "AIHealth", "AIRefreshContext" }, + keys = { + { "<leader>ai", ":AI<CR>", desc = "AI command (prompt)", mode = "n" }, + { "<leader>aA", ":AI!<CR>", desc = "AI command (auto-execute)", mode = "n" }, + { "<leader>ah", ":AIHealth<CR>", desc = "AI health + context stats", mode = "n" }, + }, + config = function() + require("nvim_ai_assist").setup({ + -- Active: Z.AI Coding Plan (uses ZAI_API_KEY from env) + provider = "zai-coding", + model = "glm-4.7", + max_tokens = 300, + temperature = 0.05, + debug = true, -- writes request/response to ~/.cache/nvim/nvim_ai_assist.log; :AILog to view + context = { + enabled = true, + max_chars = 1500, + refresh_seconds = 60, + }, + + -- Local LM Studio fallback — uncomment to use, and comment out the + -- provider/model lines above. + -- The host is deliberately not written down here: this repo is public, + -- and a LAN address is one of the things the tier split exists to keep + -- out of it. Put yours in and do not commit it back. + -- endpoint = "http://<lm-studio-host>:1234/v1/chat/completions", + -- api_key = "lm-studio", + -- model = "qwen3-almost-human-y1-7b", + }) + end, +} diff --git a/private_dot_config/nvim/lua/plugins/blink.lua b/private_dot_config/nvim/lua/plugins/blink.lua new file mode 100644 index 0000000..f8a1f3d --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/blink.lua @@ -0,0 +1,32 @@ +-- Tame blink.cmp completions for prose: +-- * drop the `buffer` (next-word) source everywhere +-- * markdown/md-render buffers use LSP+path only (no snippet callout noise) +-- * disable friendly-snippets' global snippets (datetime/dateMDY/lorem/...) +-- Inline AI suggestions are handled by minuet instead (see minuet.lua). +return { + "saghen/blink.cmp", + opts = function(_, opts) + opts.sources = opts.sources or {} + + -- Drop the buffer (next-word) source everywhere. + opts.sources.default = vim.tbl_filter(function(source) + return source ~= "buffer" + end, opts.sources.default or { "lsp", "path", "snippets" }) + + -- Markdown editing (ft=markdown) and md-render's render buffer (ft=md-render): + -- LSP+path only, so callout snippets like !WARNING don't pop up. md-render + -- flips the buffer filetype between these two as you edit, so cover both. + opts.sources.per_filetype = opts.sources.per_filetype or {} + opts.sources.per_filetype.markdown = { "lsp", "path" } + opts.sources.per_filetype["md-render"] = { "lsp", "path" } + + -- Kill friendly-snippets' GLOBAL snippets (global.json: datetime, dateMDY, + -- lorem, etc.). These load in every filetype regardless of the per_filetype + -- rules above, which is why they leaked through while editing markdown. + -- Per-language snippets (friendly_snippets) are unaffected. + opts.sources.providers = opts.sources.providers or {} + opts.sources.providers.snippets = opts.sources.providers.snippets or {} + opts.sources.providers.snippets.opts = opts.sources.providers.snippets.opts or {} + opts.sources.providers.snippets.opts.global_snippets = {} + end, +} diff --git a/private_dot_config/nvim/lua/plugins/diffview.lua b/private_dot_config/nvim/lua/plugins/diffview.lua new file mode 100644 index 0000000..ce78ed9 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/diffview.lua @@ -0,0 +1,14 @@ +return { + "sindrets/diffview.nvim", + cmd = { "DiffviewOpen", "DiffviewFileHistory", "DiffviewClose" }, + keys = { + { "<leader>dv", "<cmd>DiffviewOpen<cr>", desc = "Open diffview" }, + { "<leader>ds", "<cmd>DiffviewOpen --staged<cr>", desc = "Diffview staged" }, + { "<leader>dh", "<cmd>DiffviewFileHistory %<cr>", desc = "File history (current)" }, + { "<leader>dH", "<cmd>DiffviewFileHistory<cr>", desc = "File history (all)" }, + }, + config = function() + require("diffview").setup({}) + -- removed the vim.keymap.set lines from here + end, +} \ No newline at end of file diff --git a/private_dot_config/nvim/lua/plugins/example.lua b/private_dot_config/nvim/lua/plugins/example.lua new file mode 100644 index 0000000..17f53d6 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/example.lua @@ -0,0 +1,197 @@ +-- since this is just an example spec, don't actually load anything here and return an empty spec +-- stylua: ignore +if true then return {} end + +-- every spec file under the "plugins" directory will be loaded automatically by lazy.nvim +-- +-- In your plugin files, you can: +-- * add extra plugins +-- * disable/enabled LazyVim plugins +-- * override the configuration of LazyVim plugins +return { + -- add gruvbox + { "ellisonleao/gruvbox.nvim" }, + + -- Configure LazyVim to load gruvbox + { + "LazyVim/LazyVim", + opts = { + colorscheme = "gruvbox", + }, + }, + + -- change trouble config + { + "folke/trouble.nvim", + -- opts will be merged with the parent spec + opts = { use_diagnostic_signs = true }, + }, + + -- disable trouble + { "folke/trouble.nvim", enabled = false }, + + -- override nvim-cmp and add cmp-emoji + { + "hrsh7th/nvim-cmp", + dependencies = { "hrsh7th/cmp-emoji" }, + ---@param opts cmp.ConfigSchema + opts = function(_, opts) + table.insert(opts.sources, { name = "emoji" }) + end, + }, + + -- change some telescope options and a keymap to browse plugin files + { + "nvim-telescope/telescope.nvim", + keys = { + -- add a keymap to browse plugin files + -- stylua: ignore + { + "<leader>fp", + function() require("telescope.builtin").find_files({ cwd = require("lazy.core.config").options.root }) end, + desc = "Find Plugin File", + }, + }, + -- change some options + opts = { + defaults = { + layout_strategy = "horizontal", + layout_config = { prompt_position = "top" }, + sorting_strategy = "ascending", + winblend = 0, + }, + }, + }, + + -- add pyright to lspconfig + { + "neovim/nvim-lspconfig", + ---@class PluginLspOpts + opts = { + ---@type lspconfig.options + servers = { + -- pyright will be automatically installed with mason and loaded with lspconfig + pyright = {}, + }, + }, + }, + + -- add tsserver and setup with typescript.nvim instead of lspconfig + { + "neovim/nvim-lspconfig", + dependencies = { + "jose-elias-alvarez/typescript.nvim", + init = function() + require("lazyvim.util").lsp.on_attach(function(_, buffer) + -- stylua: ignore + vim.keymap.set( "n", "<leader>co", "TypescriptOrganizeImports", { buffer = buffer, desc = "Organize Imports" }) + vim.keymap.set("n", "<leader>cR", "TypescriptRenameFile", { desc = "Rename File", buffer = buffer }) + end) + end, + }, + ---@class PluginLspOpts + opts = { + ---@type lspconfig.options + servers = { + -- tsserver will be automatically installed with mason and loaded with lspconfig + tsserver = {}, + }, + -- you can do any additional lsp server setup here + -- return true if you don't want this server to be setup with lspconfig + ---@type table<string, fun(server:string, opts:_.lspconfig.options):boolean?> + setup = { + -- example to setup with typescript.nvim + tsserver = function(_, opts) + require("typescript").setup({ server = opts }) + return true + end, + -- Specify * to use this function as a fallback for any server + -- ["*"] = function(server, opts) end, + }, + }, + }, + + -- for typescript, LazyVim also includes extra specs to properly setup lspconfig, + -- treesitter, mason and typescript.nvim. So instead of the above, you can use: + { import = "lazyvim.plugins.extras.lang.typescript" }, + + -- add more treesitter parsers + { + "nvim-treesitter/nvim-treesitter", + opts = { + ensure_installed = { + "bash", + "html", + "javascript", + "json", + "lua", + "markdown", + "markdown_inline", + "python", + "query", + "regex", + "tsx", + "typescript", + "vim", + "yaml", + }, + }, + }, + + -- since `vim.tbl_deep_extend`, can only merge tables and not lists, the code above + -- would overwrite `ensure_installed` with the new value. + -- If you'd rather extend the default config, use the code below instead: + { + "nvim-treesitter/nvim-treesitter", + opts = function(_, opts) + -- add tsx and treesitter + vim.list_extend(opts.ensure_installed, { + "tsx", + "typescript", + }) + end, + }, + + -- the opts function can also be used to change the default opts: + { + "nvim-lualine/lualine.nvim", + event = "VeryLazy", + opts = function(_, opts) + table.insert(opts.sections.lualine_x, { + function() + return "😄" + end, + }) + end, + }, + + -- or you can return new options to override all the defaults + { + "nvim-lualine/lualine.nvim", + event = "VeryLazy", + opts = function() + return { + --[[add your custom lualine config here]] + } + end, + }, + + -- use mini.starter instead of alpha + { import = "lazyvim.plugins.extras.ui.mini-starter" }, + + -- add jsonls and schemastore packages, and setup treesitter for json, json5 and jsonc + { import = "lazyvim.plugins.extras.lang.json" }, + + -- add any tools you want to have installed below + { + "williamboman/mason.nvim", + opts = { + ensure_installed = { + "stylua", + "shellcheck", + "shfmt", + "flake8", + }, + }, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/filetypes.lua b/private_dot_config/nvim/lua/plugins/filetypes.lua new file mode 100644 index 0000000..102a64e --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/filetypes.lua @@ -0,0 +1,36 @@ +-- Filetype-specific plugins +return { + -- CSV file viewer with column alignment + -- Automatically formats CSV files for better readability + -- Toggle with :CsvViewEnable, :CsvViewDisable + { + "hat0uma/csvview.nvim", + ft = { "csv", "tsv" }, + enabled = true, -- Set to false to disable + opts = { + parser = { + async = true, + delimiter = { + default = ",", + ft = { + tsv = "\t", + }, + }, + comments = { + -- Example: Set comment prefix for specific filetypes + -- ft = { + -- csv = "#", + -- }, + }, + }, + view = { + min_column_width = 5, + spacing = 2, + display_mode = "border", -- Options: "highlight", "border" + }, + }, + keys = { + { "<leader>cv", "<cmd>CsvViewToggle<cr>", desc = "Toggle CSV View", ft = { "csv", "tsv" } }, + }, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/git.lua b/private_dot_config/nvim/lua/plugins/git.lua new file mode 100644 index 0000000..33b394d --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/git.lua @@ -0,0 +1,36 @@ +-- Git enhancement plugins +return { + -- Git blame annotations in virtual text + -- Shows git blame info at the end of each line + -- Toggle with :GitBlameToggle + { + "f-person/git-blame.nvim", + event = "VeryLazy", + opts = { + enabled = true, -- Start enabled + message_template = " <summary> • <date> • <author>", + date_format = "%r", + virtual_text_column = 80, + }, + }, + + -- Advanced git diff viewer + -- Better git diff interface with file history + -- Open with :DiffviewOpen, :DiffviewFileHistory + { + "sindrets/diffview.nvim", + cmd = { "DiffviewOpen", "DiffviewClose", "DiffviewToggleFiles", "DiffviewFocusFiles", "DiffviewFileHistory" }, + opts = { + enhanced_diff_hl = true, + view = { + default = { layout = "diff2_horizontal" }, + file_history = { layout = "diff2_horizontal" }, + }, + }, + keys = { + { "<leader>gd", "<cmd>DiffviewOpen<cr>", desc = "Open Diffview" }, + { "<leader>gh", "<cmd>DiffviewFileHistory %<cr>", desc = "File History" }, + { "<leader>gH", "<cmd>DiffviewFileHistory<cr>", desc = "Repo History" }, + }, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/image.lua b/private_dot_config/nvim/lua/plugins/image.lua new file mode 100644 index 0000000..6daa1e2 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/image.lua @@ -0,0 +1,861 @@ +-- Markdown rendering with inline mermaid via md-render.nvim. +-- +-- Why not snacks.image / diagram.nvim / image.nvim? +-- Those all try to overlay the rendered image on top of the source fenced +-- code block. snacks issue #1384 ("mermaid blocks text behind it") was +-- closed as "not planned" — overlap-in-source is a fundamental limitation +-- of the current kitty-graphics-protocol design in nvim. +-- +-- md-render takes the opposite approach: render mode is a *separate buffer*. +-- Normal mode -> auto-swap to render (full diagrams, no source text in the way). +-- Insert mode -> swap back to source so we can edit. The hop is per-buffer. +-- +-- Ghostty itself is verified by the plugin; the SSH transport shim below is +-- local glue so the terminal never has to read remote-only image paths. +local function use_direct_kitty_bytes() + return true +end + +local function image_debug(message) + if vim.env.MD_RENDER_IMAGE_DEBUG ~= "1" then return end + local path = vim.fn.stdpath("cache") .. "/md-render-image-debug.log" + local line = os.date("%Y-%m-%d %H:%M:%S") .. " " .. message .. "\n" + vim.fn.writefile({ line }, path, "a") +end + +return { + { + "delphinus/md-render.nvim", + version = "*", + ft = { "markdown" }, + dependencies = { + { "nvim-tree/nvim-web-devicons", version = "*" }, + }, + init = function() + -- Ghostty does not normally forward TERM_PROGRAM / + -- GHOSTTY_RESOURCES_DIR into the remote shell. Neovim 0.12 therefore + -- cannot prove Kitty graphics support from the remote side on its own. + -- Herdr panes expose HERDR_ENV=1, but the underlying display path is + -- still Ghostty-compatible when kitty_graphics is enabled. + -- + -- Keep the terminal identity as Ghostty. md-render uses that identity to + -- avoid Ghostty's flaky stored-image re-placement path during redraws. + if use_direct_kitty_bytes() and not vim.env.TERM_PROGRAM then + vim.env.TERM_PROGRAM = "ghostty" + end + + -- In auto mode the render view is a separate, nomodifiable buffer, so + -- normal-mode editing (dd/x/ciw/p/…) does nothing — you'd have to toggle + -- to source first. md-render already solves this for insert-entry keys + -- (i/I/a/A/o/O): pressing one hops to the source at the mapped line and + -- replays the key (see AUTO_INSERT_KEYS in the plugin). It intentionally + -- leaves operator/normal-mode keys alone. + -- + -- We extend that same hop to editing keys, scoped buffer-locally to the + -- render buffer (ft=md-render) only — the real source buffer is never + -- touched. Press dd/x/p/etc. while viewing the render and it transparently + -- drops to source on the right line and performs the edit. (Pure-motion + -- keys h/j/k/w/G/… are deliberately NOT mapped, so you can still navigate + -- the render freely.) Yank keys stay in the read-only render buffer so + -- copying text does not dismiss rendered diagrams. Visual-entry keys hop + -- to source so the resulting selection can be changed safely. + -- NOTE: the plugin sets the render buffer's filetype under + -- `eventignore=all` (preview.lua, so markdown ftplugins/treesitter don't + -- clobber the pre-rendered content), so a `FileType md-render` autocmd + -- never fires. Hook `BufEnter` instead — it fires when `toggle` swaps the + -- render buffer into the window — and guard on filetype + a buffer flag so + -- the maps install exactly once per render buffer. + local HOP_KEYS = { + "d", "D", "c", "C", "x", "X", "s", "S", "r", "R", + "p", "P", "J", ">", "<", "=", "u", "v", "V", ".", + } + local hop_group = vim.api.nvim_create_augroup("MdRenderEditHop", { clear = true }) + vim.api.nvim_create_autocmd({ "BufEnter", "BufWinEnter" }, { + group = hop_group, + desc = "md-render: editing keys hop to source then replay", + callback = function(ev) + if vim.bo[ev.buf].filetype ~= "md-render" then return end + if vim.b[ev.buf].md_render_hop_installed then return end + vim.b[ev.buf].md_render_hop_installed = true + + for _, key in ipairs(HOP_KEYS) do + vim.keymap.set("n", key, function() + -- Preserve a leading count and explicit register (e.g. 3dd, "ayy) + -- so the replayed operator behaves like the keystroke you typed. + local count = vim.v.count + local reg = vim.v.register + local prefix = (reg and reg ~= '"') and ('"' .. reg) or "" + if count > 0 then prefix = prefix .. tostring(count) end + + -- toggle() is synchronous: on return the window already shows the + -- source buffer with the cursor on the mapped line. Feed the key + -- *synchronously* (no vim.schedule) so it enters the typeahead + -- before any follow-up keystroke. That keeps multi-key operators + -- in order — `d` becomes operator-pending and your next key (e.g. + -- the second `d`, or `w`, `ip`) completes it natively in source. + -- (The plugin schedules its insert-entry hop because those are + -- single keys that immediately switch to Insert mode; operators + -- need the ordering guarantee a sync feed provides.) + require("md-render").preview.toggle() + if key == "v" or key == "V" then + -- Enter Visual mode before the user's next queued key arrives. + -- feedkeys() would put v/V behind that key, turning `Vd` into + -- `dV`; :normal! establishes the selection synchronously. + local visual_count = count > 0 and tostring(count) or "" + vim.cmd("normal! " .. visual_count .. key) + else + vim.api.nvim_feedkeys(prefix .. key, "n", false) + end + end, { + buffer = ev.buf, + noremap = true, + silent = true, + desc = "md-render: hop to source then " .. key, + }) + end + end, + }) + end, + keys = { + { + "<leader>mp", + function() require("md-render").preview.show({ max_width = 140 }) end, + ft = "markdown", + desc = "Markdown preview (wide float toggle)", + }, + { + "<leader>mt", + function() require("md-render").preview.show_tab({ max_width = 140 }) end, + ft = "markdown", + desc = "Markdown preview (wide tab toggle)", + }, + { + "<leader>ms", + function() require("md-render").preview.split({ max_width = 140 }) end, + ft = "markdown", + desc = "Markdown source/render split", + }, + { + "<leader>mr", + function() require("md-render").preview.toggle({ max_width = 140 }) end, + ft = "markdown", + desc = "Markdown render toggle in-place", + }, + { "<leader>md", "<Plug>(md-render-demo)", desc = "Markdown render demo" }, + }, + config = function() + local function patch_mermaid_rendering() + local img = require("md-render.image") + if img._thembones_mermaid_patch then return end + img._thembones_mermaid_patch = true + + local MERMAID_SCALE = "4" + local MERMAID_ROWS = 45 + local original_calc_display_size = img.calc_display_size + + img.calc_display_size = function(img_w, img_h, max_cols, max_rows) + if max_rows == 25 then + max_rows = MERMAID_ROWS + end + return original_calc_display_size(img_w, img_h, max_cols, max_rows) + end + + local function mermaid_cache_dir() + local dir = vim.fn.stdpath("cache") .. "/md-render/mermaid" + vim.fn.mkdir(dir, "p") + return dir + end + + local function mermaid_theme_args() + local bg = vim.o.background + local hl = vim.api.nvim_get_hl(0, { name = "NormalFloat", link = false }) + if not hl.bg then + hl = vim.api.nvim_get_hl(0, { name = "Normal", link = false }) + end + local bg_color = hl.bg + if bg == "dark" then + return "dark", bg_color and string.format("#%06x", bg_color) or "#1e1e2e" + end + return "default", bg_color and string.format("#%06x", bg_color) or "#ffffff" + end + + local function mermaid_cache_path(source) + local theme, bg_hex = mermaid_theme_args() + local key = table.concat({ source, theme, bg_hex, "scale=" .. MERMAID_SCALE }, "|") + local hash = vim.fn.sha256(key):sub(1, 16) + return mermaid_cache_dir() .. "/" .. hash .. ".png" + end + + local function mmdc_cmd(input_path, output_path) + local cmd + if vim.fn.executable("mmdc") == 1 then + cmd = { "mmdc" } + elseif vim.fn.executable("npx") == 1 then + cmd = { "npx", "-y", "@mermaid-js/mermaid-cli" } + else + return nil + end + + local theme, bg_hex = mermaid_theme_args() + vim.list_extend(cmd, { + "-i", input_path, + "-o", output_path, + "-t", theme, + "-b", bg_hex, + "-s", MERMAID_SCALE, + }) + return cmd + end + + img.get_mermaid_cached = function(source) + local path = mermaid_cache_path(source) + if vim.fn.filereadable(path) == 1 then + return path + end + return nil + end + + img.render_mermaid = function(source) + local cache_path = mermaid_cache_path(source) + if vim.fn.filereadable(cache_path) == 1 then + return cache_path + end + + local tmp_input = vim.fn.tempname() .. ".mmd" + local f = io.open(tmp_input, "w") + if not f then return nil end + f:write(source) + f:close() + + local cmd = mmdc_cmd(tmp_input, cache_path) + if not cmd then + os.remove(tmp_input) + return nil + end + + vim.system(cmd, { text = true, timeout = 30000 }):wait() + os.remove(tmp_input) + if vim.fn.filereadable(cache_path) == 1 then + return cache_path + end + return nil + end + + -- Callbacks per in-flight cache path, so concurrent requests for the + -- same diagram share one mmdc process instead of racing on the output. + local mermaid_inflight = {} + + img.render_mermaid_async = function(source, callback) + local cache_path = mermaid_cache_path(source) + if vim.fn.filereadable(cache_path) == 1 then + callback(cache_path) + return + end + + if mermaid_inflight[cache_path] then + table.insert(mermaid_inflight[cache_path], callback) + return + end + mermaid_inflight[cache_path] = { callback } + + local function finish(result) + local callbacks = mermaid_inflight[cache_path] + mermaid_inflight[cache_path] = nil + for _, cb in ipairs(callbacks) do + cb(result) + end + end + + local tmp_input = vim.fn.tempname() .. ".mmd" + local f = io.open(tmp_input, "w") + if not f then + finish(nil) + return + end + f:write(source) + f:close() + + local cmd = mmdc_cmd(tmp_input, cache_path) + if not cmd then + os.remove(tmp_input) + finish(nil) + return + end + + vim.system(cmd, { text = true, timeout = 30000 }, function() + vim.schedule(function() + os.remove(tmp_input) + if vim.fn.filereadable(cache_path) == 1 then + finish(cache_path) + else + finish(nil) + end + end) + end) + end + end + + patch_mermaid_rendering() + + -- md-render only processes placements near the viewport on first paint, + -- and its WinScrolled retry loop skips placements without a `path`. + -- Uncached mermaid placements carry only `mermaid_source`, so a diagram + -- that starts off-screen never renders at all, no matter how far you + -- scroll. Eagerly kick off rendering for every mermaid placement when + -- images are set up: the plugin's own rebuild-on-complete then swaps in + -- a normal `path` placement, which its retry loop handles. + local function patch_offscreen_mermaid() + local du = require("md-render.display_utils") + if du._thembones_offscreen_mermaid_patch then return end + du._thembones_offscreen_mermaid_patch = true + local original_setup_images = du.setup_images + du.setup_images = function(win, content, ns, opts) + local state = original_setup_images(win, content, ns, opts) + if state and state.placements and state.process_placement then + vim.schedule(function() + for _, placement in ipairs(state.placements) do + if placement.mermaid_source then + local ok, err = pcall(state.process_placement, placement) + if not ok then + image_debug("offscreen mermaid eager render err=" .. tostring(err)) + end + end + end + end) + end + return state + end + end + + patch_offscreen_mermaid() + + -- md-render's Ghostty path intentionally re-transmits images on every + -- placement so redraws stay visible. Its stock transport uses `t=f`, + -- which means "the terminal reads this file path". Over SSH that path is + -- remote-only, so Ghostty cannot load it. In Herdr/Ghostty, file-path + -- transfer can reserve the image area without painting the pixels, so + -- avoid that path there too. Keep Ghostty's placement model, but switch + -- transport to direct PNG bytes (`t=d`). + if use_direct_kitty_bytes() then + image_debug("direct-byte override enabled") + local img = require("md-render.image") + local tty = require("md-render.tty") + local original_begin_batch = img.begin_batch + local original_flush_batch = img.flush_batch + local original_clear_all = img.clear_all + local original_delete_image = img.delete_image + local original_delete_images = img.delete_images + local original_put_image = img.put_image + local original_get_tty_path = tty.get_tty_path + + local CHUNK = 4096 + local next_image_id = 800000 + local direct_images = {} + local batch_depth = 0 + local after_flush = {} + + tty.get_tty_path = function() + if vim.env.SSH_TTY and vim.fn.filereadable(vim.env.SSH_TTY) == 1 then + return vim.env.SSH_TTY + end + return original_get_tty_path() + end + + local function queue_terminal(data) + image_debug("queue_terminal bytes=" .. tostring(#data) .. " batch_depth=" .. tostring(batch_depth)) + if batch_depth > 0 then + table.insert(after_flush, data) + else + vim.api.nvim_ui_send(data) + end + end + + local function flush_direct_queue() + if batch_depth == 0 and #after_flush > 0 then + image_debug("flush_direct_queue chunks=" .. tostring(#after_flush)) + for _, data in ipairs(after_flush) do + vim.api.nvim_ui_send(data) + end + after_flush = {} + end + end + + img.begin_batch = function(...) + batch_depth = batch_depth + 1 + return original_begin_batch(...) + end + + img.flush_batch = function(...) + local result = original_flush_batch(...) + batch_depth = math.max(0, batch_depth - 1) + flush_direct_queue() + return result + end + + local function next_direct_id() + next_image_id = next_image_id + 1 + return next_image_id + end + + local function read_png_base64(path) + local f = io.open(path, "rb") + if not f then + return nil + end + local data = f:read("*a") + f:close() + return vim.base64.encode(data) + end + + local function send_chunked(header, payload) + image_debug("send_chunked header=" .. header .. " payload_len=" .. tostring(#payload)) + local i = 1 + local first = true + while i <= #payload do + local piece = payload:sub(i, i + CHUNK - 1) + i = i + CHUNK + local more = i <= #payload and 1 or 0 + local hdr + if first then + hdr = string.format("%s,m=%d", header, more) + first = false + else + hdr = string.format("m=%d,q=2", more) + end + queue_terminal("\27_G" .. hdr .. ";" .. piece .. "\27\\") + end + end + + local function crop_params_for(win, row, col, display_cols, display_rows, img_w, img_h) + if not vim.api.nvim_win_is_valid(win) then return nil end + + local win_pos = vim.api.nvim_win_get_position(win) + local wininfo = vim.fn.getwininfo(win)[1] + if not wininfo then return nil end + + local win_height = wininfo.height + local topline = wininfo.topline - 1 + local leftcol = wininfo.leftcol or 0 + local textoff = wininfo.textoff or 0 + + local img_end_row = row + display_rows - 1 + if img_end_row < topline or row >= topline + win_height then return nil end + + local visual_row = row - topline + local border_left_width = 0 + local border_top_height = 0 + local ok_cfg, win_cfg = pcall(vim.api.nvim_win_get_config, win) + if ok_cfg and win_cfg.border then + local border = win_cfg.border + if type(border) == "table" then + local left = border[8] + if type(left) == "table" then left = left[1] end + if left and left ~= "" then + border_left_width = vim.api.nvim_strwidth(left) + end + local top = border[2] + if type(top) == "table" then top = top[1] end + if top and top ~= "" then + border_top_height = 1 + end + elseif border ~= "none" and border ~= "" then + border_left_width = vim.api.nvim_strwidth("|") + border_top_height = 1 + end + end + + local visual_col = col - leftcol + local visible_text_cols = vim.api.nvim_win_get_width(win) - textoff + local img_end_col = col + display_cols - 1 + if img_end_col < leftcol or col >= leftcol + visible_text_cols then return nil end + + local screen_col = win_pos[2] + visual_col + border_left_width + textoff + 1 + local src_x, src_y, src_w, src_h + + if visual_col < 0 then + local hidden_cols = -visual_col + if img_w then + src_x = math.floor(img_w * hidden_cols / display_cols) + src_w = img_w - src_x + end + display_cols = display_cols - hidden_cols + visual_col = 0 + screen_col = win_pos[2] + border_left_width + textoff + 1 + end + + if visual_row < 0 then + local hidden_rows = -visual_row + if img_h then + src_y = math.floor(img_h * hidden_rows / display_rows) + src_h = img_h - src_y + end + display_rows = display_rows - hidden_rows + visual_row = 0 + end + + local winbar_height = 0 + local ok_wb, wb = pcall(function() return vim.wo[win].winbar end) + if ok_wb and wb and wb ~= "" then + winbar_height = 1 + end + local screen_row = wininfo.winrow + visual_row + border_top_height + winbar_height + + local visible_rows = win_height - visual_row + if visible_rows <= 0 then return nil end + if display_rows > visible_rows and img_h then + local remaining_h = src_h or img_h + src_h = math.floor(remaining_h * visible_rows / display_rows) + display_rows = visible_rows + end + + local visible_cols = visible_text_cols - visual_col + if visible_cols <= 0 then return nil end + if display_cols > visible_cols and img_w then + local remaining_w = src_w or img_w + src_w = math.floor(remaining_w * visible_cols / display_cols) + display_cols = visible_cols + end + + local crop_params = "" + if (src_x or src_y or src_w or src_h) and img_w and img_h then + crop_params = string.format( + ",x=%d,y=%d,w=%d,h=%d", + src_x or 0, + src_y or 0, + src_w or img_w, + src_h or img_h + ) + end + + return { + screen_col = screen_col, + screen_row = screen_row, + display_cols = display_cols, + display_rows = display_rows, + crop_params = crop_params, + } + end + + local function transmit_direct(path, callback) + image_debug("transmit_direct path=" .. tostring(path)) + local payload = read_png_base64(path) + if not payload then + image_debug("transmit_direct no payload") + callback(nil) + return + end + + local id = next_direct_id() + local img_w, img_h = img.image_dimensions(path) + image_debug("transmit_direct id=" .. tostring(id) .. " dims=" .. tostring(img_w) .. "x" .. tostring(img_h)) + direct_images[id] = { + path = path, + payload = payload, + width = img_w, + height = img_h, + } + + send_chunked( + string.format("a=t,f=100,t=d,i=%d,q=2", id), + payload + ) + callback(id, img_w, img_h) + end + + -- Herdr re-encodes every visible placement as base64 RGBA of the FULL + -- source image (bytes ≈ w*h*4*4/3, ~16x the PNG size) when forwarding + -- frames to its client, and drops ALL graphics for any frame whose + -- total exceeds 32MiB ("dropping oversized graphics payload" in + -- herdr-server.log). A scale-4 mermaid PNG alone is ~60MB on that + -- wire, so nothing paints. Cap source pixels inside herdr panes; the + -- render view is ≤45 rows so the downscale is visually lossless. + local HERDR_MAX_PIXELS = 1500000 + + local function herdr_scaled_path(png_path, max_dim) + local mtime = vim.fn.getftime(png_path) + local key = png_path .. "|" .. tostring(mtime) .. "|" .. tostring(max_dim) + local dir = vim.fn.stdpath("cache") .. "/md-render/herdr-scaled" + vim.fn.mkdir(dir, "p") + return dir .. "/" .. vim.fn.sha256(key):sub(1, 16) .. ".png" + end + + -- Downscale tool detection, same priority as md-render's own + -- find_convert_tool(): sips (macOS) → ffmpeg → magick (IM7) → + -- convert (IM6, Linux distro ImageMagick installs). + local _downscale_tool = nil + local _downscale_checked = false + local function find_downscale_tool() + if _downscale_checked then return _downscale_tool end + _downscale_checked = true + if vim.fn.has("mac") == 1 and vim.fn.executable("sips") == 1 then + _downscale_tool = "sips" + elseif vim.fn.executable("ffmpeg") == 1 then + _downscale_tool = "ffmpeg" + elseif vim.fn.executable("magick") == 1 then + _downscale_tool = "magick" + elseif vim.fn.executable("convert") == 1 then + _downscale_tool = "convert" + end + return _downscale_tool + end + + local function build_downscale_cmd(tool, src, dst, max_dim) + local dim = tostring(max_dim) + if tool == "sips" then + return { "sips", "-Z", dim, src, "--out", dst } + elseif tool == "ffmpeg" then + return { + "ffmpeg", "-y", "-i", src, + "-vframes", "1", + "-vf", "scale='min(" .. dim .. ",iw)':'min(" .. dim .. ",ih)':force_original_aspect_ratio=decrease", + dst, + } + else -- magick / convert share the same argument shape + return { tool, src, "-resize", dim .. "x" .. dim .. ">", dst } + end + end + + local function downscale_for_herdr(png_path, callback) + if vim.env.HERDR_ENV ~= "1" then + callback(png_path) + return + end + local w, h = img.image_dimensions(png_path) + if not w or not h or w * h <= HERDR_MAX_PIXELS then + callback(png_path) + return + end + local scale = math.sqrt(HERDR_MAX_PIXELS / (w * h)) + local max_dim = math.floor(math.max(w, h) * scale) + local out = herdr_scaled_path(png_path, max_dim) + if vim.fn.filereadable(out) == 1 then + image_debug("downscale_for_herdr cached " .. out) + callback(out) + return + end + -- If no tool is available or it fails, fall back to the original + -- (which herdr will drop, same as before this patch). + local tool = find_downscale_tool() + if not tool then + image_debug("downscale_for_herdr no downscale tool") + callback(png_path) + return + end + vim.system( + build_downscale_cmd(tool, png_path, out, max_dim), + { text = true, timeout = 15000 }, + function() + vim.schedule(function() + if vim.fn.filereadable(out) == 1 then + image_debug("downscale_for_herdr " .. tool .. " " .. w .. "x" .. h .. " -> max_dim=" .. max_dim) + callback(out) + else + image_debug("downscale_for_herdr " .. tool .. " failed") + callback(png_path) + end + end) + end + ) + end + + img.transmit_image_async = function(path, callback) + image_debug("transmit_image_async path=" .. tostring(path)) + if not img.supports_kitty() then + image_debug("transmit_image_async no kitty support") + callback(nil) + return + end + img.ensure_png_async(path, function(png_path) + if not png_path then + image_debug("transmit_image_async no png_path") + callback(nil) + return + end + image_debug("transmit_image_async png_path=" .. tostring(png_path)) + downscale_for_herdr(png_path, function(final_path) + transmit_direct(final_path, callback) + end) + end) + end + + img.put_image = function(image_id, win, row, col, display_cols, display_rows, anim_path, img_w, img_h) + image_debug( + "put_image id=" .. tostring(image_id) + .. " row=" .. tostring(row) + .. " col=" .. tostring(col) + .. " cols=" .. tostring(display_cols) + .. " rows=" .. tostring(display_rows) + .. " anim=" .. tostring(anim_path) + ) + local direct = direct_images[image_id] + if not direct or anim_path then + image_debug("put_image fallback direct=" .. tostring(direct ~= nil) .. " anim=" .. tostring(anim_path)) + return original_put_image(image_id, win, row, col, display_cols, display_rows, anim_path, img_w, img_h) + end + + if not img.supports_kitty() then return end + local placement = crop_params_for( + win, + row, + col, + display_cols, + display_rows, + img_w or direct.width, + img_h or direct.height + ) + if not placement then + image_debug("put_image no placement") + return + end + image_debug( + "put_image placement screen=" + .. tostring(placement.screen_row) + .. "," + .. tostring(placement.screen_col) + .. " cells=" + .. tostring(placement.display_cols) + .. "x" + .. tostring(placement.display_rows) + .. " crop=" + .. tostring(placement.crop_params) + ) + + queue_terminal("\27[s") + queue_terminal(string.format("\27[%d;%dH", placement.screen_row, placement.screen_col)) + send_chunked( + string.format( + "a=T,f=100,t=d,i=%d,c=%d,r=%d%s,C=1,q=2", + image_id, + placement.display_cols, + placement.display_rows, + placement.crop_params + ), + direct.payload + ) + queue_terminal("\27[u") + end + + img.clear_all = function(...) + direct_images = {} + return original_clear_all(...) + end + + img.delete_image = function(image_id, ...) + direct_images[image_id] = nil + return original_delete_image(image_id, ...) + end + + img.delete_images = function(image_ids, ...) + for _, image_id in ipairs(image_ids or {}) do + direct_images[image_id] = nil + end + return original_delete_images(image_ids, ...) + end + end + + local function mermaid_sources(buf) + local sources = {} + local in_mermaid = false + local block = {} + + for _, line in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do + if not in_mermaid then + if line:match("^%s*```%s*mermaid%s*$") then + in_mermaid = true + block = {} + end + elseif line:match("^%s*```%s*$") then + if #block > 0 then + table.insert(sources, table.concat(block, "\n")) + end + in_mermaid = false + block = {} + else + table.insert(block, line) + end + end + + return sources + end + + local function prime_mermaid_cache(buf) + if not vim.api.nvim_buf_is_valid(buf) then return end + + local ok, image = pcall(require, "md-render.image") + if not ok or not image.has_mmdc() then return end + + for _, source in ipairs(mermaid_sources(buf)) do + if not image.get_mermaid_cached(source) then + image.render_mermaid(source) + end + end + end + + local function ensure_md_render_auto(buf) + if not vim.api.nvim_buf_is_valid(buf) then return end + if vim.bo[buf].filetype ~= "markdown" then return end + + if not vim.b[buf].md_render_auto then + local cache_group = vim.api.nvim_create_augroup("MdRenderMermaidCache" .. buf, { clear = true }) + vim.api.nvim_create_autocmd({ "InsertLeave", "BufWritePost" }, { + group = cache_group, + buffer = buf, + callback = function() + prime_mermaid_cache(buf) + end, + desc = "Prime md-render Mermaid cache before redraw", + }) + end + + prime_mermaid_cache(buf) + if vim.b[buf].md_render_auto then return end + + local ok, md = pcall(require, "md-render") + if ok then + local win = vim.api.nvim_get_current_win() + local ok_state, state = pcall(vim.api.nvim_win_get_var, win, "md_render_state") + if ok_state and type(state) == "table" and state.source_buf ~= buf then + pcall(vim.api.nvim_win_del_var, win, "md_render_state") + end + + pcall(md.preview.auto_on, { max_width = 140 }) + end + end + + local group = vim.api.nvim_create_augroup("MdRenderMarkdownAuto", { clear = true }) + vim.api.nvim_create_autocmd("FileType", { + group = group, + pattern = { "markdown", "*.md", "*.markdown" }, + callback = function(ev) + -- Defer so the FileType pass that loads md-render completes first. + vim.schedule(function() + if vim.api.nvim_get_current_buf() == ev.buf then + ensure_md_render_auto(ev.buf) + end + end) + end, + desc = "Enable md-render auto mode for markdown buffers", + }) + + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + vim.schedule(function() + ensure_md_render_auto(buf) + end) + end + end, + }, + + -- Disable the previous attempts so they don't fight md-render for the same + -- terminal cells (the "1 frame then gone" / "behind the text" symptoms). + { "3rd/image.nvim", enabled = false }, + { "3rd/diagram.nvim", enabled = false }, + { + "folke/snacks.nvim", + opts = function(_, opts) + opts.image = vim.tbl_deep_extend("force", opts.image or {}, { + enabled = false, + }) + return opts + end, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/indent.lua b/private_dot_config/nvim/lua/plugins/indent.lua new file mode 100644 index 0000000..1294db4 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/indent.lua @@ -0,0 +1,17 @@ +-- Override LazyVim's indent-blankline for VS Code-like indent guides +return { + { + "lukas-reineke/indent-blankline.nvim", + main = "ibl", -- v3 module name + opts = { + indent = { + char = "│", + }, + scope = { + enabled = true, + show_start = false, + show_end = false, + }, + }, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/markdown-no-lint.lua b/private_dot_config/nvim/lua/plugins/markdown-no-lint.lua new file mode 100644 index 0000000..5b46426 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/markdown-no-lint.lua @@ -0,0 +1,12 @@ +return { + { + "mfussenegger/nvim-lint", + optional = true, + opts = { + linters_by_ft = { + markdown = {}, -- no linters for .md + ["markdown.mdx"] = {}, -- no linters for .mdx + }, + }, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/markdown.lua b/private_dot_config/nvim/lua/plugins/markdown.lua new file mode 100644 index 0000000..a5451dd --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/markdown.lua @@ -0,0 +1,27 @@ +return { + -- Disable markdown diagnostics/linting but keep LSP for formatting + { + "neovim/nvim-lspconfig", + opts = { + servers = { + marksman = { + -- Keep marksman for markdown LSP features like formatting + handlers = { + -- Disable diagnostics from marksman + ["textDocument/publishDiagnostics"] = function() end, + }, + }, + }, + }, + }, + -- Also disable any lint-related diagnostics for markdown files + { + "mfussenegger/nvim-lint", + optional = true, + opts = { + linters_by_ft = { + markdown = {}, + }, + }, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/minimap.lua b/private_dot_config/nvim/lua/plugins/minimap.lua new file mode 100644 index 0000000..58a4345 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/minimap.lua @@ -0,0 +1,52 @@ +-- VS Code-style minimap using neominimap.nvim +return { + { + "Isrothy/neominimap.nvim", + version = "v3.*.*", + lazy = false, + keys = { + { "<leader>um", "<cmd>Neominimap Toggle<cr>", desc = "Toggle minimap" }, + { "<leader>uf", "<cmd>Neominimap Focus<cr>", desc = "Focus minimap" }, + }, + init = function() + vim.g.neominimap = { + auto_enable = true, + + -- Higher resolution (lower = denser) + x_multiplier = 2, + y_multiplier = 1, + + layout = "float", + float = { + minimap_width = 8, + window_border = "none", + }, + + -- Enable mouse clicks + click = { + enabled = true, + auto_switch_focus = true, + }, + + -- Annotations + diagnostic = { + enabled = true, + mode = "line", -- Highlight full line + }, + git = { + enabled = true, + mode = "line", -- Full line highlight for git changes + }, + search = { + enabled = true, + mode = "line", + }, + treesitter = { + enabled = true, + }, + + exclude_filetypes = { "help", "neo-tree", "lazy", "mason", "dashboard" }, + } + end, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/minuet.lua b/private_dot_config/nvim/lua/plugins/minuet.lua new file mode 100644 index 0000000..023f23f --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/minuet.lua @@ -0,0 +1,115 @@ +-- Inline LLM (ghost-text) completion tuned for writing markdown/prose. +-- Reuses ZAI_API_KEY from the environment (same key the :AI command uses). +-- Suggestions are virtual text, auto-triggered behind a debounce delay. +local PROSE_FT = { + markdown = true, + ["md-render"] = true, + text = true, + rst = true, + tex = true, + gitcommit = true, + mail = true, +} + +return { + "milanglacier/minuet-ai.nvim", + dependencies = { "nvim-lua/plenary.nvim" }, + -- Load eagerly: minuet only sets its buffer-local auto-trigger flag from a + -- FileType autocmd registered in setup(). Lazy-loading on InsertEnter would + -- register that autocmd *after* the current buffer's FileType already fired, + -- so virtualtext would never auto-trigger on the buffer you're editing. + lazy = false, + opts = { + provider = "openai_compatible", + -- One natural continuation, not three alternatives — prose has a single + -- "next sentence", and fewer candidates means less to read past. + n_completions = 1, + -- Give the model more of the text *before* the cursor (default 0.75). + context_ratio = 0.85, + -- Cap how much surrounding text is sent. Smaller = lower, steadier latency + -- (the full default of 16000 chars is overkill for prose continuation and + -- inflates time-to-first-token). 4000 chars ≈ plenty of preceding context. + context_window = 4000, + -- Delay knobs: wait for an idle pause, and rate-limit requests. Groq is fast + -- and deterministic, so we can afford a snappier debounce than the cloud + -- defaults; throttle stays moderate to respect the free-tier rate limit. + throttle = 800, -- min ms between requests + debounce = 400, -- ms of idle typing before a request fires + provider_options = { + openai_compatible = { + api_key = "GROQ_API_KEY", -- env var NAME; minuet reads it at runtime + name = "Groq", + -- Groq's LPU inference gives ~80-100ms time-to-first-token with very low + -- variance — that consistency is the whole reason for moving off Z.AI's + -- hit-or-miss coding endpoint. OpenAI-compatible, so the schema is unchanged. + end_point = "https://api.groq.com/openai/v1/chat/completions", + -- llama-3.3-70b is Groq's flagship general model: strong prose, still fast + -- on their hardware. If you ever want the absolute lowest latency, swap to + -- "llama-3.1-8b-instant" (faster, slightly weaker prose). + model = "llama-3.3-70b-versatile", + -- Swap minuet's "code completion engine" system prompt for a prose one + -- in writing filetypes; keep the default (string) for code. Guidelines + -- and few-shots are left at defaults so minuet's <endCompletion> parsing + -- still works. + system = { + prompt = function() + if PROSE_FT[vim.bo.filetype] then + return [[ +You are a prose writing assistant embedded in a text editor. Continue the +author's text at the <cursorPosition> marker naturally and fluently, matching +their voice, tone, and sentence rhythm. + +- Output ONLY the continuation text — no preamble, no explanation. +- Never wrap the output in markdown code fences or backticks. +- Do not add headings, bullets, or numbering unless the surrounding text + already uses them. +- Keep it to at most 1-3 sentences and stop at a natural boundary.]] + end + return require("minuet.config").default_system_prefix_first.prompt + end, + }, + optional = { + -- Short, focused completions. (No `thinking` field here — that was a + -- Z.AI-specific param; Groq's Llama models 400 on unknown body fields.) + max_tokens = 96, + temperature = 0.3, + }, + }, + }, + virtualtext = { + auto_trigger_ft = { "*" }, + -- Alt keymaps are kept as a secondary path, but the primary accept key is + -- <Tab>, wired in config() below (Alt+* doesn't fire in Ghostty without + -- macos-option-as-alt, and Tab is the natural "accept" key for prose). + keymap = { + accept = "<A-y>", -- accept full suggestion + accept_line = "<A-l>", -- accept one line + prev = "<A-[>", + next = "<A-]>", + dismiss = "<A-e>", + }, + }, + }, + config = function(_, opts) + require("minuet").setup(opts) + + -- Smart <Tab>: accept a visible minuet ghost-text suggestion; otherwise fall + -- back to the exact super-tab behavior we had before (jump an active snippet, + -- else insert a literal Tab). Blink's menu navigates with <C-n>/<C-p>, not + -- Tab, so this doesn't fight the completion popup. + local vt = require("minuet.virtualtext").action + vim.keymap.set("i", "<Tab>", function() + if vt.is_visible() then + vt.accept() + elseif vim.snippet.active({ direction = 1 }) then + vim.snippet.jump(1) + else + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes("<Tab>", true, false, true), + "n", + false + ) + end + end, { desc = "minuet: accept suggestion, else snippet jump / Tab" }) + end, +} diff --git a/private_dot_config/nvim/lua/plugins/navigation.lua b/private_dot_config/nvim/lua/plugins/navigation.lua new file mode 100644 index 0000000..2bf3335 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/navigation.lua @@ -0,0 +1,3 @@ +-- Navigation enhancement plugins +-- (neoscroll removed due to conflicts with smear-cursor) +return {} diff --git a/private_dot_config/nvim/lua/plugins/neo-tree-extras.lua b/private_dot_config/nvim/lua/plugins/neo-tree-extras.lua new file mode 100644 index 0000000..5b61748 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/neo-tree-extras.lua @@ -0,0 +1,14 @@ +return { + "nvim-neo-tree/neo-tree.nvim", + opts = function(_, opts) + opts.filesystem = opts.filesystem or {} + opts.filesystem.filtered_items = opts.filesystem.filtered_items or {} + + -- make sure .log files are visible + opts.filesystem.filtered_items.hide_gitignored = false + opts.filesystem.filtered_items.hide_dotfiles = false + + opts.filesystem.filtered_items.always_show = opts.filesystem.filtered_items.always_show or {} + table.insert(opts.filesystem.filtered_items.always_show, "*.log") + end, +} diff --git a/private_dot_config/nvim/lua/plugins/noice-fix.lua b/private_dot_config/nvim/lua/plugins/noice-fix.lua new file mode 100644 index 0000000..c4fbd83 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/noice-fix.lua @@ -0,0 +1,4 @@ +-- Disable noice.nvim (causing treesitter errors) +return { + { "folke/noice.nvim", enabled = false }, +} diff --git a/private_dot_config/nvim/lua/plugins/nvim-tree.lua b/private_dot_config/nvim/lua/plugins/nvim-tree.lua new file mode 100644 index 0000000..2760043 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/nvim-tree.lua @@ -0,0 +1,12 @@ +return { + "nvim-tree/nvim-tree.lua", + dependencies = { + "nvim-tree/nvim-web-devicons", + }, + keys = { + { "<leader>e", "<cmd>NvimTreeToggle<cr>", desc = "Toggle file explorer" }, + }, + config = function() + require("nvim-tree").setup({}) + end, +} \ No newline at end of file diff --git a/private_dot_config/nvim/lua/plugins/private_telescope.lua b/private_dot_config/nvim/lua/plugins/private_telescope.lua new file mode 100644 index 0000000..fd770e4 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/private_telescope.lua @@ -0,0 +1,35 @@ +return { + "nvim-telescope/telescope.nvim", + opts = { + defaults = { + -- Don't respect .gitignore - show logs and temp files + file_ignore_patterns = { + "node_modules/", + "__pycache__/", + ".git/", + ".pytest_cache/", + "%.pyc$", + "%.pyo$", + }, + -- Make sure to not use git_files by default + vimgrep_arguments = { + "rg", + "--color=never", + "--no-heading", + "--with-filename", + "--line-number", + "--column", + "--smart-case", + -- Don't respect gitignore + "--no-ignore", + }, + }, + pickers = { + find_files = { + -- Show hidden files and don't respect gitignore + hidden = true, + no_ignore = true, + }, + }, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/refactoring.lua b/private_dot_config/nvim/lua/plugins/refactoring.lua new file mode 100644 index 0000000..3a12351 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/refactoring.lua @@ -0,0 +1,14 @@ +return { + { + "ThePrimeagen/refactoring.nvim", + dependencies = { + "nvim-lua/plenary.nvim", + "nvim-treesitter/nvim-treesitter", + "lewis6991/async.nvim", + }, + ft = { "lua", "python", "javascript", "typescript", "go", "c", "cpp", "java", "php", "ruby" }, + config = function() + require("refactoring").setup() + end, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/search.lua b/private_dot_config/nvim/lua/plugins/search.lua new file mode 100644 index 0000000..9592f17 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/search.lua @@ -0,0 +1,58 @@ +-- Enhanced search and replace functionality +return { + -- Better search highlighting with match counts + -- Shows "X/Y" match indicator and improves n/N navigation + { + "kevinhwang91/nvim-hlslens", + event = "VeryLazy", + enabled = true, -- Set to false to disable + keys = { + { "n", [[<Cmd>execute('normal! ' . v:count1 . 'n')<CR><Cmd>lua require('hlslens').start()<CR>]], desc = "Next search result" }, + { "N", [[<Cmd>execute('normal! ' . v:count1 . 'N')<CR><Cmd>lua require('hlslens').start()<CR>]], desc = "Previous search result" }, + { "*", [[*<Cmd>lua require('hlslens').start()<CR>]], desc = "Search word under cursor" }, + { "#", [[#<Cmd>lua require('hlslens').start()<CR>]], desc = "Search word under cursor (backward)" }, + { "g*", [[g*<Cmd>lua require('hlslens').start()<CR>]], desc = "Search word under cursor (no boundary)" }, + { "g#", [[g#<Cmd>lua require('hlslens').start()<CR>]], desc = "Search word under cursor (no boundary, backward)" }, + }, + opts = { + calm_down = true, + nearest_only = true, + nearest_float_when = "always", + }, + }, + + -- Advanced search and replace with multiple strategies + -- Provides visual search/replace interface + -- Use :SearchReplaceSingleBuffer, :SearchReplaceMultiBuffer + { + "roobert/search-replace.nvim", + cmd = { + "SearchReplaceSingleBuffer", + "SearchReplaceMultiBuffer", + "SearchReplaceWithinVisualSelection", + "SearchReplaceWithinVisualSelectionCWord", + }, + enabled = true, -- Set to false to disable + opts = { + default_replace_single_buffer_options = "gcI", + default_replace_multi_buffer_options = "egcI", + }, + keys = { + -- Single buffer replacements + { "<leader>rs", "<CMD>SearchReplaceSingleBufferSelections<CR>", desc = "Search/Replace Selections", mode = "v" }, + { "<leader>ro", "<CMD>SearchReplaceSingleBufferOpen<CR>", desc = "Search/Replace Open" }, + { "<leader>rw", "<CMD>SearchReplaceSingleBufferCWord<CR>", desc = "Search/Replace Word" }, + { "<leader>rW", "<CMD>SearchReplaceSingleBufferCWORD<CR>", desc = "Search/Replace WORD" }, + { "<leader>re", "<CMD>SearchReplaceSingleBufferCExpr<CR>", desc = "Search/Replace Expr" }, + { "<leader>rf", "<CMD>SearchReplaceSingleBufferCFile<CR>", desc = "Search/Replace File" }, + + -- Multi buffer replacements + { "<leader>rbs", "<CMD>SearchReplaceMultiBufferSelections<CR>", desc = "Search/Replace Selections (Multi)", mode = "v" }, + { "<leader>rbo", "<CMD>SearchReplaceMultiBufferOpen<CR>", desc = "Search/Replace Open (Multi)" }, + { "<leader>rbw", "<CMD>SearchReplaceMultiBufferCWord<CR>", desc = "Search/Replace Word (Multi)" }, + { "<leader>rbW", "<CMD>SearchReplaceMultiBufferCWORD<CR>", desc = "Search/Replace WORD (Multi)" }, + { "<leader>rbe", "<CMD>SearchReplaceMultiBufferCExpr<CR>", desc = "Search/Replace Expr (Multi)" }, + { "<leader>rbf", "<CMD>SearchReplaceMultiBufferCFile<CR>", desc = "Search/Replace File (Multi)" }, + }, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/smear_cursor.lua b/private_dot_config/nvim/lua/plugins/smear_cursor.lua new file mode 100644 index 0000000..c8c4f15 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/smear_cursor.lua @@ -0,0 +1,9 @@ +return { + "sphamba/smear-cursor.nvim", + opts = { + smear_between_buffers = true, + smear_between_neighbor_lines = true, + scroll_buffer_space = true, + smear_insert_mode = true, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/themes.lua b/private_dot_config/nvim/lua/plugins/themes.lua new file mode 100644 index 0000000..cb32d21 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/themes.lua @@ -0,0 +1,273 @@ +-- Colorscheme/theme plugins +return { + -- Catppuccin + { + "catppuccin/nvim", + name = "catppuccin", + lazy = false, + priority = 1000, + }, + + -- Tokyo Night + { + "folke/tokyonight.nvim", + lazy = false, + priority = 1000, + }, + + -- Kanagawa + { + "rebelot/kanagawa.nvim", + lazy = false, + priority = 1000, + }, + + -- Rose Pine + { + "rose-pine/neovim", + name = "rose-pine", + lazy = false, + priority = 1000, + }, + + -- Nightfox family (Nightfox, Nordfox, Dawnfox, Duskfox, Carbonfox, Terafox) + { + "EdenEast/nightfox.nvim", + lazy = false, + priority = 1000, + }, + + -- Gruvbox + { + "ellisonleao/gruvbox.nvim", + lazy = false, + priority = 1000, + }, + + -- Gruvbox Material + { + "sainnhe/gruvbox-material", + lazy = false, + priority = 1000, + }, + + -- Nord + { + "shaunsingh/nord.nvim", + lazy = false, + priority = 1000, + }, + + -- Nordic + { + "AlexvZyl/nordic.nvim", + lazy = false, + priority = 1000, + }, + + -- Everforest + { + "neanias/everforest-nvim", + lazy = false, + priority = 1000, + }, + + -- Dracula + { + "Mofiqul/dracula.nvim", + lazy = false, + priority = 1000, + }, + + -- OneDark + { + "navarasu/onedark.nvim", + lazy = false, + priority = 1000, + }, + + -- One Dark Pro + { + "olimorris/onedarkpro.nvim", + lazy = false, + priority = 1000, + }, + + -- Solarized + { + "maxmx03/solarized.nvim", + lazy = false, + priority = 1000, + }, + + -- Monokai Pro + { + "loctvl842/monokai-pro.nvim", + lazy = false, + priority = 1000, + }, + + -- Cyberdream + { + "scottmckendry/cyberdream.nvim", + lazy = false, + priority = 1000, + }, + + -- Oxocarbon + { + "nyoom-engineering/oxocarbon.nvim", + lazy = false, + priority = 1000, + }, + + -- Melange + { + "savq/melange-nvim", + lazy = false, + priority = 1000, + }, + + -- Nightfly + { + "bluz71/vim-nightfly-colors", + name = "nightfly", + lazy = false, + priority = 1000, + }, + + -- Moonfly + { + "bluz71/vim-moonfly-colors", + name = "moonfly", + lazy = false, + priority = 1000, + }, + + -- Sonokai + { + "sainnhe/sonokai", + lazy = false, + priority = 1000, + }, + + -- Edge + { + "sainnhe/edge", + lazy = false, + priority = 1000, + }, + + -- Ayu + { + "Shatur/neovim-ayu", + lazy = false, + priority = 1000, + }, + + -- Material + { + "marko-cerovac/material.nvim", + lazy = false, + priority = 1000, + }, + + -- Palenight + { + "drewtempelmeyer/palenight.vim", + lazy = false, + priority = 1000, + }, + + -- GitHub theme + { + "projekt0n/github-nvim-theme", + lazy = false, + priority = 1000, + }, + + -- Tokyodark + { + "tiagovla/tokyodark.nvim", + lazy = false, + priority = 1000, + }, + + -- Darkplus (VS Code dark theme) + { + "lunarvim/darkplus.nvim", + lazy = false, + priority = 1000, + }, + + -- Poimandres + { + "olivercederborg/poimandres.nvim", + lazy = false, + priority = 1000, + }, + + -- Flow + { + "0xstepit/flow.nvim", + lazy = false, + priority = 1000, + }, + + -- Modus themes + { + "miikanissi/modus-themes.nvim", + lazy = false, + priority = 1000, + }, + + -- Zenbones + { + "mcchrish/zenbones.nvim", + dependencies = "rktjmp/lush.nvim", + lazy = false, + priority = 1000, + }, + + -- Apprentice + { + "romainl/Apprentice", + lazy = false, + priority = 1000, + }, + + -- Jellybeans + { + "nanotech/jellybeans.vim", + lazy = false, + priority = 1000, + }, + + -- Tender + { + "jacoborus/tender.vim", + lazy = false, + priority = 1000, + }, + + -- Horizon + { + "ntk148v/vim-horizon", + lazy = false, + priority = 1000, + }, + + -- Moonlight + { + "shaunsingh/moonlight.nvim", + lazy = false, + priority = 1000, + }, + + -- Lackluster + { + "slugbyte/lackluster.nvim", + lazy = false, + priority = 1000, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/ui-enhancements.lua b/private_dot_config/nvim/lua/plugins/ui-enhancements.lua new file mode 100644 index 0000000..8cde267 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/ui-enhancements.lua @@ -0,0 +1,162 @@ +-- UI enhancement plugins for better visual experience +return { + -- Render markdown with better formatting in buffers + { + "MeanderingProgrammer/render-markdown.nvim", + ft = "markdown", + enabled = true, + dependencies = { "nvim-treesitter/nvim-treesitter", "nvim-tree/nvim-web-devicons" }, + opts = { + render_modes = { "n", "i", "c", "t" }, + heading = { + enabled = true, + sign = true, + icons = { "󰲡 ", "󰲣 ", "󰲥 ", "󰲧 ", "󰲩 ", "󰲫 " }, + }, + code = { + enabled = true, + sign = true, + style = "normal", + width = "block", + }, + bullet = { + enabled = true, + icons = { "●", "○", "◆", "◇" }, + }, + pipe_table = { + cell = "trimmed", + }, + }, + }, + + -- Highlight function arguments with different colors + -- Makes it easier to distinguish parameters + { + "m-demare/hlargs.nvim", + event = "VeryLazy", + enabled = true, -- Set to false to disable + dependencies = { "nvim-treesitter/nvim-treesitter" }, + opts = { + color = "#ef9062", + highlight = {}, + excluded_filetypes = {}, + paint_arg_declarations = true, + paint_arg_usages = true, + performance = { + parse_delay = 1, + slow_parse_delay = 50, + max_iterations = 400, + max_concurrent_partial_parses = 30, + }, + }, + }, + + -- Dim inactive portions of code + -- Great for focusing on specific functions/blocks + -- Toggle with :Twilight + { + "folke/twilight.nvim", + cmd = { "Twilight", "TwilightEnable", "TwilightDisable" }, + enabled = true, -- Set to false to disable + opts = { + dimming = { + alpha = 0.25, + color = { "Normal", "#ffffff" }, + term_bg = "#000000", + inactive = false, + }, + context = 10, + treesitter = true, + expand = { + "function", + "method", + "table", + "if_statement", + }, + }, + keys = { + { "<leader>ut", "<cmd>Twilight<cr>", desc = "Toggle Twilight" }, + }, + }, + + -- Theme switcher with live preview + -- Browse and switch between colorschemes with instant preview + -- Open with :Themery + { + "zaldih/themery.nvim", + lazy = false, -- Load on startup to apply saved theme + priority = 1000, -- Load before other plugins + enabled = true, + keys = { + { "<leader>uT", "<cmd>Themery<cr>", desc = "Theme Picker" }, + }, + config = function() + require("themery").setup({ + themes = { + -- Catppuccin variants + { name = "Catppuccin Latte", colorscheme = "catppuccin-latte" }, + { name = "Catppuccin Frappe", colorscheme = "catppuccin-frappe" }, + { name = "Catppuccin Macchiato", colorscheme = "catppuccin-macchiato" }, + { name = "Catppuccin Mocha", colorscheme = "catppuccin-mocha" }, + -- Tokyo Night variants + { name = "Tokyo Night", colorscheme = "tokyonight" }, + { name = "Tokyo Night - Night", colorscheme = "tokyonight-night" }, + { name = "Tokyo Night - Storm", colorscheme = "tokyonight-storm" }, + { name = "Tokyo Night - Day", colorscheme = "tokyonight-day" }, + { name = "Tokyo Night - Moon", colorscheme = "tokyonight-moon" }, + -- Kanagawa variants + { name = "Kanagawa", colorscheme = "kanagawa" }, + { name = "Kanagawa Wave", colorscheme = "kanagawa-wave" }, + { name = "Kanagawa Dragon", colorscheme = "kanagawa-dragon" }, + { name = "Kanagawa Lotus", colorscheme = "kanagawa-lotus" }, + -- Rose Pine variants + { name = "Rose Pine", colorscheme = "rose-pine" }, + { name = "Rose Pine Moon", colorscheme = "rose-pine-moon" }, + { name = "Rose Pine Dawn", colorscheme = "rose-pine-dawn" }, + -- Nightfox variants + { name = "Nightfox", colorscheme = "nightfox" }, + { name = "Nordfox", colorscheme = "nordfox" }, + { name = "Dawnfox", colorscheme = "dawnfox" }, + { name = "Duskfox", colorscheme = "duskfox" }, + { name = "Carbonfox", colorscheme = "carbonfox" }, + { name = "Terafox", colorscheme = "terafox" }, + -- Gruvbox variants + { name = "Gruvbox Dark", colorscheme = "gruvbox" }, + { name = "Gruvbox Material", colorscheme = "gruvbox-material" }, + -- Popular themes that actually work + { name = "Nord", colorscheme = "nord" }, + { name = "Everforest", colorscheme = "everforest" }, + { name = "Dracula", colorscheme = "dracula" }, + { name = "OneDark", colorscheme = "onedark" }, + { name = "Solarized", colorscheme = "solarized" }, + { name = "Monokai Pro", colorscheme = "monokai-pro" }, + { name = "Cyberdream", colorscheme = "cyberdream" }, + { name = "Oxocarbon", colorscheme = "oxocarbon" }, + { name = "Melange", colorscheme = "melange" }, + { name = "Nightfly", colorscheme = "nightfly" }, + { name = "Moonfly", colorscheme = "moonfly" }, + { name = "Sonokai", colorscheme = "sonokai" }, + { name = "Edge", colorscheme = "edge" }, + { name = "Ayu", colorscheme = "ayu" }, + { name = "Material", colorscheme = "material" }, + { name = "Palenight", colorscheme = "palenight" }, + { name = "GitHub Dark", colorscheme = "github_dark" }, + { name = "GitHub Dark Dimmed", colorscheme = "github_dark_dimmed" }, + { name = "GitHub Light", colorscheme = "github_light" }, + { name = "Tokyodark", colorscheme = "tokyodark" }, + { name = "Darkplus", colorscheme = "darkplus" }, + { name = "Poimandres", colorscheme = "poimandres" }, + { name = "Modus Vivendi", colorscheme = "modus" }, + { name = "Zenbones", colorscheme = "zenbones" }, + { name = "Nordic", colorscheme = "nordic" }, + { name = "Jellybeans", colorscheme = "jellybeans" }, + { name = "Tender", colorscheme = "tender" }, + { name = "Horizon", colorscheme = "horizon" }, + { name = "Moonlight", colorscheme = "moonlight" }, + { name = "Lackluster", colorscheme = "lackluster" }, + }, + livePreview = true, -- Apply theme while navigating + }) + end, + }, +} diff --git a/private_dot_config/nvim/lua/plugins/yanky.lua b/private_dot_config/nvim/lua/plugins/yanky.lua new file mode 100644 index 0000000..1f49079 --- /dev/null +++ b/private_dot_config/nvim/lua/plugins/yanky.lua @@ -0,0 +1,14 @@ +-- Yanky is enabled via LazyVim's `coding.yanky` extra. Its default +-- `system_clipboard.sync_with_ring = true` reads the `+` register on every yank. +-- Over SSH/herdr the clipboard provider is copy-only OSC 52 (see +-- config/options.lua), so a ring<->clipboard sync is pointless and historically +-- threw when OSC 52 reads were attempted. Disable it on remote sessions only; +-- keep full sync locally where wl-copy/pbcopy make reads cheap and useful. +return { + "gbprod/yanky.nvim", + opts = { + system_clipboard = { + sync_with_ring = vim.env.SSH_CONNECTION == nil, + }, + }, +} diff --git a/private_dot_config/nvim/stylua.toml b/private_dot_config/nvim/stylua.toml new file mode 100644 index 0000000..5d6c50d --- /dev/null +++ b/private_dot_config/nvim/stylua.toml @@ -0,0 +1,3 @@ +indent_type = "Spaces" +indent_width = 2 +column_width = 120 \ No newline at end of file diff --git a/private_dot_npmrc.tmpl b/private_dot_npmrc.tmpl new file mode 100644 index 0000000..dd56af3 --- /dev/null +++ b/private_dot_npmrc.tmpl @@ -0,0 +1 @@ +prefix={{ .chezmoi.homeDir }}/.npm-global diff --git a/private_dot_p10k.zsh b/private_dot_p10k.zsh new file mode 100644 index 0000000..8594a75 --- /dev/null +++ b/private_dot_p10k.zsh @@ -0,0 +1,1739 @@ +# Generated by Powerlevel10k configuration wizard on 2025-06-10 at 10:25 EDT. +# Based on romkatv/powerlevel10k/config/p10k-classic.zsh, checksum 13396. +# Wizard options: nerdfont-v3 + powerline, small icons, classic, unicode, light, +# angled separators, sharp heads, flat tails, 1 line, compact, many icons, concise, +# transient_prompt, instant_prompt=verbose. +# Type `p10k configure` to generate another config. +# +# Config for Powerlevel10k with classic powerline prompt style. Type `p10k configure` to generate +# your own config based on it. +# +# Tip: Looking for a nice color? Here's a one-liner to print colormap. +# +# for i in {0..255}; do print -Pn "%K{$i} %k%F{$i}${(l:3::0:)i}%f " ${${(M)$((i%6)):#3}:+$'\n'}; done + +# Temporarily change options. +'builtin' 'local' '-a' 'p10k_config_opts' +[[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases') +[[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob') +[[ ! -o 'no_brace_expand' ]] || p10k_config_opts+=('no_brace_expand') +'builtin' 'setopt' 'no_aliases' 'no_sh_glob' 'brace_expand' + +() { + emulate -L zsh -o extended_glob + + # Unset all configuration options. This allows you to apply configuration changes without + # restarting zsh. Edit ~/.p10k.zsh and type `source ~/.p10k.zsh`. + unset -m '(POWERLEVEL9K_*|DEFAULT_USER)~POWERLEVEL9K_GITSTATUS_DIR' + + # Zsh >= 5.1 is required. + [[ $ZSH_VERSION == (5.<1->*|<6->.*) ]] || return + + # The list of segments shown on the left. Fill it with the most important segments. + typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=( + os_icon # os identifier + dir # current directory + vcs # git status + # prompt_char # prompt symbol + ) + + # The list of segments shown on the right. Fill it with less important segments. + # Right prompt on the last prompt line (where you are typing your commands) gets + # automatically hidden when the input line reaches it. Right prompt above the + # last prompt line gets hidden if it would overlap with left prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=( + status # exit code of the last command + command_execution_time # duration of the last command + background_jobs # presence of background jobs + direnv # direnv status (https://direnv.net/) + asdf # asdf version manager (https://github.com/asdf-vm/asdf) + virtualenv # python virtual environment (https://docs.python.org/3/library/venv.html) + anaconda # conda environment (https://conda.io/) + pyenv # python environment (https://github.com/pyenv/pyenv) + goenv # go environment (https://github.com/syndbg/goenv) + nodenv # node.js version from nodenv (https://github.com/nodenv/nodenv) + nvm # node.js version from nvm (https://github.com/nvm-sh/nvm) + nodeenv # node.js environment (https://github.com/ekalinin/nodeenv) + # node_version # node.js version + # go_version # go version (https://golang.org) + # rust_version # rustc version (https://www.rust-lang.org) + # dotnet_version # .NET version (https://dotnet.microsoft.com) + # php_version # php version (https://www.php.net/) + # laravel_version # laravel php framework version (https://laravel.com/) + # java_version # java version (https://www.java.com/) + # package # name@version from package.json (https://docs.npmjs.com/files/package.json) + rbenv # ruby version from rbenv (https://github.com/rbenv/rbenv) + rvm # ruby version from rvm (https://rvm.io) + fvm # flutter version management (https://github.com/leoafarias/fvm) + luaenv # lua version from luaenv (https://github.com/cehoffman/luaenv) + jenv # java version from jenv (https://github.com/jenv/jenv) + plenv # perl version from plenv (https://github.com/tokuhirom/plenv) + perlbrew # perl version from perlbrew (https://github.com/gugod/App-perlbrew) + phpenv # php version from phpenv (https://github.com/phpenv/phpenv) + scalaenv # scala version from scalaenv (https://github.com/scalaenv/scalaenv) + haskell_stack # haskell version from stack (https://haskellstack.org/) + kubecontext # current kubernetes context (https://kubernetes.io/) + terraform # terraform workspace (https://www.terraform.io) + # terraform_version # terraform version (https://www.terraform.io) + aws # aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) + aws_eb_env # aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/) + azure # azure account name (https://docs.microsoft.com/en-us/cli/azure) + gcloud # google cloud cli account and project (https://cloud.google.com/) + google_app_cred # google application credentials (https://cloud.google.com/docs/authentication/production) + toolbox # toolbox name (https://github.com/containers/toolbox) + context # user@hostname + nordvpn # nordvpn connection status, linux only (https://nordvpn.com/) + ranger # ranger shell (https://github.com/ranger/ranger) + yazi # yazi shell (https://github.com/sxyazi/yazi) + nnn # nnn shell (https://github.com/jarun/nnn) + lf # lf shell (https://github.com/gokcehan/lf) + xplr # xplr shell (https://github.com/sayanarijit/xplr) + vim_shell # vim shell indicator (:sh) + midnight_commander # midnight commander shell (https://midnight-commander.org/) + nix_shell # nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html) + chezmoi_shell # chezmoi shell (https://www.chezmoi.io/) + vi_mode # vi mode (you don't need this if you've enabled prompt_char) + # vpn_ip # virtual private network indicator + # load # CPU load + # disk_usage # disk usage + # ram # free RAM + # swap # used swap + todo # todo items (https://github.com/todotxt/todo.txt-cli) + timewarrior # timewarrior tracking status (https://timewarrior.net/) + taskwarrior # taskwarrior task count (https://taskwarrior.org/) + per_directory_history # Oh My Zsh per-directory-history local/global indicator + # cpu_arch # CPU architecture + # time # current time + # ip # ip address and bandwidth usage for a specified network interface + # public_ip # public IP address + # proxy # system-wide http/https/ftp proxy + # battery # internal battery + # wifi # wifi speed + # example # example user-defined segment (see prompt_example function below) + ) + + # Defines character set used by powerlevel10k. It's best to let `p10k configure` set it for you. + typeset -g POWERLEVEL9K_MODE=nerdfont-v3 + # When set to `moderate`, some icons will have an extra space after them. This is meant to avoid + # icon overlap when using non-monospace fonts. When set to `none`, spaces are not added. + typeset -g POWERLEVEL9K_ICON_PADDING=none + + # When set to true, icons appear before content on both sides of the prompt. When set + # to false, icons go after content. If empty or not set, icons go before content in the left + # prompt and after content in the right prompt. + # + # You can also override it for a specific segment: + # + # POWERLEVEL9K_STATUS_ICON_BEFORE_CONTENT=false + # + # Or for a specific segment in specific state: + # + # POWERLEVEL9K_DIR_NOT_WRITABLE_ICON_BEFORE_CONTENT=false + typeset -g POWERLEVEL9K_ICON_BEFORE_CONTENT= + + # Add an empty line before each prompt. + typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=false + + # Connect left prompt lines with these symbols. You'll probably want to use the same color + # as POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND below. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_PREFIX='%242F╭─' + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_PREFIX='%242F├─' + typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_PREFIX='%242F╰─' + # Connect right prompt lines with these symbols. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_SUFFIX='%242F─╮' + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_SUFFIX='%242F─┤' + typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_SUFFIX='%242F─╯' + + # Filler between left and right prompt on the first prompt line. You can set it to ' ', '·' or + # '─'. The last two make it easier to see the alignment between left and right prompt and to + # separate prompt from command output. You might want to set POWERLEVEL9K_PROMPT_ADD_NEWLINE=false + # for more compact prompt if using this option. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR=' ' + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_BACKGROUND= + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_GAP_BACKGROUND= + if [[ $POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR != ' ' ]]; then + # The color of the filler. You'll probably want to match the color of POWERLEVEL9K_MULTILINE + # ornaments defined above. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND=242 + # Start filler from the edge of the screen if there are no left segments on the first line. + typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_FIRST_SEGMENT_END_SYMBOL='%{%}' + # End filler on the edge of the screen if there are no right segments on the first line. + typeset -g POWERLEVEL9K_EMPTY_LINE_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='%{%}' + fi + + # Default background color. + typeset -g POWERLEVEL9K_BACKGROUND=238 + + # Separator between same-color segments on the left. + typeset -g POWERLEVEL9K_LEFT_SUBSEGMENT_SEPARATOR='%246F\uE0B1' + # Separator between same-color segments on the right. + typeset -g POWERLEVEL9K_RIGHT_SUBSEGMENT_SEPARATOR='%246F\uE0B3' + # Separator between different-color segments on the left. + typeset -g POWERLEVEL9K_LEFT_SEGMENT_SEPARATOR='\uE0B0' + # Separator between different-color segments on the right. + typeset -g POWERLEVEL9K_RIGHT_SEGMENT_SEPARATOR='\uE0B2' + # To remove a separator between two segments, add "_joined" to the second segment name. + # For example: POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(os_icon context_joined) + + # The right end of left prompt. + typeset -g POWERLEVEL9K_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL='\uE0B0' + # The left end of right prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='\uE0B2' + # The left end of left prompt. + typeset -g POWERLEVEL9K_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL='' + # The right end of right prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL='' + # Left prompt terminator for lines without any segments. + typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= + + #################################[ os_icon: os identifier ]################################## + # OS identifier color. + typeset -g POWERLEVEL9K_OS_ICON_FOREGROUND=255 + # Custom icon. + # typeset -g POWERLEVEL9K_OS_ICON_CONTENT_EXPANSION='⭐' + + ################################[ prompt_char: prompt symbol ]################################ + # Transparent background. + typeset -g POWERLEVEL9K_PROMPT_CHAR_BACKGROUND= + # Green prompt symbol if the last command succeeded. + typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=76 + # Red prompt symbol if the last command failed. + typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=196 + # Default prompt symbol. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION='❯' + # Prompt symbol in command vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION='❮' + # Prompt symbol in visual vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION='V' + # Prompt symbol in overwrite vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIOWR_CONTENT_EXPANSION='▶' + typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=true + # No line terminator if prompt_char is the last segment. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= + # No line introducer if prompt_char is the first segment. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL= + # No surrounding whitespace. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_{LEFT,RIGHT}_WHITESPACE= + + ##################################[ dir: current directory ]################################## + # Default current directory color. + typeset -g POWERLEVEL9K_DIR_FOREGROUND=31 + # If directory is too long, shorten some of its segments to the shortest possible unique + # prefix. The shortened directory can be tab-completed to the original. + typeset -g POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_unique + # Replace removed segment suffixes with this symbol. + typeset -g POWERLEVEL9K_SHORTEN_DELIMITER= + # Color of the shortened directory segments. + typeset -g POWERLEVEL9K_DIR_SHORTENED_FOREGROUND=103 + # Color of the anchor directory segments. Anchor segments are never shortened. The first + # segment is always an anchor. + typeset -g POWERLEVEL9K_DIR_ANCHOR_FOREGROUND=39 + # Display anchor directory segments in bold. + typeset -g POWERLEVEL9K_DIR_ANCHOR_BOLD=true + # Don't shorten directories that contain any of these files. They are anchors. + local anchor_files=( + .bzr + .citc + .git + .hg + .node-version + .python-version + .go-version + .ruby-version + .lua-version + .java-version + .perl-version + .php-version + .tool-versions + .mise.toml + .shorten_folder_marker + .svn + .terraform + CVS + Cargo.toml + composer.json + go.mod + package.json + stack.yaml + ) + typeset -g POWERLEVEL9K_SHORTEN_FOLDER_MARKER="(${(j:|:)anchor_files})" + # If set to "first" ("last"), remove everything before the first (last) subdirectory that contains + # files matching $POWERLEVEL9K_SHORTEN_FOLDER_MARKER. For example, when the current directory is + # /foo/bar/git_repo/nested_git_repo/baz, prompt will display git_repo/nested_git_repo/baz (first) + # or nested_git_repo/baz (last). This assumes that git_repo and nested_git_repo contain markers + # and other directories don't. + # + # Optionally, "first" and "last" can be followed by ":<offset>" where <offset> is an integer. + # This moves the truncation point to the right (positive offset) or to the left (negative offset) + # relative to the marker. Plain "first" and "last" are equivalent to "first:0" and "last:0" + # respectively. + typeset -g POWERLEVEL9K_DIR_TRUNCATE_BEFORE_MARKER=false + # Don't shorten this many last directory segments. They are anchors. + typeset -g POWERLEVEL9K_SHORTEN_DIR_LENGTH=1 + # Shorten directory if it's longer than this even if there is space for it. The value can + # be either absolute (e.g., '80') or a percentage of terminal width (e.g, '50%'). If empty, + # directory will be shortened only when prompt doesn't fit or when other parameters demand it + # (see POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS and POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT below). + # If set to `0`, directory will always be shortened to its minimum length. + typeset -g POWERLEVEL9K_DIR_MAX_LENGTH=80 + # When `dir` segment is on the last prompt line, try to shorten it enough to leave at least this + # many columns for typing commands. + typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS=40 + # When `dir` segment is on the last prompt line, try to shorten it enough to leave at least + # COLUMNS * POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT * 0.01 columns for typing commands. + typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT=50 + # If set to true, embed a hyperlink into the directory. Useful for quickly + # opening a directory in the file manager simply by clicking the link. + # Can also be handy when the directory is shortened, as it allows you to see + # the full directory that was used in previous commands. + typeset -g POWERLEVEL9K_DIR_HYPERLINK=false + + # Enable special styling for non-writable and non-existent directories. See POWERLEVEL9K_LOCK_ICON + # and POWERLEVEL9K_DIR_CLASSES below. + typeset -g POWERLEVEL9K_DIR_SHOW_WRITABLE=v3 + + # The default icon shown next to non-writable and non-existent directories when + # POWERLEVEL9K_DIR_SHOW_WRITABLE is set to v3. + # typeset -g POWERLEVEL9K_LOCK_ICON='⭐' + + # POWERLEVEL9K_DIR_CLASSES allows you to specify custom icons and colors for different + # directories. It must be an array with 3 * N elements. Each triplet consists of: + # + # 1. A pattern against which the current directory ($PWD) is matched. Matching is done with + # extended_glob option enabled. + # 2. Directory class for the purpose of styling. + # 3. An empty string. + # + # Triplets are tried in order. The first triplet whose pattern matches $PWD wins. + # + # If POWERLEVEL9K_DIR_SHOW_WRITABLE is set to v3, non-writable and non-existent directories + # acquire class suffix _NOT_WRITABLE and NON_EXISTENT respectively. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_DIR_CLASSES=( + # '~/work(|/*)' WORK '' + # '~(|/*)' HOME '' + # '*' DEFAULT '') + # + # Whenever the current directory is ~/work or a subdirectory of ~/work, it gets styled with one + # of the following classes depending on its writability and existence: WORK, WORK_NOT_WRITABLE or + # WORK_NON_EXISTENT. + # + # Simply assigning classes to directories doesn't have any visible effects. It merely gives you an + # option to define custom colors and icons for different directory classes. + # + # # Styling for WORK. + # typeset -g POWERLEVEL9K_DIR_WORK_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_DIR_WORK_FOREGROUND=31 + # typeset -g POWERLEVEL9K_DIR_WORK_SHORTENED_FOREGROUND=103 + # typeset -g POWERLEVEL9K_DIR_WORK_ANCHOR_FOREGROUND=39 + # + # # Styling for WORK_NOT_WRITABLE. + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_FOREGROUND=31 + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_SHORTENED_FOREGROUND=103 + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_ANCHOR_FOREGROUND=39 + # + # # Styling for WORK_NON_EXISTENT. + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_FOREGROUND=31 + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_SHORTENED_FOREGROUND=103 + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_ANCHOR_FOREGROUND=39 + # + # If a styling parameter isn't explicitly defined for some class, it falls back to the classless + # parameter. For example, if POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_FOREGROUND is not set, it falls + # back to POWERLEVEL9K_DIR_FOREGROUND. + # + # typeset -g POWERLEVEL9K_DIR_CLASSES=() + + # Custom prefix. + # typeset -g POWERLEVEL9K_DIR_PREFIX='%248Fin ' + + #####################################[ vcs: git status ]###################################### + # Branch icon. Set this parameter to '\UE0A0 ' for the popular Powerline branch icon. + typeset -g POWERLEVEL9K_VCS_BRANCH_ICON='\uF126 ' + + # Untracked files icon. It's really a question mark, your font isn't broken. + # Change the value of this parameter to show a different icon. + typeset -g POWERLEVEL9K_VCS_UNTRACKED_ICON='?' + + # Formatter for Git status. + # + # Example output: master wip ⇣42⇡42 *42 merge ~42 +42 !42 ?42. + # + # You can edit the function to customize how Git status looks. + # + # VCS_STATUS_* parameters are set by gitstatus plugin. See reference: + # https://github.com/romkatv/gitstatus/blob/master/gitstatus.plugin.zsh. + function my_git_formatter() { + emulate -L zsh + + if [[ -n $P9K_CONTENT ]]; then + # If P9K_CONTENT is not empty, use it. It's either "loading" or from vcs_info (not from + # gitstatus plugin). VCS_STATUS_* parameters are not available in this case. + typeset -g my_git_format=$P9K_CONTENT + return + fi + + if (( $1 )); then + # Styling for up-to-date Git status. + local meta='%248F' # grey foreground + local clean='%76F' # green foreground + local modified='%178F' # yellow foreground + local untracked='%39F' # blue foreground + local conflicted='%196F' # red foreground + else + # Styling for incomplete and stale Git status. + local meta='%244F' # grey foreground + local clean='%244F' # grey foreground + local modified='%244F' # grey foreground + local untracked='%244F' # grey foreground + local conflicted='%244F' # grey foreground + fi + + local res + + if [[ -n $VCS_STATUS_LOCAL_BRANCH ]]; then + local branch=${(V)VCS_STATUS_LOCAL_BRANCH} + # If local branch name is at most 32 characters long, show it in full. + # Otherwise show the first 12 … the last 12. + # Tip: To always show local branch name in full without truncation, delete the next line. + (( $#branch > 32 )) && branch[13,-13]="…" # <-- this line + res+="${clean}${(g::)POWERLEVEL9K_VCS_BRANCH_ICON}${branch//\%/%%}" + fi + + if [[ -n $VCS_STATUS_TAG + # Show tag only if not on a branch. + # Tip: To always show tag, delete the next line. + && -z $VCS_STATUS_LOCAL_BRANCH # <-- this line + ]]; then + local tag=${(V)VCS_STATUS_TAG} + # If tag name is at most 32 characters long, show it in full. + # Otherwise show the first 12 … the last 12. + # Tip: To always show tag name in full without truncation, delete the next line. + (( $#tag > 32 )) && tag[13,-13]="…" # <-- this line + res+="${meta}#${clean}${tag//\%/%%}" + fi + + # Display the current Git commit if there is no branch and no tag. + # Tip: To always display the current Git commit, delete the next line. + [[ -z $VCS_STATUS_LOCAL_BRANCH && -z $VCS_STATUS_TAG ]] && # <-- this line + res+="${meta}@${clean}${VCS_STATUS_COMMIT[1,8]}" + + # Show tracking branch name if it differs from local branch. + if [[ -n ${VCS_STATUS_REMOTE_BRANCH:#$VCS_STATUS_LOCAL_BRANCH} ]]; then + res+="${meta}:${clean}${(V)VCS_STATUS_REMOTE_BRANCH//\%/%%}" + fi + + # Display "wip" if the latest commit's summary contains "wip" or "WIP". + if [[ $VCS_STATUS_COMMIT_SUMMARY == (|*[^[:alnum:]])(wip|WIP)(|[^[:alnum:]]*) ]]; then + res+=" ${modified}wip" + fi + + if (( VCS_STATUS_COMMITS_AHEAD || VCS_STATUS_COMMITS_BEHIND )); then + # ⇣42 if behind the remote. + (( VCS_STATUS_COMMITS_BEHIND )) && res+=" ${clean}⇣${VCS_STATUS_COMMITS_BEHIND}" + # ⇡42 if ahead of the remote; no leading space if also behind the remote: ⇣42⇡42. + (( VCS_STATUS_COMMITS_AHEAD && !VCS_STATUS_COMMITS_BEHIND )) && res+=" " + (( VCS_STATUS_COMMITS_AHEAD )) && res+="${clean}⇡${VCS_STATUS_COMMITS_AHEAD}" + elif [[ -n $VCS_STATUS_REMOTE_BRANCH ]]; then + # Tip: Uncomment the next line to display '=' if up to date with the remote. + # res+=" ${clean}=" + fi + + # ⇠42 if behind the push remote. + (( VCS_STATUS_PUSH_COMMITS_BEHIND )) && res+=" ${clean}⇠${VCS_STATUS_PUSH_COMMITS_BEHIND}" + (( VCS_STATUS_PUSH_COMMITS_AHEAD && !VCS_STATUS_PUSH_COMMITS_BEHIND )) && res+=" " + # ⇢42 if ahead of the push remote; no leading space if also behind: ⇠42⇢42. + (( VCS_STATUS_PUSH_COMMITS_AHEAD )) && res+="${clean}⇢${VCS_STATUS_PUSH_COMMITS_AHEAD}" + # *42 if have stashes. + (( VCS_STATUS_STASHES )) && res+=" ${clean}*${VCS_STATUS_STASHES}" + # 'merge' if the repo is in an unusual state. + [[ -n $VCS_STATUS_ACTION ]] && res+=" ${conflicted}${VCS_STATUS_ACTION}" + # ~42 if have merge conflicts. + (( VCS_STATUS_NUM_CONFLICTED )) && res+=" ${conflicted}~${VCS_STATUS_NUM_CONFLICTED}" + # +42 if have staged changes. + (( VCS_STATUS_NUM_STAGED )) && res+=" ${modified}+${VCS_STATUS_NUM_STAGED}" + # !42 if have unstaged changes. + (( VCS_STATUS_NUM_UNSTAGED )) && res+=" ${modified}!${VCS_STATUS_NUM_UNSTAGED}" + # ?42 if have untracked files. It's really a question mark, your font isn't broken. + # See POWERLEVEL9K_VCS_UNTRACKED_ICON above if you want to use a different icon. + # Remove the next line if you don't want to see untracked files at all. + (( VCS_STATUS_NUM_UNTRACKED )) && res+=" ${untracked}${(g::)POWERLEVEL9K_VCS_UNTRACKED_ICON}${VCS_STATUS_NUM_UNTRACKED}" + # "─" if the number of unstaged files is unknown. This can happen due to + # POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY (see below) being set to a non-negative number lower + # than the number of files in the Git index, or due to bash.showDirtyState being set to false + # in the repository config. The number of staged and untracked files may also be unknown + # in this case. + (( VCS_STATUS_HAS_UNSTAGED == -1 )) && res+=" ${modified}─" + + typeset -g my_git_format=$res + } + functions -M my_git_formatter 2>/dev/null + + # Don't count the number of unstaged, untracked and conflicted files in Git repositories with + # more than this many files in the index. Negative value means infinity. + # + # If you are working in Git repositories with tens of millions of files and seeing performance + # sagging, try setting POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY to a number lower than the output + # of `git ls-files | wc -l`. Alternatively, add `bash.showDirtyState = false` to the repository's + # config: `git config bash.showDirtyState false`. + typeset -g POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY=-1 + + # Don't show Git status in prompt for repositories whose workdir matches this pattern. + # For example, if set to '~', the Git repository at $HOME/.git will be ignored. + # Multiple patterns can be combined with '|': '~(|/foo)|/bar/baz/*'. + typeset -g POWERLEVEL9K_VCS_DISABLED_WORKDIR_PATTERN='~' + + # Disable the default Git status formatting. + typeset -g POWERLEVEL9K_VCS_DISABLE_GITSTATUS_FORMATTING=true + # Install our own Git status formatter. + typeset -g POWERLEVEL9K_VCS_CONTENT_EXPANSION='${$((my_git_formatter(1)))+${my_git_format}}' + typeset -g POWERLEVEL9K_VCS_LOADING_CONTENT_EXPANSION='${$((my_git_formatter(0)))+${my_git_format}}' + # Enable counters for staged, unstaged, etc. + typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED,CONFLICTED,COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=-1 + + # Icon color. + typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_COLOR=76 + typeset -g POWERLEVEL9K_VCS_LOADING_VISUAL_IDENTIFIER_COLOR=244 + # Custom icon. + # typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_VCS_PREFIX='%248Fon ' + + # Show status of repositories of these types. You can add svn and/or hg if you are + # using them. If you do, your prompt may become slow even when your current directory + # isn't in an svn or hg reposotiry. + typeset -g POWERLEVEL9K_VCS_BACKENDS=(git) + + # These settings are used for repositories other than Git or when gitstatusd fails and + # Powerlevel10k has to fall back to using vcs_info. + typeset -g POWERLEVEL9K_VCS_CLEAN_FOREGROUND=76 + typeset -g POWERLEVEL9K_VCS_UNTRACKED_FOREGROUND=76 + typeset -g POWERLEVEL9K_VCS_MODIFIED_FOREGROUND=178 + + ##########################[ status: exit code of the last command ]########################### + # Enable OK_PIPE, ERROR_PIPE and ERROR_SIGNAL status states to allow us to enable, disable and + # style them independently from the regular OK and ERROR state. + typeset -g POWERLEVEL9K_STATUS_EXTENDED_STATES=true + + # Status on success. No content, just an icon. No need to show it if prompt_char is enabled as + # it will signify success by turning green. + typeset -g POWERLEVEL9K_STATUS_OK=true + typeset -g POWERLEVEL9K_STATUS_OK_FOREGROUND=70 + typeset -g POWERLEVEL9K_STATUS_OK_VISUAL_IDENTIFIER_EXPANSION='✔' + + # Status when some part of a pipe command fails but the overall exit status is zero. It may look + # like this: 1|0. + typeset -g POWERLEVEL9K_STATUS_OK_PIPE=true + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_FOREGROUND=70 + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_VISUAL_IDENTIFIER_EXPANSION='✔' + + # Status when it's just an error code (e.g., '1'). No need to show it if prompt_char is enabled as + # it will signify error by turning red. + typeset -g POWERLEVEL9K_STATUS_ERROR=true + typeset -g POWERLEVEL9K_STATUS_ERROR_FOREGROUND=160 + typeset -g POWERLEVEL9K_STATUS_ERROR_VISUAL_IDENTIFIER_EXPANSION='✘' + + # Status when the last command was terminated by a signal. + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL=true + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_FOREGROUND=160 + # Use terse signal names: "INT" instead of "SIGINT(2)". + typeset -g POWERLEVEL9K_STATUS_VERBOSE_SIGNAME=false + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_VISUAL_IDENTIFIER_EXPANSION='✘' + + # Status when some part of a pipe command fails and the overall exit status is also non-zero. + # It may look like this: 1|0. + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE=true + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_FOREGROUND=160 + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_VISUAL_IDENTIFIER_EXPANSION='✘' + + ###################[ command_execution_time: duration of the last command ]################### + # Show duration of the last command if takes at least this many seconds. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3 + # Show this many fractional digits. Zero means round to seconds. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0 + # Execution time color. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=248 + # Duration format: 1d 2h 3m 4s. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s' + # Custom icon. + # typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PREFIX='%248Ftook ' + + #######################[ background_jobs: presence of background jobs ]####################### + # Don't show the number of background jobs. + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VERBOSE=false + # Background jobs color. + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_FOREGROUND=37 + # Custom icon. + # typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ direnv: direnv status (https://direnv.net/) ]######################## + # Direnv color. + typeset -g POWERLEVEL9K_DIRENV_FOREGROUND=178 + # Custom icon. + # typeset -g POWERLEVEL9K_DIRENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ asdf: asdf version manager (https://github.com/asdf-vm/asdf) ]############### + # Default asdf color. Only used to display tools for which there is no color override (see below). + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_FOREGROUND. + typeset -g POWERLEVEL9K_ASDF_FOREGROUND=66 + + # There are four parameters that can be used to hide asdf tools. Each parameter describes + # conditions under which a tool gets hidden. Parameters can hide tools but not unhide them. If at + # least one parameter decides to hide a tool, that tool gets hidden. If no parameter decides to + # hide a tool, it gets shown. + # + # Special note on the difference between POWERLEVEL9K_ASDF_SOURCES and + # POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW. Consider the effect of the following commands: + # + # asdf local python 3.8.1 + # asdf global python 3.8.1 + # + # After running both commands the current python version is 3.8.1 and its source is "local" as + # it takes precedence over "global". If POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW is set to false, + # it'll hide python version in this case because 3.8.1 is the same as the global version. + # POWERLEVEL9K_ASDF_SOURCES will hide python version only if the value of this parameter doesn't + # contain "local". + + # Hide tool versions that don't come from one of these sources. + # + # Available sources: + # + # - shell `asdf current` says "set by ASDF_${TOOL}_VERSION environment variable" + # - local `asdf current` says "set by /some/not/home/directory/file" + # - global `asdf current` says "set by /home/username/file" + # + # Note: If this parameter is set to (shell local global), it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SOURCES. + typeset -g POWERLEVEL9K_ASDF_SOURCES=(shell local global) + + # If set to false, hide tool versions that are the same as global. + # + # Note: The name of this parameter doesn't reflect its meaning at all. + # Note: If this parameter is set to true, it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_PROMPT_ALWAYS_SHOW. + typeset -g POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW=false + + # If set to false, hide tool versions that are equal to "system". + # + # Note: If this parameter is set to true, it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SHOW_SYSTEM. + typeset -g POWERLEVEL9K_ASDF_SHOW_SYSTEM=true + + # If set to non-empty value, hide tools unless there is a file matching the specified file pattern + # in the current directory, or its parent directory, or its grandparent directory, and so on. + # + # Note: If this parameter is set to empty value, it won't hide tools. + # Note: SHOW_ON_UPGLOB isn't specific to asdf. It works with all prompt segments. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SHOW_ON_UPGLOB. + # + # Example: Hide nodejs version when there is no package.json and no *.js files in the current + # directory, in `..`, in `../..` and so on. + # + # typeset -g POWERLEVEL9K_ASDF_NODEJS_SHOW_ON_UPGLOB='*.js|package.json' + typeset -g POWERLEVEL9K_ASDF_SHOW_ON_UPGLOB= + + # Ruby version from asdf. + typeset -g POWERLEVEL9K_ASDF_RUBY_FOREGROUND=168 + # typeset -g POWERLEVEL9K_ASDF_RUBY_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_RUBY_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Python version from asdf. + typeset -g POWERLEVEL9K_ASDF_PYTHON_FOREGROUND=37 + # typeset -g POWERLEVEL9K_ASDF_PYTHON_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_PYTHON_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Go version from asdf. + typeset -g POWERLEVEL9K_ASDF_GOLANG_FOREGROUND=37 + # typeset -g POWERLEVEL9K_ASDF_GOLANG_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_GOLANG_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Node.js version from asdf. + typeset -g POWERLEVEL9K_ASDF_NODEJS_FOREGROUND=70 + # typeset -g POWERLEVEL9K_ASDF_NODEJS_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_NODEJS_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Rust version from asdf. + typeset -g POWERLEVEL9K_ASDF_RUST_FOREGROUND=37 + # typeset -g POWERLEVEL9K_ASDF_RUST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_RUST_SHOW_ON_UPGLOB='*.foo|*.bar' + + # .NET Core version from asdf. + typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_FOREGROUND=134 + # typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Flutter version from asdf. + typeset -g POWERLEVEL9K_ASDF_FLUTTER_FOREGROUND=38 + # typeset -g POWERLEVEL9K_ASDF_FLUTTER_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_FLUTTER_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Lua version from asdf. + typeset -g POWERLEVEL9K_ASDF_LUA_FOREGROUND=32 + # typeset -g POWERLEVEL9K_ASDF_LUA_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_LUA_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Java version from asdf. + typeset -g POWERLEVEL9K_ASDF_JAVA_FOREGROUND=32 + # typeset -g POWERLEVEL9K_ASDF_JAVA_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_JAVA_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Perl version from asdf. + typeset -g POWERLEVEL9K_ASDF_PERL_FOREGROUND=67 + # typeset -g POWERLEVEL9K_ASDF_PERL_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_PERL_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Erlang version from asdf. + typeset -g POWERLEVEL9K_ASDF_ERLANG_FOREGROUND=125 + # typeset -g POWERLEVEL9K_ASDF_ERLANG_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_ERLANG_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Elixir version from asdf. + typeset -g POWERLEVEL9K_ASDF_ELIXIR_FOREGROUND=129 + # typeset -g POWERLEVEL9K_ASDF_ELIXIR_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_ELIXIR_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Postgres version from asdf. + typeset -g POWERLEVEL9K_ASDF_POSTGRES_FOREGROUND=31 + # typeset -g POWERLEVEL9K_ASDF_POSTGRES_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_POSTGRES_SHOW_ON_UPGLOB='*.foo|*.bar' + + # PHP version from asdf. + typeset -g POWERLEVEL9K_ASDF_PHP_FOREGROUND=99 + # typeset -g POWERLEVEL9K_ASDF_PHP_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_PHP_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Haskell version from asdf. + typeset -g POWERLEVEL9K_ASDF_HASKELL_FOREGROUND=172 + # typeset -g POWERLEVEL9K_ASDF_HASKELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_HASKELL_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Julia version from asdf. + typeset -g POWERLEVEL9K_ASDF_JULIA_FOREGROUND=70 + # typeset -g POWERLEVEL9K_ASDF_JULIA_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_JULIA_SHOW_ON_UPGLOB='*.foo|*.bar' + + ##########[ nordvpn: nordvpn connection status, linux only (https://nordvpn.com/) ]########### + # NordVPN connection indicator color. + typeset -g POWERLEVEL9K_NORDVPN_FOREGROUND=39 + # Hide NordVPN connection indicator when not connected. + typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_CONTENT_EXPANSION= + typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_VISUAL_IDENTIFIER_EXPANSION= + # Custom icon. + # typeset -g POWERLEVEL9K_NORDVPN_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #################[ ranger: ranger shell (https://github.com/ranger/ranger) ]################## + # Ranger shell color. + typeset -g POWERLEVEL9K_RANGER_FOREGROUND=178 + # Custom icon. + # typeset -g POWERLEVEL9K_RANGER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ####################[ yazi: yazi shell (https://github.com/sxyazi/yazi) ]##################### + # Yazi shell color. + typeset -g POWERLEVEL9K_YAZI_FOREGROUND=178 + # Custom icon. + # typeset -g POWERLEVEL9K_YAZI_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################[ nnn: nnn shell (https://github.com/jarun/nnn) ]####################### + # Nnn shell color. + typeset -g POWERLEVEL9K_NNN_FOREGROUND=72 + # Custom icon. + # typeset -g POWERLEVEL9K_NNN_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################[ lf: lf shell (https://github.com/gokcehan/lf) ]####################### + # lf shell color. + typeset -g POWERLEVEL9K_LF_FOREGROUND=72 + # Custom icon. + # typeset -g POWERLEVEL9K_LF_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################[ xplr: xplr shell (https://github.com/sayanarijit/xplr) ]################## + # xplr shell color. + typeset -g POWERLEVEL9K_XPLR_FOREGROUND=72 + # Custom icon. + # typeset -g POWERLEVEL9K_XPLR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########################[ vim_shell: vim shell indicator (:sh) ]########################### + # Vim shell indicator color. + typeset -g POWERLEVEL9K_VIM_SHELL_FOREGROUND=34 + # Custom icon. + # typeset -g POWERLEVEL9K_VIM_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######[ midnight_commander: midnight commander shell (https://midnight-commander.org/) ]###### + # Midnight Commander shell color. + typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_FOREGROUND=178 + # Custom icon. + # typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #[ nix_shell: nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html) ]## + # Nix shell color. + typeset -g POWERLEVEL9K_NIX_SHELL_FOREGROUND=74 + + # Display the icon of nix_shell if PATH contains a subdirectory of /nix/store. + # typeset -g POWERLEVEL9K_NIX_SHELL_INFER_FROM_PATH=false + + # Tip: If you want to see just the icon without "pure" and "impure", uncomment the next line. + # typeset -g POWERLEVEL9K_NIX_SHELL_CONTENT_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_NIX_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################[ chezmoi_shell: chezmoi shell (https://www.chezmoi.io/) ]################## + # chezmoi shell color. + typeset -g POWERLEVEL9K_CHEZMOI_SHELL_FOREGROUND=33 + # Custom icon. + # typeset -g POWERLEVEL9K_CHEZMOI_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################################[ disk_usage: disk usage ]################################## + # Colors for different levels of disk usage. + typeset -g POWERLEVEL9K_DISK_USAGE_NORMAL_FOREGROUND=35 + typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_FOREGROUND=220 + typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_FOREGROUND=160 + # Thresholds for different levels of disk usage (percentage points). + typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL=90 + typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_LEVEL=95 + # If set to true, hide disk usage when below $POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL percent. + typeset -g POWERLEVEL9K_DISK_USAGE_ONLY_WARNING=false + # Custom icon. + # typeset -g POWERLEVEL9K_DISK_USAGE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ vi_mode: vi mode (you don't need this if you've enabled prompt_char) ]########### + # Text and color for normal (a.k.a. command) vi mode. + typeset -g POWERLEVEL9K_VI_COMMAND_MODE_STRING=NORMAL + typeset -g POWERLEVEL9K_VI_MODE_NORMAL_FOREGROUND=106 + # Text and color for visual vi mode. + typeset -g POWERLEVEL9K_VI_VISUAL_MODE_STRING=VISUAL + typeset -g POWERLEVEL9K_VI_MODE_VISUAL_FOREGROUND=68 + # Text and color for overtype (a.k.a. overwrite and replace) vi mode. + typeset -g POWERLEVEL9K_VI_OVERWRITE_MODE_STRING=OVERTYPE + typeset -g POWERLEVEL9K_VI_MODE_OVERWRITE_FOREGROUND=172 + # Text and color for insert vi mode. + typeset -g POWERLEVEL9K_VI_INSERT_MODE_STRING= + typeset -g POWERLEVEL9K_VI_MODE_INSERT_FOREGROUND=66 + # Custom icon. + # typeset -g POWERLEVEL9K_VI_MODE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################################[ ram: free RAM ]####################################### + # RAM color. + typeset -g POWERLEVEL9K_RAM_FOREGROUND=66 + # Custom icon. + # typeset -g POWERLEVEL9K_RAM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################################[ swap: used swap ]###################################### + # Swap color. + typeset -g POWERLEVEL9K_SWAP_FOREGROUND=96 + # Custom icon. + # typeset -g POWERLEVEL9K_SWAP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################################[ load: CPU load ]###################################### + # Show average CPU load over this many last minutes. Valid values are 1, 5 and 15. + typeset -g POWERLEVEL9K_LOAD_WHICH=5 + # Load color when load is under 50%. + typeset -g POWERLEVEL9K_LOAD_NORMAL_FOREGROUND=66 + # Load color when load is between 50% and 70%. + typeset -g POWERLEVEL9K_LOAD_WARNING_FOREGROUND=178 + # Load color when load is over 70%. + typeset -g POWERLEVEL9K_LOAD_CRITICAL_FOREGROUND=166 + # Custom icon. + # typeset -g POWERLEVEL9K_LOAD_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ todo: todo items (https://github.com/todotxt/todo.txt-cli) ]################ + # Todo color. + typeset -g POWERLEVEL9K_TODO_FOREGROUND=110 + # Hide todo when the total number of tasks is zero. + typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_TOTAL=true + # Hide todo when the number of tasks after filtering is zero. + typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_FILTERED=false + + # Todo format. The following parameters are available within the expansion. + # + # - P9K_TODO_TOTAL_TASK_COUNT The total number of tasks. + # - P9K_TODO_FILTERED_TASK_COUNT The number of tasks after filtering. + # + # These variables correspond to the last line of the output of `todo.sh -p ls`: + # + # TODO: 24 of 42 tasks shown + # + # Here 24 is P9K_TODO_FILTERED_TASK_COUNT and 42 is P9K_TODO_TOTAL_TASK_COUNT. + # + # typeset -g POWERLEVEL9K_TODO_CONTENT_EXPANSION='$P9K_TODO_FILTERED_TASK_COUNT' + + # Custom icon. + # typeset -g POWERLEVEL9K_TODO_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ timewarrior: timewarrior tracking status (https://timewarrior.net/) ]############ + # Timewarrior color. + typeset -g POWERLEVEL9K_TIMEWARRIOR_FOREGROUND=110 + # If the tracked task is longer than 24 characters, truncate and append "…". + # Tip: To always display tasks without truncation, delete the following parameter. + # Tip: To hide task names and display just the icon when time tracking is enabled, set the + # value of the following parameter to "". + typeset -g POWERLEVEL9K_TIMEWARRIOR_CONTENT_EXPANSION='${P9K_CONTENT:0:24}${${P9K_CONTENT:24}:+…}' + + # Custom icon. + # typeset -g POWERLEVEL9K_TIMEWARRIOR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############[ taskwarrior: taskwarrior task count (https://taskwarrior.org/) ]############## + # Taskwarrior color. + typeset -g POWERLEVEL9K_TASKWARRIOR_FOREGROUND=74 + + # Taskwarrior segment format. The following parameters are available within the expansion. + # + # - P9K_TASKWARRIOR_PENDING_COUNT The number of pending tasks: `task +PENDING count`. + # - P9K_TASKWARRIOR_OVERDUE_COUNT The number of overdue tasks: `task +OVERDUE count`. + # + # Zero values are represented as empty parameters. + # + # The default format: + # + # '${P9K_TASKWARRIOR_OVERDUE_COUNT:+"!$P9K_TASKWARRIOR_OVERDUE_COUNT/"}$P9K_TASKWARRIOR_PENDING_COUNT' + # + # typeset -g POWERLEVEL9K_TASKWARRIOR_CONTENT_EXPANSION='$P9K_TASKWARRIOR_PENDING_COUNT' + + # Custom icon. + # typeset -g POWERLEVEL9K_TASKWARRIOR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######[ per_directory_history: Oh My Zsh per-directory-history local/global indicator ]####### + # Color when using local/global history. + typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_FOREGROUND=135 + typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_FOREGROUND=130 + + # Tip: Uncomment the next two lines to hide "local"/"global" text and leave just the icon. + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_CONTENT_EXPANSION='' + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_CONTENT_EXPANSION='' + + # Custom icon. + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################################[ cpu_arch: CPU architecture ]################################ + # CPU architecture color. + typeset -g POWERLEVEL9K_CPU_ARCH_FOREGROUND=172 + + # Hide the segment when on a specific CPU architecture. + # typeset -g POWERLEVEL9K_CPU_ARCH_X86_64_CONTENT_EXPANSION= + # typeset -g POWERLEVEL9K_CPU_ARCH_X86_64_VISUAL_IDENTIFIER_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_CPU_ARCH_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################################[ context: user@hostname ]################################## + # Context color when running with privileges. + typeset -g POWERLEVEL9K_CONTEXT_ROOT_FOREGROUND=178 + # Context color in SSH without privileges. + typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_FOREGROUND=180 + # Default context color (no privileges, no SSH). + typeset -g POWERLEVEL9K_CONTEXT_FOREGROUND=180 + + # Context format when running with privileges: bold user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE='%B%n@%m' + # Context format when in SSH without privileges: user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_TEMPLATE='%n@%m' + # Default context format (no privileges, no SSH): user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_TEMPLATE='%n@%m' + + # Don't show context unless running with privileges or in SSH. + # Tip: Remove the next line to always show context. + typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_{CONTENT,VISUAL_IDENTIFIER}_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_CONTEXT_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_CONTEXT_PREFIX='%248Fwith ' + + ###[ virtualenv: python virtual environment (https://docs.python.org/3/library/venv.html) ]### + # Python virtual environment color. + typeset -g POWERLEVEL9K_VIRTUALENV_FOREGROUND=37 + # Don't show Python version next to the virtual environment name. + typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_PYTHON_VERSION=false + # If set to "false", won't show virtualenv if pyenv is already shown. + # If set to "if-different", won't show virtualenv if it's the same as pyenv. + typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_WITH_PYENV=false + # Separate environment name from Python version only with a space. + typeset -g POWERLEVEL9K_VIRTUALENV_{LEFT,RIGHT}_DELIMITER= + # Custom icon. + # typeset -g POWERLEVEL9K_VIRTUALENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################[ anaconda: conda environment (https://conda.io/) ]###################### + # Anaconda environment color. + typeset -g POWERLEVEL9K_ANACONDA_FOREGROUND=37 + + # Anaconda segment format. The following parameters are available within the expansion. + # + # - CONDA_PREFIX Absolute path to the active Anaconda/Miniconda environment. + # - CONDA_DEFAULT_ENV Name of the active Anaconda/Miniconda environment. + # - CONDA_PROMPT_MODIFIER Configurable prompt modifier (see below). + # - P9K_ANACONDA_PYTHON_VERSION Current python version (python --version). + # + # CONDA_PROMPT_MODIFIER can be configured with the following command: + # + # conda config --set env_prompt '({default_env}) ' + # + # The last argument is a Python format string that can use the following variables: + # + # - prefix The same as CONDA_PREFIX. + # - default_env The same as CONDA_DEFAULT_ENV. + # - name The last segment of CONDA_PREFIX. + # - stacked_env Comma-separated list of names in the environment stack. The first element is + # always the same as default_env. + # + # Note: '({default_env}) ' is the default value of env_prompt. + # + # The default value of POWERLEVEL9K_ANACONDA_CONTENT_EXPANSION expands to $CONDA_PROMPT_MODIFIER + # without the surrounding parentheses, or to the last path component of CONDA_PREFIX if the former + # is empty. + typeset -g POWERLEVEL9K_ANACONDA_CONTENT_EXPANSION='${${${${CONDA_PROMPT_MODIFIER#\(}% }%\)}:-${CONDA_PREFIX:t}}' + + # Custom icon. + # typeset -g POWERLEVEL9K_ANACONDA_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ pyenv: python environment (https://github.com/pyenv/pyenv) ]################ + # Pyenv color. + typeset -g POWERLEVEL9K_PYENV_FOREGROUND=37 + # Hide python version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PYENV_SOURCES=(shell local global) + # If set to false, hide python version if it's the same as global: + # $(pyenv version-name) == $(pyenv global). + typeset -g POWERLEVEL9K_PYENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide python version if it's equal to "system". + typeset -g POWERLEVEL9K_PYENV_SHOW_SYSTEM=true + + # Pyenv segment format. The following parameters are available within the expansion. + # + # - P9K_CONTENT Current pyenv environment (pyenv version-name). + # - P9K_PYENV_PYTHON_VERSION Current python version (python --version). + # + # The default format has the following logic: + # + # 1. Display just "$P9K_CONTENT" if it's equal to "$P9K_PYENV_PYTHON_VERSION" or + # starts with "$P9K_PYENV_PYTHON_VERSION/". + # 2. Otherwise display "$P9K_CONTENT $P9K_PYENV_PYTHON_VERSION". + typeset -g POWERLEVEL9K_PYENV_CONTENT_EXPANSION='${P9K_CONTENT}${${P9K_CONTENT:#$P9K_PYENV_PYTHON_VERSION(|/*)}:+ $P9K_PYENV_PYTHON_VERSION}' + + # Custom icon. + # typeset -g POWERLEVEL9K_PYENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ goenv: go environment (https://github.com/syndbg/goenv) ]################ + # Goenv color. + typeset -g POWERLEVEL9K_GOENV_FOREGROUND=37 + # Hide go version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_GOENV_SOURCES=(shell local global) + # If set to false, hide go version if it's the same as global: + # $(goenv version-name) == $(goenv global). + typeset -g POWERLEVEL9K_GOENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide go version if it's equal to "system". + typeset -g POWERLEVEL9K_GOENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_GOENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ nodenv: node.js version from nodenv (https://github.com/nodenv/nodenv) ]########## + # Nodenv color. + typeset -g POWERLEVEL9K_NODENV_FOREGROUND=70 + # Hide node version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_NODENV_SOURCES=(shell local global) + # If set to false, hide node version if it's the same as global: + # $(nodenv version-name) == $(nodenv global). + typeset -g POWERLEVEL9K_NODENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide node version if it's equal to "system". + typeset -g POWERLEVEL9K_NODENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_NODENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############[ nvm: node.js version from nvm (https://github.com/nvm-sh/nvm) ]############### + # Nvm color. + typeset -g POWERLEVEL9K_NVM_FOREGROUND=70 + # If set to false, hide node version if it's the same as default: + # $(nvm version current) == $(nvm version default). + typeset -g POWERLEVEL9K_NVM_PROMPT_ALWAYS_SHOW=false + # If set to false, hide node version if it's equal to "system". + typeset -g POWERLEVEL9K_NVM_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_NVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ############[ nodeenv: node.js environment (https://github.com/ekalinin/nodeenv) ]############ + # Nodeenv color. + typeset -g POWERLEVEL9K_NODEENV_FOREGROUND=70 + # Don't show Node version next to the environment name. + typeset -g POWERLEVEL9K_NODEENV_SHOW_NODE_VERSION=false + # Separate environment name from Node version only with a space. + typeset -g POWERLEVEL9K_NODEENV_{LEFT,RIGHT}_DELIMITER= + # Custom icon. + # typeset -g POWERLEVEL9K_NODEENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############################[ node_version: node.js version ]############################### + # Node version color. + typeset -g POWERLEVEL9K_NODE_VERSION_FOREGROUND=70 + # Show node version only when in a directory tree containing package.json. + typeset -g POWERLEVEL9K_NODE_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_NODE_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ go_version: go version (https://golang.org) ]######################## + # Go version color. + typeset -g POWERLEVEL9K_GO_VERSION_FOREGROUND=37 + # Show go version only when in a go project subdirectory. + typeset -g POWERLEVEL9K_GO_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_GO_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #################[ rust_version: rustc version (https://www.rust-lang.org) ]################## + # Rust version color. + typeset -g POWERLEVEL9K_RUST_VERSION_FOREGROUND=37 + # Show rust version only when in a rust project subdirectory. + typeset -g POWERLEVEL9K_RUST_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_RUST_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ dotnet_version: .NET version (https://dotnet.microsoft.com) ]################ + # .NET version color. + typeset -g POWERLEVEL9K_DOTNET_VERSION_FOREGROUND=134 + # Show .NET version only when in a .NET project subdirectory. + typeset -g POWERLEVEL9K_DOTNET_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_DOTNET_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################[ php_version: php version (https://www.php.net/) ]###################### + # PHP version color. + typeset -g POWERLEVEL9K_PHP_VERSION_FOREGROUND=99 + # Show PHP version only when in a PHP project subdirectory. + typeset -g POWERLEVEL9K_PHP_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_PHP_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ laravel_version: laravel php framework version (https://laravel.com/) ]########### + # Laravel version color. + typeset -g POWERLEVEL9K_LARAVEL_VERSION_FOREGROUND=161 + # Custom icon. + # typeset -g POWERLEVEL9K_LARAVEL_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ####################[ java_version: java version (https://www.java.com/) ]#################### + # Java version color. + typeset -g POWERLEVEL9K_JAVA_VERSION_FOREGROUND=32 + # Show java version only when in a java project subdirectory. + typeset -g POWERLEVEL9K_JAVA_VERSION_PROJECT_ONLY=true + # Show brief version. + typeset -g POWERLEVEL9K_JAVA_VERSION_FULL=false + # Custom icon. + # typeset -g POWERLEVEL9K_JAVA_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###[ package: name@version from package.json (https://docs.npmjs.com/files/package.json) ]#### + # Package color. + typeset -g POWERLEVEL9K_PACKAGE_FOREGROUND=117 + # Package format. The following parameters are available within the expansion. + # + # - P9K_PACKAGE_NAME The value of `name` field in package.json. + # - P9K_PACKAGE_VERSION The value of `version` field in package.json. + # + # typeset -g POWERLEVEL9K_PACKAGE_CONTENT_EXPANSION='${P9K_PACKAGE_NAME//\%/%%}@${P9K_PACKAGE_VERSION//\%/%%}' + # Custom icon. + # typeset -g POWERLEVEL9K_PACKAGE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ rbenv: ruby version from rbenv (https://github.com/rbenv/rbenv) ]############## + # Rbenv color. + typeset -g POWERLEVEL9K_RBENV_FOREGROUND=168 + # Hide ruby version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_RBENV_SOURCES=(shell local global) + # If set to false, hide ruby version if it's the same as global: + # $(rbenv version-name) == $(rbenv global). + typeset -g POWERLEVEL9K_RBENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide ruby version if it's equal to "system". + typeset -g POWERLEVEL9K_RBENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_RBENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ rvm: ruby version from rvm (https://rvm.io) ]######################## + # Rvm color. + typeset -g POWERLEVEL9K_RVM_FOREGROUND=168 + # Don't show @gemset at the end. + typeset -g POWERLEVEL9K_RVM_SHOW_GEMSET=false + # Don't show ruby- at the front. + typeset -g POWERLEVEL9K_RVM_SHOW_PREFIX=false + # Custom icon. + # typeset -g POWERLEVEL9K_RVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ fvm: flutter version management (https://github.com/leoafarias/fvm) ]############ + # Fvm color. + typeset -g POWERLEVEL9K_FVM_FOREGROUND=38 + # Custom icon. + # typeset -g POWERLEVEL9K_FVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ luaenv: lua version from luaenv (https://github.com/cehoffman/luaenv) ]########### + # Lua color. + typeset -g POWERLEVEL9K_LUAENV_FOREGROUND=32 + # Hide lua version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_LUAENV_SOURCES=(shell local global) + # If set to false, hide lua version if it's the same as global: + # $(luaenv version-name) == $(luaenv global). + typeset -g POWERLEVEL9K_LUAENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide lua version if it's equal to "system". + typeset -g POWERLEVEL9K_LUAENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_LUAENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ jenv: java version from jenv (https://github.com/jenv/jenv) ]################ + # Java color. + typeset -g POWERLEVEL9K_JENV_FOREGROUND=32 + # Hide java version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_JENV_SOURCES=(shell local global) + # If set to false, hide java version if it's the same as global: + # $(jenv version-name) == $(jenv global). + typeset -g POWERLEVEL9K_JENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide java version if it's equal to "system". + typeset -g POWERLEVEL9K_JENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_JENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ plenv: perl version from plenv (https://github.com/tokuhirom/plenv) ]############ + # Perl color. + typeset -g POWERLEVEL9K_PLENV_FOREGROUND=67 + # Hide perl version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PLENV_SOURCES=(shell local global) + # If set to false, hide perl version if it's the same as global: + # $(plenv version-name) == $(plenv global). + typeset -g POWERLEVEL9K_PLENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide perl version if it's equal to "system". + typeset -g POWERLEVEL9K_PLENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_PLENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ perlbrew: perl version from perlbrew (https://github.com/gugod/App-perlbrew) ]############ + # Perlbrew color. + typeset -g POWERLEVEL9K_PERLBREW_FOREGROUND=67 + # Show perlbrew version only when in a perl project subdirectory. + typeset -g POWERLEVEL9K_PERLBREW_PROJECT_ONLY=true + # Don't show "perl-" at the front. + typeset -g POWERLEVEL9K_PERLBREW_SHOW_PREFIX=false + # Custom icon. + # typeset -g POWERLEVEL9K_PERLBREW_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ############[ phpenv: php version from phpenv (https://github.com/phpenv/phpenv) ]############ + # PHP color. + typeset -g POWERLEVEL9K_PHPENV_FOREGROUND=99 + # Hide php version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PHPENV_SOURCES=(shell local global) + # If set to false, hide php version if it's the same as global: + # $(phpenv version-name) == $(phpenv global). + typeset -g POWERLEVEL9K_PHPENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide php version if it's equal to "system". + typeset -g POWERLEVEL9K_PHPENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_PHPENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######[ scalaenv: scala version from scalaenv (https://github.com/scalaenv/scalaenv) ]####### + # Scala color. + typeset -g POWERLEVEL9K_SCALAENV_FOREGROUND=160 + # Hide scala version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_SCALAENV_SOURCES=(shell local global) + # If set to false, hide scala version if it's the same as global: + # $(scalaenv version-name) == $(scalaenv global). + typeset -g POWERLEVEL9K_SCALAENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide scala version if it's equal to "system". + typeset -g POWERLEVEL9K_SCALAENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_SCALAENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ haskell_stack: haskell version from stack (https://haskellstack.org/) ]########### + # Haskell color. + typeset -g POWERLEVEL9K_HASKELL_STACK_FOREGROUND=172 + # Hide haskell version if it doesn't come from one of these sources. + # + # shell: version is set by STACK_YAML + # local: version is set by stack.yaml up the directory tree + # global: version is set by the implicit global project (~/.stack/global-project/stack.yaml) + typeset -g POWERLEVEL9K_HASKELL_STACK_SOURCES=(shell local) + # If set to false, hide haskell version if it's the same as in the implicit global project. + typeset -g POWERLEVEL9K_HASKELL_STACK_ALWAYS_SHOW=true + # Custom icon. + # typeset -g POWERLEVEL9K_HASKELL_STACK_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ terraform: terraform workspace (https://www.terraform.io) ]################# + # Don't show terraform workspace if it's literally "default". + typeset -g POWERLEVEL9K_TERRAFORM_SHOW_DEFAULT=false + # POWERLEVEL9K_TERRAFORM_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current terraform workspace gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_TERRAFORM_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_TERRAFORM_CLASSES defines the workspace class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' OTHER) + # + # If your current terraform workspace is "project_test", its class is TEST because "project_test" + # doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' OTHER) + typeset -g POWERLEVEL9K_TERRAFORM_OTHER_FOREGROUND=38 + # typeset -g POWERLEVEL9K_TERRAFORM_OTHER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ terraform_version: terraform version (https://www.terraform.io) ]############## + # Terraform version color. + typeset -g POWERLEVEL9K_TERRAFORM_VERSION_FOREGROUND=38 + # Custom icon. + # typeset -g POWERLEVEL9K_TERRAFORM_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ kubecontext: current kubernetes context (https://kubernetes.io/) ]############# + # Show kubecontext only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show kubecontext. + typeset -g POWERLEVEL9K_KUBECONTEXT_SHOW_ON_COMMAND='kubectl|helm|kubens|kubectx|oc|istioctl|kogito|k9s|helmfile|flux|fluxctl|stern|kubeseal|skaffold|kubent|kubecolor|cmctl|sparkctl' + + # Kubernetes context classes for the purpose of using different colors, icons and expansions with + # different contexts. + # + # POWERLEVEL9K_KUBECONTEXT_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current kubernetes context gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_KUBECONTEXT_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_KUBECONTEXT_CLASSES defines the context class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' DEFAULT) + # + # If your current kubernetes context is "deathray-testing/default", its class is TEST + # because "deathray-testing/default" doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_FOREGROUND=134 + # typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use POWERLEVEL9K_KUBECONTEXT_CONTENT_EXPANSION to specify the content displayed by kubecontext + # segment. Parameter expansions are very flexible and fast, too. See reference: + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion. + # + # Within the expansion the following parameters are always available: + # + # - P9K_CONTENT The content that would've been displayed if there was no content + # expansion defined. + # - P9K_KUBECONTEXT_NAME The current context's name. Corresponds to column NAME in the + # output of `kubectl config get-contexts`. + # - P9K_KUBECONTEXT_CLUSTER The current context's cluster. Corresponds to column CLUSTER in the + # output of `kubectl config get-contexts`. + # - P9K_KUBECONTEXT_NAMESPACE The current context's namespace. Corresponds to column NAMESPACE + # in the output of `kubectl config get-contexts`. If there is no + # namespace, the parameter is set to "default". + # - P9K_KUBECONTEXT_USER The current context's user. Corresponds to column AUTHINFO in the + # output of `kubectl config get-contexts`. + # + # If the context points to Google Kubernetes Engine (GKE) or Elastic Kubernetes Service (EKS), + # the following extra parameters are available: + # + # - P9K_KUBECONTEXT_CLOUD_NAME Either "gke" or "eks". + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT Account/project ID. + # - P9K_KUBECONTEXT_CLOUD_ZONE Availability zone. + # - P9K_KUBECONTEXT_CLOUD_CLUSTER Cluster. + # + # P9K_KUBECONTEXT_CLOUD_* parameters are derived from P9K_KUBECONTEXT_CLUSTER. For example, + # if P9K_KUBECONTEXT_CLUSTER is "gke_my-account_us-east1-a_my-cluster-01": + # + # - P9K_KUBECONTEXT_CLOUD_NAME=gke + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT=my-account + # - P9K_KUBECONTEXT_CLOUD_ZONE=us-east1-a + # - P9K_KUBECONTEXT_CLOUD_CLUSTER=my-cluster-01 + # + # If P9K_KUBECONTEXT_CLUSTER is "arn:aws:eks:us-east-1:123456789012:cluster/my-cluster-01": + # + # - P9K_KUBECONTEXT_CLOUD_NAME=eks + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT=123456789012 + # - P9K_KUBECONTEXT_CLOUD_ZONE=us-east-1 + # - P9K_KUBECONTEXT_CLOUD_CLUSTER=my-cluster-01 + typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION= + # Show P9K_KUBECONTEXT_CLOUD_CLUSTER if it's not empty and fall back to P9K_KUBECONTEXT_NAME. + POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${P9K_KUBECONTEXT_CLOUD_CLUSTER:-${P9K_KUBECONTEXT_NAME}}' + # Append the current context's namespace if it's not "default". + POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${${:-/$P9K_KUBECONTEXT_NAMESPACE}:#/default}' + + # Custom prefix. + # typeset -g POWERLEVEL9K_KUBECONTEXT_PREFIX='%248Fat ' + + #[ aws: aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) ]# + # Show aws only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show aws. + typeset -g POWERLEVEL9K_AWS_SHOW_ON_COMMAND='aws|awless|cdk|terraform|pulumi|terragrunt' + + # POWERLEVEL9K_AWS_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current AWS profile gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_AWS_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_AWS_CLASSES defines the profile class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_AWS_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' DEFAULT) + # + # If your current AWS profile is "company_test", its class is TEST + # because "company_test" doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_AWS_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_AWS_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_AWS_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_AWS_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_AWS_DEFAULT_FOREGROUND=208 + # typeset -g POWERLEVEL9K_AWS_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # AWS segment format. The following parameters are available within the expansion. + # + # - P9K_AWS_PROFILE The name of the current AWS profile. + # - P9K_AWS_REGION The region associated with the current AWS profile. + typeset -g POWERLEVEL9K_AWS_CONTENT_EXPANSION='${P9K_AWS_PROFILE//\%/%%}${P9K_AWS_REGION:+ ${P9K_AWS_REGION//\%/%%}}' + + #[ aws_eb_env: aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/) ]# + # AWS Elastic Beanstalk environment color. + typeset -g POWERLEVEL9K_AWS_EB_ENV_FOREGROUND=70 + # Custom icon. + # typeset -g POWERLEVEL9K_AWS_EB_ENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ azure: azure account name (https://docs.microsoft.com/en-us/cli/azure) ]########## + # Show azure only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show azure. + typeset -g POWERLEVEL9K_AZURE_SHOW_ON_COMMAND='az|terraform|pulumi|terragrunt' + + # POWERLEVEL9K_AZURE_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current azure account name gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_AZURE_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_AZURE_CLASSES defines the account class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_AZURE_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' OTHER) + # + # If your current azure account is "company_test", its class is TEST because "company_test" + # doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_AZURE_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_AZURE_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_AZURE_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_AZURE_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' OTHER) + + # Azure account name color. + typeset -g POWERLEVEL9K_AZURE_OTHER_FOREGROUND=32 + # Custom icon. + # typeset -g POWERLEVEL9K_AZURE_OTHER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ gcloud: google cloud account and project (https://cloud.google.com/) ]########### + # Show gcloud only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show gcloud. + typeset -g POWERLEVEL9K_GCLOUD_SHOW_ON_COMMAND='gcloud|gcs|gsutil' + # Google cloud color. + typeset -g POWERLEVEL9K_GCLOUD_FOREGROUND=32 + + # Google cloud format. Change the value of POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION and/or + # POWERLEVEL9K_GCLOUD_COMPLETE_CONTENT_EXPANSION if the default is too verbose or not informative + # enough. You can use the following parameters in the expansions. Each of them corresponds to the + # output of `gcloud` tool. + # + # Parameter | Source + # -------------------------|-------------------------------------------------------------------- + # P9K_GCLOUD_CONFIGURATION | gcloud config configurations list --format='value(name)' + # P9K_GCLOUD_ACCOUNT | gcloud config get-value account + # P9K_GCLOUD_PROJECT_ID | gcloud config get-value project + # P9K_GCLOUD_PROJECT_NAME | gcloud projects describe $P9K_GCLOUD_PROJECT_ID --format='value(name)' + # + # Note: ${VARIABLE//\%/%%} expands to ${VARIABLE} with all occurrences of '%' replaced with '%%'. + # + # Obtaining project name requires sending a request to Google servers. This can take a long time + # and even fail. When project name is unknown, P9K_GCLOUD_PROJECT_NAME is not set and gcloud + # prompt segment is in state PARTIAL. When project name gets known, P9K_GCLOUD_PROJECT_NAME gets + # set and gcloud prompt segment transitions to state COMPLETE. + # + # You can customize the format, icon and colors of gcloud segment separately for states PARTIAL + # and COMPLETE. You can also hide gcloud in state PARTIAL by setting + # POWERLEVEL9K_GCLOUD_PARTIAL_VISUAL_IDENTIFIER_EXPANSION and + # POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION to empty. + typeset -g POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT_ID//\%/%%}' + typeset -g POWERLEVEL9K_GCLOUD_COMPLETE_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT_NAME//\%/%%}' + + # Send a request to Google (by means of `gcloud projects describe ...`) to obtain project name + # this often. Negative value disables periodic polling. In this mode project name is retrieved + # only when the current configuration, account or project id changes. + typeset -g POWERLEVEL9K_GCLOUD_REFRESH_PROJECT_NAME_SECONDS=60 + + # Custom icon. + # typeset -g POWERLEVEL9K_GCLOUD_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #[ google_app_cred: google application credentials (https://cloud.google.com/docs/authentication/production) ]# + # Show google_app_cred only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show google_app_cred. + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_SHOW_ON_COMMAND='terraform|pulumi|terragrunt' + + # Google application credentials classes for the purpose of using different colors, icons and + # expansions with different credentials. + # + # POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES is an array with even number of elements. The first + # element in each pair defines a pattern against which the current kubernetes context gets + # matched. More specifically, it's P9K_CONTENT prior to the application of context expansion + # (see below) that gets matched. If you unset all POWERLEVEL9K_GOOGLE_APP_CRED_*CONTENT_EXPANSION + # parameters, you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES defines the context class. Patterns are tried in order. + # The first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=( + # '*:*prod*:*' PROD + # '*:*test*:*' TEST + # '*' DEFAULT) + # + # If your current Google application credentials is "service_account deathray-testing x@y.com", + # its class is TEST because it doesn't match the pattern '* *prod* *' but does match '* *test* *'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_CONTENT_EXPANSION='$P9K_GOOGLE_APP_CRED_PROJECT_ID' + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=( + # '*:*prod*:*' PROD # These values are examples that are unlikely + # '*:*test*:*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_FOREGROUND=32 + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use POWERLEVEL9K_GOOGLE_APP_CRED_CONTENT_EXPANSION to specify the content displayed by + # google_app_cred segment. Parameter expansions are very flexible and fast, too. See reference: + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion. + # + # You can use the following parameters in the expansion. Each of them corresponds to one of the + # fields in the JSON file pointed to by GOOGLE_APPLICATION_CREDENTIALS. + # + # Parameter | JSON key file field + # ---------------------------------+--------------- + # P9K_GOOGLE_APP_CRED_TYPE | type + # P9K_GOOGLE_APP_CRED_PROJECT_ID | project_id + # P9K_GOOGLE_APP_CRED_CLIENT_EMAIL | client_email + # + # Note: ${VARIABLE//\%/%%} expands to ${VARIABLE} with all occurrences of '%' replaced by '%%'. + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_CONTENT_EXPANSION='${P9K_GOOGLE_APP_CRED_PROJECT_ID//\%/%%}' + + ##############[ toolbox: toolbox name (https://github.com/containers/toolbox) ]############### + # Toolbox color. + typeset -g POWERLEVEL9K_TOOLBOX_FOREGROUND=178 + # Don't display the name of the toolbox if it matches fedora-toolbox-*. + typeset -g POWERLEVEL9K_TOOLBOX_CONTENT_EXPANSION='${P9K_TOOLBOX_NAME:#fedora-toolbox-*}' + # Custom icon. + # typeset -g POWERLEVEL9K_TOOLBOX_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_TOOLBOX_PREFIX='%248Fin ' + + ###############################[ public_ip: public IP address ]############################### + # Public IP color. + typeset -g POWERLEVEL9K_PUBLIC_IP_FOREGROUND=94 + # Custom icon. + # typeset -g POWERLEVEL9K_PUBLIC_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ########################[ vpn_ip: virtual private network indicator ]######################### + # VPN IP color. + typeset -g POWERLEVEL9K_VPN_IP_FOREGROUND=81 + # When on VPN, show just an icon without the IP address. + # Tip: To display the private IP address when on VPN, remove the next line. + typeset -g POWERLEVEL9K_VPN_IP_CONTENT_EXPANSION= + # Regular expression for the VPN network interface. Run `ifconfig` or `ip -4 a show` while on VPN + # to see the name of the interface. + typeset -g POWERLEVEL9K_VPN_IP_INTERFACE='(gpd|wg|(.*tun)|tailscale)[0-9]*|(zt.*)' + # If set to true, show one segment per matching network interface. If set to false, show only + # one segment corresponding to the first matching network interface. + # Tip: If you set it to true, you'll probably want to unset POWERLEVEL9K_VPN_IP_CONTENT_EXPANSION. + typeset -g POWERLEVEL9K_VPN_IP_SHOW_ALL=false + # Custom icon. + # typeset -g POWERLEVEL9K_VPN_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ ip: ip address and bandwidth usage for a specified network interface ]########### + # IP color. + typeset -g POWERLEVEL9K_IP_FOREGROUND=38 + # The following parameters are accessible within the expansion: + # + # Parameter | Meaning + # ----------------------+------------------------------------------- + # P9K_IP_IP | IP address + # P9K_IP_INTERFACE | network interface + # P9K_IP_RX_BYTES | total number of bytes received + # P9K_IP_TX_BYTES | total number of bytes sent + # P9K_IP_RX_BYTES_DELTA | number of bytes received since last prompt + # P9K_IP_TX_BYTES_DELTA | number of bytes sent since last prompt + # P9K_IP_RX_RATE | receive rate (since last prompt) + # P9K_IP_TX_RATE | send rate (since last prompt) + typeset -g POWERLEVEL9K_IP_CONTENT_EXPANSION='${P9K_IP_RX_RATE:+%70F⇣$P9K_IP_RX_RATE }${P9K_IP_TX_RATE:+%215F⇡$P9K_IP_TX_RATE }%38F$P9K_IP_IP' + # Show information for the first network interface whose name matches this regular expression. + # Run `ifconfig` or `ip -4 a show` to see the names of all network interfaces. + typeset -g POWERLEVEL9K_IP_INTERFACE='[ew].*' + # Custom icon. + # typeset -g POWERLEVEL9K_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #########################[ proxy: system-wide http/https/ftp proxy ]########################## + # Proxy color. + typeset -g POWERLEVEL9K_PROXY_FOREGROUND=68 + # Custom icon. + # typeset -g POWERLEVEL9K_PROXY_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################################[ battery: internal battery ]################################# + # Show battery in red when it's below this level and not connected to power supply. + typeset -g POWERLEVEL9K_BATTERY_LOW_THRESHOLD=20 + typeset -g POWERLEVEL9K_BATTERY_LOW_FOREGROUND=160 + # Show battery in green when it's charging or fully charged. + typeset -g POWERLEVEL9K_BATTERY_{CHARGING,CHARGED}_FOREGROUND=70 + # Show battery in yellow when it's discharging. + typeset -g POWERLEVEL9K_BATTERY_DISCONNECTED_FOREGROUND=178 + # Battery pictograms going from low to high level of charge. + typeset -g POWERLEVEL9K_BATTERY_STAGES='\UF008E\UF007A\UF007B\UF007C\UF007D\UF007E\UF007F\UF0080\UF0081\UF0082\UF0079' + # Don't show the remaining time to charge/discharge. + typeset -g POWERLEVEL9K_BATTERY_VERBOSE=false + + #####################################[ wifi: wifi speed ]##################################### + # WiFi color. + typeset -g POWERLEVEL9K_WIFI_FOREGROUND=68 + # Custom icon. + # typeset -g POWERLEVEL9K_WIFI_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use different colors and icons depending on signal strength ($P9K_WIFI_BARS). + # + # # Wifi colors and icons for different signal strength levels (low to high). + # typeset -g my_wifi_fg=(68 68 68 68 68) # <-- change these values + # typeset -g my_wifi_icon=('WiFi' 'WiFi' 'WiFi' 'WiFi' 'WiFi') # <-- change these values + # + # typeset -g POWERLEVEL9K_WIFI_CONTENT_EXPANSION='%F{${my_wifi_fg[P9K_WIFI_BARS+1]}}$P9K_WIFI_LAST_TX_RATE Mbps' + # typeset -g POWERLEVEL9K_WIFI_VISUAL_IDENTIFIER_EXPANSION='%F{${my_wifi_fg[P9K_WIFI_BARS+1]}}${my_wifi_icon[P9K_WIFI_BARS+1]}' + # + # The following parameters are accessible within the expansions: + # + # Parameter | Meaning + # ----------------------+--------------- + # P9K_WIFI_SSID | service set identifier, a.k.a. network name + # P9K_WIFI_LINK_AUTH | authentication protocol such as "wpa2-psk" or "none"; empty if unknown + # P9K_WIFI_LAST_TX_RATE | wireless transmit rate in megabits per second + # P9K_WIFI_RSSI | signal strength in dBm, from -120 to 0 + # P9K_WIFI_NOISE | noise in dBm, from -120 to 0 + # P9K_WIFI_BARS | signal strength in bars, from 0 to 4 (derived from P9K_WIFI_RSSI and P9K_WIFI_NOISE) + + ####################################[ time: current time ]#################################### + # Current time color. + typeset -g POWERLEVEL9K_TIME_FOREGROUND=66 + # Format for the current time: 09:51:02. See `man 3 strftime`. + typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M:%S}' + # If set to true, time will update when you hit enter. This way prompts for the past + # commands will contain the start times of their commands as opposed to the default + # behavior where they contain the end times of their preceding commands. + typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false + # Custom icon. + # typeset -g POWERLEVEL9K_TIME_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_TIME_PREFIX='%248Fat ' + + # Example of a user-defined prompt segment. Function prompt_example will be called on every + # prompt if `example` prompt segment is added to POWERLEVEL9K_LEFT_PROMPT_ELEMENTS or + # POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS. It displays an icon and orange text greeting the user. + # + # Type `p10k help segment` for documentation and a more sophisticated example. + function prompt_example() { + p10k segment -f 208 -i '⭐' -t 'hello, %n' + } + + # User-defined prompt segments may optionally provide an instant_prompt_* function. Its job + # is to generate the prompt segment for display in instant prompt. See + # https://github.com/romkatv/powerlevel10k#instant-prompt. + # + # Powerlevel10k will call instant_prompt_* at the same time as the regular prompt_* function + # and will record all `p10k segment` calls it makes. When displaying instant prompt, Powerlevel10k + # will replay these calls without actually calling instant_prompt_*. It is imperative that + # instant_prompt_* always makes the same `p10k segment` calls regardless of environment. If this + # rule is not observed, the content of instant prompt will be incorrect. + # + # Usually, you should either not define instant_prompt_* or simply call prompt_* from it. If + # instant_prompt_* is not defined for a segment, the segment won't be shown in instant prompt. + function instant_prompt_example() { + # Since prompt_example always makes the same `p10k segment` calls, we can call it from + # instant_prompt_example. This will give us the same `example` prompt segment in the instant + # and regular prompts. + prompt_example + } + + # User-defined prompt segments can be customized the same way as built-in segments. + # typeset -g POWERLEVEL9K_EXAMPLE_FOREGROUND=208 + # typeset -g POWERLEVEL9K_EXAMPLE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Transient prompt works similarly to the builtin transient_rprompt option. It trims down prompt + # when accepting a command line. Supported values: + # + # - off: Don't change prompt when accepting a command line. + # - always: Trim down prompt when accepting a command line. + # - same-dir: Trim down prompt when accepting a command line unless this is the first command + # typed after changing current working directory. + typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always + + # Instant prompt mode. + # + # - off: Disable instant prompt. Choose this if you've tried instant prompt and found + # it incompatible with your zsh configuration files. + # - quiet: Enable instant prompt and don't print warnings when detecting console output + # during zsh initialization. Choose this if you've read and understood + # https://github.com/romkatv/powerlevel10k#instant-prompt. + # - verbose: Enable instant prompt and print a warning when detecting console output during + # zsh initialization. Choose this if you've never tried instant prompt, haven't + # seen the warning, or if you are unsure what this all means. + typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose + + # Hot reload allows you to change POWERLEVEL9K options after Powerlevel10k has been initialized. + # For example, you can type POWERLEVEL9K_BACKGROUND=red and see your prompt turn red. Hot reload + # can slow down prompt by 1-2 milliseconds, so it's better to keep it turned off unless you + # really need it. + typeset -g POWERLEVEL9K_DISABLE_HOT_RELOAD=true + + # If p10k is already loaded, reload configuration. + # This works even with POWERLEVEL9K_DISABLE_HOT_RELOAD=true. + (( ! $+functions[p10k] )) || p10k reload +} + +# Tell `p10k configure` which file it should overwrite. +typeset -g POWERLEVEL9K_CONFIG_FILE=${${(%):-%x}:a} + +(( ${#p10k_config_opts} )) && setopt ${p10k_config_opts[@]} +'builtin' 'unset' 'p10k_config_opts' diff --git a/private_dot_zshenv b/private_dot_zshenv new file mode 100644 index 0000000..7173405 --- /dev/null +++ b/private_dot_zshenv @@ -0,0 +1,3 @@ +# Guarded. Unguarded this printed "no such file or directory" on every zsh +# start on any machine without rustup -- which is every throwaway VM. +[ -r "$HOME/.cargo/env" ] && . "$HOME/.cargo/env" diff --git a/private_dot_zshrc b/private_dot_zshrc new file mode 100644 index 0000000..dd7a9a5 --- /dev/null +++ b/private_dot_zshrc @@ -0,0 +1,192 @@ +# ~/.zshrc +# +# A PLAIN FILE. Not a chezmoi template, deliberately. +# +# Templating this file is what broke sync-back: `chezmoi re-add` silently does +# nothing on a .tmpl, so every edit made here had to be hand-copied into the +# source or lost. Platform differences are handled at runtime below, by zsh, +# which it is perfectly capable of doing. +# +# NO SECRETS LIVE HERE. Keys come from ~/.config/zsh/secrets.zsh, which is +# generated from Bitwarden Secrets Manager, mode 600, and in no repository. +# If that file is absent this shell still works — you just have no API keys. + +# ---------------------------------------------------------------- platform --- +# Set before anything else; everything below branches on these instead of on +# chezmoi template conditionals. +typeset -g _mac=0 _lin=0 _wsl=0 _ubuntu=0 +[[ $OSTYPE == darwin* ]] && _mac=1 +[[ $OSTYPE == linux* ]] && _lin=1 +[[ -n $WSL_DISTRO_NAME || -r /proc/sys/fs/binfmt_misc/WSLInterop ]] && _wsl=1 +(( _lin )) && [[ -r /etc/os-release ]] && \ + grep -qi '^ID=ubuntu' /etc/os-release && _ubuntu=1 + +# ----------------------------------------------------------------- secrets --- +# One guarded source, and it is the only path by which a key enters this shell. +# Regenerate with `dotsecrets`. Never edit by hand, never commit. +[[ -r ${XDG_CONFIG_HOME:-$HOME/.config}/zsh/secrets.zsh ]] && \ + source ${XDG_CONFIG_HOME:-$HOME/.config}/zsh/secrets.zsh + +# The bws token itself is a file, not a secret in this file. It is what lets +# `bws` and `dotsecrets` run at all. +[[ -r $HOME/.config/bitwarden/bws-token ]] && \ + export BWS_ACCESS_TOKEN="$(<$HOME/.config/bitwarden/bws-token)" + +# --------------------------------------------------------- pre-prompt setup --- +# Anything that must be set before the p10k instant prompt goes here. Nothing +# in this section may write to stdout. +export ZSH_AI_PROVIDER="gemini" +export ZSH_AI_GEMINI_MODEL="gemini-2.5-flash" + +# Endpoint, not a credential — the matching key comes from secrets.zsh. +export ZAI_BASE_URL="https://api.z.ai/api/anthropic" + +(( _ubuntu )) && { + export GDK_BACKEND=wayland + export QT_QPA_PLATFORM=wayland + export CLUTTER_BACKEND=wayland + export SDL_VIDEODRIVER=wayland +} + +# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. +# Initialization code that may require console input (password prompts, [y/n] +# confirmations, etc.) must go above this block; everything else may go below. +if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then + source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" +fi + +# -------------------------------------------------------------------- PATH --- +# `typeset -U` makes $path a unique-only array, so ordering is intentional and +# duplicates are impossible — which is what the eight scattered appends and the +# doubled ~/.local/bin used to produce. +typeset -U path PATH +_prepend() { [[ -d $1 ]] && path=("$1" $path) } +_append() { [[ -d $1 ]] && path=($path "$1") } + +_prepend "$HOME/bin" +_prepend "$HOME/.local/bin" +_prepend "$HOME/.npm-global/bin" +_prepend "$HOME/code/speccy/bin" +_append /usr/local/bin +_append /usr/local/go/bin + +export GOPATH="$HOME/go" +export GOMODCACHE="$GOPATH/pkg/mod" +_prepend "$GOPATH/bin" + +_append "$HOME/.cache/lm-studio/bin" + +(( _mac )) && { + _prepend /opt/homebrew/bin + _prepend /opt/homebrew/sbin + _prepend /opt/homebrew/opt/hcxtools/bin + [[ -n $BUN_INSTALL ]] && _prepend "$BUN_INSTALL/bin" + export TERMINFO=/Applications/Ghostty.app/Contents/Resources/terminfo + + # Garmin Connect IQ SDK (tracks the SDK selected in current-sdk.cfg) + _ciq="$HOME/Library/Application Support/Garmin/ConnectIQ/current-sdk.cfg" + [[ -f $_ciq ]] && _append "$(<"$_ciq")bin" + unset _ciq +} + +(( _lin )) && { + _append /usr/libexec/virtiofsd + _append "$HOME/code/vp9enc-rs/target/release" +} + +(( _ubuntu )) && export LD_LIBRARY_PATH="/opt/Qt-6.7.3/lib:$LD_LIBRARY_PATH" + +unfunction _prepend _append + +# ------------------------------------------------------------------ limits --- +ulimit -n 65535 +(( _mac )) && ulimit -u 2048 || ulimit -u 65535 + +# ------------------------------------------------------------------ oh-my-zsh - +export ZSH="$HOME/.oh-my-zsh" +export ZSH_CUSTOM="${ZSH_CUSTOM:-$ZSH/custom}" +ZSH_THEME="powerlevel10k/powerlevel10k" + +plugins=(git zsh-autosuggestions sudo web-search dirhistory history) +(( _mac )) && plugins+=(docker) + +# zsh-ai is loaded HERE and only here. It used to be added to this array on +# every platform AND separately sourced from /opt/homebrew on macOS, so it +# loaded twice on mac. The existence guard also stops the array referencing a +# plugin that was never installed. +[[ -z $ZSH_AI_DISABLE && -d $ZSH_CUSTOM/plugins/zsh-ai ]] && plugins+=(zsh-ai) + +# Guarded. Unguarded, a clean bootstrap produces a broken login shell and +# nothing tells you why. +if [[ -r $ZSH/oh-my-zsh.sh ]]; then + source $ZSH/oh-my-zsh.sh +else + print -u2 "zshrc: oh-my-zsh missing at $ZSH — run dotup to install it" +fi + +zle -N create_completion +bindkey '^B' create_completion + +[[ -f ~/.p10k.zsh ]] && source ~/.p10k.zsh + +# ----------------------------------------------------------------- aliases --- +(( _ubuntu )) && alias fd=fdfind +(( _mac )) && { + alias py="/opt/homebrew/opt/python@3.13/Frameworks/Python.framework/Versions/3.13/bin/python3" + alias tailscale="/Applications/Tailscale.app/Contents/MacOS/Tailscale" +} + +alias umpv="$HOME/code/mpv/TOOLS/umpv" +(( _lin )) && { + alias lada-runner="LADA_RUNNER_CONCURRENCY=2 uv --directory $HOME/tools/lada-runner run lada-runner tui" + alias lada-cli="flatpak run --filesystem=host --command=lada-cli io.github.ladaapp.lada" +} + +# Work Claude account — separate creds/history/projects, shared CLAUDE.md+rules+skills+agents +alias wclaude='CLAUDE_CONFIG_DIR=$HOME/.claude-work claude' + +# A function, not an alias. As an alias the token was expanded into the alias +# body at definition time, so `alias` printed the key in full to anyone who ran +# it. A function defers expansion to call time. +glm() { + ANTHROPIC_BASE_URL="$ZAI_BASE_URL" ANTHROPIC_AUTH_TOKEN="$ZAI_API_KEY" \ + claude --dangerously-skip-permissions \ + --settings "$HOME/.claude/settings-glm.json" "$@" +} + +_open() { (( _mac )) && open "$1" || xdg-open "$1" >/dev/null 2>&1 } + +# ------------------------------------------------------------------- local --- +# Identity-bearing shell config -- the gitea aliases and their WAN/LAN hosts, +# work shortcuts, anything naming a host you own -- lives in the PRIVATE tier, +# which writes this file. It is absent on a public-only machine, and the guard +# makes that a silent no-op. `_mac` and `_open` above are still in scope here, +# so a moved alias needs no rewriting. +[[ -r ${XDG_CONFIG_HOME:-$HOME/.config}/zsh/local.zsh ]] && \ + source ${XDG_CONFIG_HOME:-$HOME/.config}/zsh/local.zsh + +# ---------------------------------------------------------------- homebrew --- +if (( _mac )); then + eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null || /usr/local/bin/brew shellenv)" +elif [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +fi + +# ---------------------------------------------------------------- bitwarden --- +# Interactive personal-vault unlock. Nothing automated depends on this; the +# unattended path is bws + the token file above. +if command -v bw >/dev/null 2>&1; then + bwunlock() { + export BW_SESSION="$(bw unlock --raw)" + print "Bitwarden unlocked. Session expires based on your timeout setting." + } +fi + +export ZSH_CODEX_PYTHON="$HOME/.local/venvs/codex/bin/python" + +# iris was removed here. Do NOT add `eval "$(iris init zsh)"` — it emits +# `exec iris`, which replaces every interactive shell, and combined with the +# p10k instant prompt above (upstream #55) logins fork-cascade until SSH +# appears frozen. See the sysjournal note before reintroducing it. + +unset _mac _lin _wsl _ubuntu diff --git a/symlink_dot_tmux.conf b/symlink_dot_tmux.conf new file mode 100644 index 0000000..6a08cee --- /dev/null +++ b/symlink_dot_tmux.conf @@ -0,0 +1 @@ +.tmux/.tmux.conf