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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||||
@@ -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"]
|
||||||
@@ -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
|
||||||
+34
@@ -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
|
||||||
Executable
+32
@@ -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
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
_log
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
_log
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
_log
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
_log
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
_log
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
_log
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
_log
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
_log
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
_log
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
_log
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
_log
|
||||||
Executable
+4
@@ -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
|
||||||
Executable
+46
@@ -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
|
||||||
Executable
+347
@@ -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 <desc> <expected> <actual>
|
||||||
|
if [ "$2" = "$3" ]; then ok "$1"; else no "$1 — want [$2] got [$3]"; fi
|
||||||
|
}
|
||||||
|
has() { # has <desc> <needle> <haystack>
|
||||||
|
case $3 in *"$2"*) ok "$1" ;; *) no "$1 — [$2] not in output" ;; esac
|
||||||
|
}
|
||||||
|
hasnt(){ # hasnt <desc> <needle> <haystack>
|
||||||
|
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" </dev/null 2>&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 ]
|
||||||
@@ -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 <this repo url>
|
||||||
|
~/.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.
|
||||||
@@ -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
|
||||||
Executable
+893
@@ -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 <group/pkg> <dep> [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
|
||||||
|
}
|
||||||
|
# @group<TAB>note — 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: <channel><TAB><argument>
|
||||||
|
# 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 <channel><TAB><arg><TAB><key> 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:<owner>/<repo>:<asset substring> 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/<asset matching *$match*>"
|
||||||
|
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://<user>:<read-only-token>@host/path/dotfiles-private.git
|
||||||
|
# BWS_ACCESS_TOKEN=<machine account token, scoped to one project>
|
||||||
|
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<y) exit 1 }
|
||||||
|
exit 0 }'
|
||||||
|
}
|
||||||
|
|
||||||
|
fzf_version() { fzf --version 2>/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
|
||||||
Executable
+167
@@ -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 <keywords...> Recall: grep the journal first (case-insensitive)
|
||||||
|
sysjournal new "<Title>" [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 "$@"
|
||||||
@@ -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
|
||||||
|
Can't render this file because it contains an unexpected character in line 17 and column 49.
|
@@ -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
|
||||||
@@ -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 }}
|
||||||
@@ -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
|
||||||
@@ -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 }}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
**/.claude/settings.local.json
|
||||||
@@ -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"
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
tt.*
|
||||||
|
.tests
|
||||||
|
doc/tags
|
||||||
|
debug
|
||||||
|
.repro
|
||||||
|
foo.*
|
||||||
|
*.log
|
||||||
|
data
|
||||||
|
.claude
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"neodev": {
|
||||||
|
"library": {
|
||||||
|
"enabled": true,
|
||||||
|
"plugins": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"neoconf": {
|
||||||
|
"plugins": {
|
||||||
|
"lua_ls": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- bootstrap lazy.nvim, LazyVim and your plugins
|
||||||
|
require("config.lazy")
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
@@ -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" })
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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" }
|
||||||
@@ -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)" })
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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 = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Navigation enhancement plugins
|
||||||
|
-- (neoscroll removed due to conflicts with smear-cursor)
|
||||||
|
return {}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Disable noice.nvim (causing treesitter errors)
|
||||||
|
return {
|
||||||
|
{ "folke/noice.nvim", enabled = false },
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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)" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
indent_type = "Spaces"
|
||||||
|
indent_width = 2
|
||||||
|
column_width = 120
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
prefix={{ .chezmoi.homeDir }}/.npm-global
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.tmux/.tmux.conf
|
||||||
Reference in New Issue
Block a user