Homescale — a roaming home for your fleet
SSH into one stable Tailscale node and reach your whole tailnet from it via short per-host functions + sshfs mounts. A riced Arch dev container with sshd, a stateful home, and declarative .env auth. See README.md and ROAMING.md.
This commit is contained in:
commit
c349fe488c
@ -0,0 +1,39 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
|
||||||
|
NhAAAAAwEAAQAAAYEAnFyYUvo2ItCC0o3MmXWKFEqV32AyTIO7gUhZOXEHqeBATnwbITFk
|
||||||
|
a6EIoAyenLeimd7Kn7SvPNoedxLgdq5AorLO1QMLCvd8ccVtaizREmpGWCB7iDn1xguf+9
|
||||||
|
Zmg6EcQgp7NVgYSlJV7mVGfhYib5Zh6cFlxWkNEOrVaSxu9yYsMg0dA0o+G+DbrtitArY0
|
||||||
|
lBiFcUA/URAdXiKIVR83sk4OeTcQQ5MijaKWGq95y5S6qP3fyvm7Lbe9XC43GFliMlhIN+
|
||||||
|
EHtmREVoZyqFIzO2ZZ3o/SWdM9TfPwCbuJNBH1ZY9z0lI7/YROsJvtbK8973fq4lsuV8FY
|
||||||
|
zuLe7R8R/acMv5n0ZTJa8Ky5/wt3vVuYkxx9XJ+eGoSBxzOge9LMsLb0QojUofPcRI0Nln
|
||||||
|
rwE3MHKFTEw/+hsyEhKZ42+jueyCbs3WdhzMq1WGcxa4Rwe7tQVthMLjpkhbr2x2R4stRT
|
||||||
|
oxbqgNdj1Nke0qaniiQCbASuqS1t39Dm02SaRZujAAAFoAF9wnoBfcJ6AAAAB3NzaC1yc2
|
||||||
|
EAAAGBAJxcmFL6NiLQgtKNzJl1ihRKld9gMkyDu4FIWTlxB6ngQE58GyExZGuhCKAMnpy3
|
||||||
|
opneyp+0rzzaHncS4HauQKKyztUDCwr3fHHFbWos0RJqRlgge4g59cYLn/vWZoOhHEIKez
|
||||||
|
VYGEpSVe5lRn4WIm+WYenBZcVpDRDq1WksbvcmLDINHQNKPhvg267YrQK2NJQYhXFAP1EQ
|
||||||
|
HV4iiFUfN7JODnk3EEOTIo2ilhqvecuUuqj938r5uy23vVwuNxhZYjJYSDfhB7ZkRFaGcq
|
||||||
|
hSMztmWd6P0lnTPU3z8Am7iTQR9WWPc9JSO/2ETrCb7WyvPe936uJbLlfBWM7i3u0fEf2n
|
||||||
|
DL+Z9GUyWvCsuf8Ld71bmJMcfVyfnhqEgcczoHvSzLC29EKI1KHz3ESNDZZ68BNzByhUxM
|
||||||
|
P/obMhISmeNvo7nsgm7N1nYczKtVhnMWuEcHu7UFbYTC46ZIW69sdkeLLUU6MW6oDXY9TZ
|
||||||
|
HtKmp4okAmwErqktbd/Q5tNkmkWbowAAAAMBAAEAAAGAAJ825ugxmoCPnCR0lFUcSCIL+R
|
||||||
|
44OVCwF7GC11/Q1rGv2bLcd5bZB8Nz8uSsfxavjPCjKm611ERZKxHnux4Rusc9yWpGAkvh
|
||||||
|
WADSh3y0Ic4WbRzRXCUHUXaKvT57iiRBVM/6o1YVvYGOutj6rIXTdduTzXDtfu6TKAoT6G
|
||||||
|
OC/bPELOcjk65yo5rP093i2FC/5kwC9TNqrApOwfIOeLfCoh6yhUtePitDpQ4Nhixc50yX
|
||||||
|
4kTYLXSflO72fJvkk796f+kwuFDP788lo1gohrNZYsZKr20sJuyQvxpAiOU20bNIG8E7pw
|
||||||
|
PsnYIIkhTgSAH5klJa9d+Nl+7y8lw9EzTI4dhdTUc9PWmo4G3wjwTHLE1GluDc2zHvJV9Q
|
||||||
|
vLo/nflWn2eqn26JdZmjGI23sVRPNh1or5S7BLldPIMXnz8l6jiGztpGTc2tapHCeXsHm9
|
||||||
|
37guDFQHHhZ0ZoUdYy2pB8rC1WoMo52t9N3s957+zwff4MqSeEdgrsMz5jAkmBNfQlAAAA
|
||||||
|
wQDIf2w9+9NGy9i1kBUCL40XwBhJwT9vGYxoyhB87UdV+exuYU8Mu5mDs1un9YIJRT3lTe
|
||||||
|
j0l+2Ko57uym/FJ2zvM+lErZSAzUNH4QGcMCaDNtZ61+KHRCkkY+CirXEjDs9+pzsRdVBl
|
||||||
|
r/LZASdPxsmOMHKAgC91jKSaRmcIe52YTsMJRtKpCOx6A11YnJhJqzMbpWPV8S3CZkLk4D
|
||||||
|
MGd8DTd59fcgGYzbs/svgPIHzRSXXYdUHFATX6NgCvaJGyM+8AAADBAM6AnnZ4PROkRrla
|
||||||
|
1fiOgWDnCHSR/wKe1c4e1tlpv9wPchddtq3UDNoWaiqp49+CUnrimkrEPgGig2QArv3Bop
|
||||||
|
BwAQEAYr1wLXGr31m6wLschHmGzOIhA09V1PmWzB3resAsCAw2BA1a5HYDS+IuD0JS6TMu
|
||||||
|
DZ3J8mLCHO8C95QI0AQzRoJ9903i9Y3v3uj4zYebZXHK1oJot2o4natrjKJlz3tPsCgnQN
|
||||||
|
tROruIOlzsbzSQrft6fY1u58isE1nuxwAAAMEAwdc/BhtXnVghY9Ftj6xqFH2uhP9ul3rw
|
||||||
|
FVeEItMvciVihrex8KYMP3+GHcAkInaPLVqW0b6K5IxxsO1bgxHukgDBWWIacS5RguDM7B
|
||||||
|
FO1sghEPL1QJBGONJ0rJBPDgiXkVdSRCo8ay2TASBVD9dHTI2MV0q+a0W1o74LQvYj/7cx
|
||||||
|
3MXkbeaywz5MqS5F6h5RXl0BczafJa4EsUeibZP1FaadgDDQT7VupR9Ol8gd1iF5607n5X
|
||||||
|
avX4guaGi5dcBFAAAAI3dheW5lQGJveC53YXluZWhheWVzZGV2ZWxvcG1lbnQuY29tAQID
|
||||||
|
BAUGBw==
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
17
.env.example
Normal file
17
.env.example
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# nvimide — declarative config. Copy to .env (gitignored) and fill in.
|
||||||
|
# cp .env.example .env
|
||||||
|
|
||||||
|
# ── Tailnet (your Tailscale OAuth client + tailnet) ──
|
||||||
|
TAILSCALE_VERSION=v1.98.4
|
||||||
|
TS_OAUTH_CLIENT_SECRET= # OAuth client secret, authorized for tag:nvimide
|
||||||
|
TS_TAILNET= # e.g. your-tailnet.ts.net — used by the roaming engine for MagicDNS
|
||||||
|
NVIMIDE_MAGIC_NAME=nvimide
|
||||||
|
|
||||||
|
# ── Declarative auth (NixOS-style — nothing baked in the image) ──
|
||||||
|
NVIMIDE_USER=dev
|
||||||
|
NVIMIDE_PASSWORD= # sets the user's password → enables sudo + ssh fallback
|
||||||
|
NVIMIDE_SSH_PUBKEY= # your PUBLIC key (e.g. ~/.ssh/id_ed25519.pub) — the real login
|
||||||
|
|
||||||
|
# ── Build (match host uid so the home volume perms line up) ──
|
||||||
|
UID=1000
|
||||||
|
GID=1000
|
||||||
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# ── arch-dev repo .gitignore ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Docker build artifacts
|
||||||
|
*.tar.gz
|
||||||
|
*.tar
|
||||||
|
|
||||||
|
# Environment / secrets
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# Workspace contents (bind mount — user's project files, not ours)
|
||||||
|
workspace/*
|
||||||
|
!workspace/.gitkeep
|
||||||
|
|
||||||
|
# nvimide: never ship auth material
|
||||||
|
authorized_keys
|
||||||
|
*.key
|
||||||
|
*_rsa
|
||||||
|
*_ed25519
|
||||||
|
id_*
|
||||||
|
ssh_host_*
|
||||||
|
|
||||||
|
# local-only compose override (external/migrated home volume, etc.)
|
||||||
|
docker-compose.override.yml
|
||||||
131
Dockerfile
Normal file
131
Dockerfile
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
FROM archlinux:latest
|
||||||
|
|
||||||
|
# ── Build args: UID/GID matching for clean /workspace permissions ─────────────
|
||||||
|
# Override at build: UID=$(id -u) GID=$(id -g) docker compose build
|
||||||
|
# Or set in .env file
|
||||||
|
ARG USER_UID=1000
|
||||||
|
ARG USER_GID=1000
|
||||||
|
|
||||||
|
# ── Rolling release: full system update first, always ─────────────────────────
|
||||||
|
RUN pacman -Syu --noconfirm
|
||||||
|
|
||||||
|
# ── Crown Jewel #1: pacman ────────────────────────────────────────────────────
|
||||||
|
RUN pacman -S --noconfirm --needed \
|
||||||
|
base-devel git curl wget unzip zip \
|
||||||
|
zsh tmux screen mosh \
|
||||||
|
zsh-syntax-highlighting zsh-autosuggestions zsh-history-substring-search \
|
||||||
|
zsh-completions \
|
||||||
|
neovim \
|
||||||
|
starship \
|
||||||
|
python python-pip python-pynvim \
|
||||||
|
perl \
|
||||||
|
pyright \
|
||||||
|
bash-language-server \
|
||||||
|
python-black ruff shellcheck shfmt \
|
||||||
|
python-pylint \
|
||||||
|
ripgrep fd bat eza fzf zoxide \
|
||||||
|
git-delta lazygit \
|
||||||
|
btop \
|
||||||
|
ttf-nerd-fonts-symbols ttf-jetbrains-mono-nerd \
|
||||||
|
man-db man-pages \
|
||||||
|
jq tree wget \
|
||||||
|
rsync \
|
||||||
|
imagemagick chafa jp2a \
|
||||||
|
go \
|
||||||
|
github-cli \
|
||||||
|
libnewt \
|
||||||
|
openssh sshfs \
|
||||||
|
&& pacman -Scc --noconfirm
|
||||||
|
|
||||||
|
# ── Crown Jewel #2: AUR ───────────────────────────────────────────────────────
|
||||||
|
RUN useradd -m -s /bin/zsh -u 9001 -G wheel aurbuild && \
|
||||||
|
echo 'aurbuild ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers.d/aurbuild
|
||||||
|
|
||||||
|
RUN cd /tmp && \
|
||||||
|
git clone --depth=1 https://aur.archlinux.org/yay-bin.git && \
|
||||||
|
chown -R aurbuild:aurbuild yay-bin && \
|
||||||
|
cd yay-bin && \
|
||||||
|
sudo -u aurbuild makepkg -si --noconfirm && \
|
||||||
|
cd / && rm -rf /tmp/yay-bin
|
||||||
|
|
||||||
|
RUN sudo -u aurbuild yay -S --noconfirm --needed \
|
||||||
|
eza \
|
||||||
|
wl-clipboard \
|
||||||
|
trash-cli \
|
||||||
|
tailscale \
|
||||||
|
&& sudo -u aurbuild yay -Scc --noconfirm
|
||||||
|
|
||||||
|
# ── FUSE config: allow_other for sshfs mounts ─────────────────────────────────
|
||||||
|
RUN sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf 2>/dev/null || \
|
||||||
|
echo 'user_allow_other' >> /etc/fuse.conf
|
||||||
|
|
||||||
|
# ── Dev user with host-matching UID/GID ───────────────────────────────────────
|
||||||
|
# UID/GID match host so /workspace bind mount has clean permissions both sides.
|
||||||
|
# aurbuild is parked at UID 9001 so there's no collision with host UID.
|
||||||
|
RUN set -e; \
|
||||||
|
if getent group ${USER_GID} >/dev/null; then \
|
||||||
|
groupmod -n dev "$(getent group ${USER_GID} | cut -d: -f1)"; \
|
||||||
|
else \
|
||||||
|
groupadd -g ${USER_GID} dev; \
|
||||||
|
fi; \
|
||||||
|
useradd -m -s /bin/zsh -u ${USER_UID} -g ${USER_GID} -G wheel dev; \
|
||||||
|
echo 'dev ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers.d/dev
|
||||||
|
|
||||||
|
# ── Skeleton: bake dotfiles into /etc/skel-arch-dev/ ──────────────────────────
|
||||||
|
COPY --chown=dev:dev dotfiles/ /etc/skel-arch-dev/
|
||||||
|
|
||||||
|
# ── Roaming toolkit: add_new_machine.sh onto PATH ─────────────────────────────
|
||||||
|
COPY scripts/add_new_machine.sh /usr/local/bin/add_new_machine.sh
|
||||||
|
RUN chmod +x /usr/local/bin/add_new_machine.sh
|
||||||
|
|
||||||
|
# ── Initial seed of /home/dev so plugin bake works at build time ──────────────
|
||||||
|
RUN cp -an /etc/skel-arch-dev/. /home/dev/ && \
|
||||||
|
chown -R dev:dev /home/dev
|
||||||
|
|
||||||
|
# ── nvm + LTS Node (as dev user) ──────────────────────────────────────────────
|
||||||
|
# nvm install script writes to ~/.zshrc — we sandbox it then merge cleanly
|
||||||
|
RUN sudo -u dev bash -c '\
|
||||||
|
export PROFILE=/dev/null && \
|
||||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash && \
|
||||||
|
export NVM_DIR="/home/dev/.nvm" && \
|
||||||
|
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && \
|
||||||
|
nvm install --lts && \
|
||||||
|
nvm alias default node \
|
||||||
|
'
|
||||||
|
|
||||||
|
# Persist nvm install into the skel template so volume seeding includes it
|
||||||
|
RUN cp -an /home/dev/.nvm /etc/skel-arch-dev/ && \
|
||||||
|
chown -R dev:dev /etc/skel-arch-dev/.nvm
|
||||||
|
|
||||||
|
# ── Python tools ──────────────────────────────────────────────────────────────
|
||||||
|
RUN pip install --break-system-packages pynvim httpx requests
|
||||||
|
|
||||||
|
# ── Bake neovim plugins ───────────────────────────────────────────────────────
|
||||||
|
RUN sudo -u dev HOME=/home/dev XDG_DATA_HOME=/home/dev/.local/share \
|
||||||
|
nvim --headless +"Lazy! sync" +qa 2>/dev/null; exit 0
|
||||||
|
|
||||||
|
RUN sudo -u dev HOME=/home/dev XDG_DATA_HOME=/home/dev/.local/share \
|
||||||
|
nvim --headless \
|
||||||
|
+"TSUpdateSync python bash lua json yaml toml markdown vim vimdoc regex" \
|
||||||
|
+qa 2>/dev/null; exit 0
|
||||||
|
|
||||||
|
# Copy fully-baked /home/dev back into the skel template
|
||||||
|
RUN cp -an /home/dev/.local /etc/skel-arch-dev/ && \
|
||||||
|
cp -an /home/dev/.cache /etc/skel-arch-dev/ 2>/dev/null || true && \
|
||||||
|
chown -R dev:dev /etc/skel-arch-dev
|
||||||
|
|
||||||
|
# ── Cleanup AUR build user ────────────────────────────────────────────────────
|
||||||
|
RUN userdel -r aurbuild && rm -f /etc/sudoers.d/aurbuild
|
||||||
|
|
||||||
|
# ── Entrypoint script ─────────────────────────────────────────────────────────
|
||||||
|
COPY entrypoint.sh /usr/local/bin/arch-dev-entrypoint
|
||||||
|
RUN chmod +x /usr/local/bin/arch-dev-entrypoint
|
||||||
|
|
||||||
|
# ── Final permissions ─────────────────────────────────────────────────────────
|
||||||
|
RUN chown -R dev:dev /home/dev
|
||||||
|
|
||||||
|
USER dev
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/arch-dev-entrypoint"]
|
||||||
|
CMD ["/bin/zsh"]
|
||||||
98
README.md
Normal file
98
README.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# Homescale
|
||||||
|
### Your fleet, one word away · a Tailscale-native roaming home
|
||||||
|
|
||||||
|
> *"I'd far rather be happy than right any day."* And you're far more likely to
|
||||||
|
> be both when your whole fleet answers from a single container you `ssh` into.
|
||||||
|
|
||||||
|
**Homescale** turns a riced Arch dev container into a **roaming home for your
|
||||||
|
fleet**: one stable Tailscale node you SSH into, from which every host on your
|
||||||
|
tailnet is a single word away. It holds the keyring; you carry nothing but a
|
||||||
|
terminal. Run a coding agent inside it and it becomes a fleet control plane with
|
||||||
|
hands.
|
||||||
|
|
||||||
|
The IDE is still under the hood — colorscheme, LSP, a git-snapshotted home. It's
|
||||||
|
just no longer the point. The point is **home**.
|
||||||
|
|
||||||
|
*(Tailscale today → Headscale later: the "home" you scale.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh <user>@<node>.<your-tailnet>.ts.net
|
||||||
|
```
|
||||||
|
|
||||||
|
You land in your full stateful home. Each registered host is then just a verb:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
web uptime # runs on the host named "web", over the tailnet
|
||||||
|
db sudo systemctl status pg # remote sudo prompts — the boundary holds
|
||||||
|
edge journalctl -u caddy # any registered host
|
||||||
|
mount_host db # sshfs its filesystem → /workspace/db
|
||||||
|
roaming-status # what's registered
|
||||||
|
```
|
||||||
|
|
||||||
|
Each host becomes a shell function. Pipes/redirects run **locally**, exactly like
|
||||||
|
plain ssh — quote the whole pipeline to run it remote.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Onboard a host
|
||||||
|
|
||||||
|
```bash
|
||||||
|
add_new_machine.sh NAME NAME.<your-tailnet>.ts.net <user> /
|
||||||
|
roaming-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Three things that bite, up front:
|
||||||
|
- **Key-only hosts** (`PasswordAuthentication no`) can't be bootstrapped by
|
||||||
|
`ssh-copy-id`. Pre-install Homescale's `~/.ssh/id_ed25519.pub` via a path the
|
||||||
|
host already trusts — keyless `tailscale ssh` is ideal.
|
||||||
|
- **Custom ssh port?** Add a `Host … Port <PORT>` block to `~/.ssh/config` (the
|
||||||
|
roaming engine uses plain ssh).
|
||||||
|
- **Aliases don't travel** — `NAME <cmd>` is non-interactive ssh. Use real
|
||||||
|
binaries or `~/bin` scripts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How it's built
|
||||||
|
|
||||||
|
| Piece | What |
|
||||||
|
|---|---|
|
||||||
|
| `ts-nvimide` | Tailscale sidecar — **stable** node (no `?ephemeral`), `tag:nvimide` |
|
||||||
|
| `nvimide` | the dev image sharing the sidecar's netns, running **sshd as init** |
|
||||||
|
| home volume | stateful home (snapshots, tools, `.ssh`) |
|
||||||
|
| auth | declarative via `.env` (`NVIMIDE_USER` / `NVIMIDE_PASSWORD` / `NVIMIDE_SSH_PUBKEY`) |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env # fill TS_OAUTH_CLIENT_SECRET, NVIMIDE_SSH_PUBKEY, TS_TAILNET …
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
> The committed compose uses a **managed** home volume so it works out of the box.
|
||||||
|
> To pin it to an existing/migrated volume, override `nvimide-home` in a local
|
||||||
|
> (gitignored) `docker-compose.override.yml`.
|
||||||
|
|
||||||
|
### Tailnet ACL (lives in your Tailscale console, not this repo)
|
||||||
|
|
||||||
|
A **grant** for plain-TCP reach + **two `ssh` rules** — a separate section from
|
||||||
|
grants, needed in **both directions**:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{ "src": ["tag:nvimide"], "dst": ["*"], "ip": ["*"] } // reach (plain ssh)
|
||||||
|
{ "src": ["tag:nvimide"], "dst": ["*"], "users": ["<user>"], "action": "accept" } // tailscale ssh OUT
|
||||||
|
{ "src": ["*"], "dst": ["tag:nvimide"], "users": ["<user>"], "action": "accept" } // tailscale ssh IN — "Go Home!"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deeper
|
||||||
|
|
||||||
|
- **`ROAMING.md`** — the roaming engine: registry format, host-functions, sshfs
|
||||||
|
mounts, the gotchas (host-key persistence, the `CAP_SYS_CHROOT` sshd needs, the
|
||||||
|
sidecar healthcheck that must not gate on the app's `:22`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*MIT — fork it, name your node something silly, make it home.*
|
||||||
132
ROAMING.md
Normal file
132
ROAMING.md
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# Roaming ~ — arch-dev v3
|
||||||
|
|
||||||
|
Turn neovim-ide into your **roaming home directory**. SSH in from any device,
|
||||||
|
and every server on your tailnet feels like home: one keyring, one shell
|
||||||
|
history, one set of dotfiles, reachable from anywhere.
|
||||||
|
|
||||||
|
This is what makes neovim-ide more than another riced nvim install — it's a
|
||||||
|
control plane for your whole fleet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Model
|
||||||
|
|
||||||
|
```
|
||||||
|
You (phone / tablet / chromebook)
|
||||||
|
↓ ssh
|
||||||
|
nvimide.tailnet.ts ← your roaming home (this container)
|
||||||
|
↓ ssh / sshfs over tailnet
|
||||||
|
┌──────────┬──────────┬──────────┐
|
||||||
|
mail proxy dev (any host)
|
||||||
|
```
|
||||||
|
|
||||||
|
You live in nvimide. Files live on remote hosts (mounted via sshfs when you
|
||||||
|
want them). Commands run on remote hosts via short per-host functions.
|
||||||
|
|
||||||
|
Keys never leave nvimide — agent auth handles remote login, remote `sudo`
|
||||||
|
still prompts (the security boundary stays intact).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `~/.config/roaming/hosts` | Registry — one line per host (source of truth) |
|
||||||
|
| `~/.config/roaming/roaming.zsh` | Engine — generates host functions, mount helpers |
|
||||||
|
| `add_new_machine.sh` | Onboard a new host (key + register + verify) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Run commands on a host
|
||||||
|
|
||||||
|
Each registry entry becomes a command. Dumb by design — pipes and redirects
|
||||||
|
run **locally**, exactly like plain ssh (predictable, no surprises).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mail # interactive shell on mail
|
||||||
|
mail tail -f /var/log/mail.log # TTY allocated — -f works
|
||||||
|
mail sudo systemctl restart postfix # remote sudo prompts for password
|
||||||
|
proxy sudo systemctl reload caddy
|
||||||
|
dev docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mount a host's filesystem
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mount_host mail # sshfs the registered path → /workspace/mail
|
||||||
|
ls /workspace/mail/etc/postfix/
|
||||||
|
unmount_host mail # clean unmount (handles stale mounts)
|
||||||
|
```
|
||||||
|
|
||||||
|
Mounts use `reconnect,ServerAliveInterval=15,ServerAliveCountMax=3` so a
|
||||||
|
network blip errors out in ~45s instead of freezing your shell forever.
|
||||||
|
|
||||||
|
### Status & reload
|
||||||
|
|
||||||
|
```bash
|
||||||
|
roaming-status # list hosts + which are mounted
|
||||||
|
roaming-reload # re-read registry after editing hosts by hand
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a new machine
|
||||||
|
|
||||||
|
```bash
|
||||||
|
add_new_machine.sh mail mail.tailnet.ts root /
|
||||||
|
# 1. ssh-copy-id (one password prompt)
|
||||||
|
# 2. appends to ~/.config/roaming/hosts
|
||||||
|
# 3. verifies key-based login
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, `mail` works in the next shell (or after `roaming-reload`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Registry Format
|
||||||
|
|
||||||
|
```
|
||||||
|
# name fqdn user mount
|
||||||
|
mail mail.tailnet.ts root /
|
||||||
|
proxy proxy.tailnet.ts admin /etc/caddy
|
||||||
|
dev dev-server.tailnet.ts youruser
|
||||||
|
```
|
||||||
|
|
||||||
|
- `user` optional — defaults to `$ROAMING_DEFAULT_USER` or current user
|
||||||
|
- `mount` optional — omit if you only run commands, never mount
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Onboarding a Fresh Client
|
||||||
|
|
||||||
|
The whole point: a new device needs almost nothing.
|
||||||
|
|
||||||
|
1. Install your SSH key to reach nvimide.tailnet.ts
|
||||||
|
2. SSH in — you're home. Dotfiles, history (atuin), keys, the works.
|
||||||
|
|
||||||
|
No per-device key sprawl. nvimide holds the fleet keyring; clients only need
|
||||||
|
to reach nvimide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gotchas (learned, baked in)
|
||||||
|
|
||||||
|
- **Pipes are local.** `mail tail -f log | grep err` greps locally. Want it
|
||||||
|
remote? Quote the whole pipeline: `mail 'tail -f log | grep err'`.
|
||||||
|
- **Stale mount frozen?** `fusermount3 -uz /workspace/<name>` force-clears it.
|
||||||
|
- **`tail -f` / `htop` / sudo need a TTY** — handled (`ssh -t` always).
|
||||||
|
- **Agent forwarding (`-A`) is intentionally OFF** — a compromised remote
|
||||||
|
could abuse a forwarded agent. Add per-host later only if you need to hop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roadmap (future hardening)
|
||||||
|
|
||||||
|
- **SSH CA + OIDC (Authelia)** — replace `authorized_keys` with short-lived
|
||||||
|
certs issued after Authelia auth. Collapses key management fleet-wide.
|
||||||
|
Deferred: keys stay as the stable fallback until the cert flow is proven.
|
||||||
|
- **autofs** — system-level lazy mount/unmount (60s idle auto-unmount) as an
|
||||||
|
alternative to manual `mount_host`.
|
||||||
|
- **atuin** — single shell-history brain in nvimide, DB in the postgres
|
||||||
|
cluster; retire atuin-server.
|
||||||
114
docker-compose.yml
Normal file
114
docker-compose.yml
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# nvimide — roaming dev IDE (arch-dev v3). Its OWN thing; follows the
|
||||||
|
# Tailscale sidecar pattern and rides your tailnet, but is
|
||||||
|
# not part of that stack.
|
||||||
|
#
|
||||||
|
# You SSH into this node from any device; it holds the fleet keyring + roaming
|
||||||
|
# engine and reaches every other host. It is a STABLE tailnet node (not
|
||||||
|
# ephemeral) — always reachable, clean MagicDNS name (no -N tombstones) — so
|
||||||
|
# TS_AUTHKEY omits ?ephemeral=true.
|
||||||
|
#
|
||||||
|
# Declarative auth (NixOS-style): user, password, and ssh pubkey all come from
|
||||||
|
# .env — nothing baked into the image, nothing mounted. Copy .env.example to
|
||||||
|
# .env and fill it in.
|
||||||
|
#
|
||||||
|
# Bring up:
|
||||||
|
# 1. ACL: add `tag:nvimide` + authorize the OAuth client to mint it.
|
||||||
|
# 2. cp .env.example .env && fill NVIMIDE_SSH_PUBKEY / NVIMIDE_PASSWORD / TS_*
|
||||||
|
# 3. docker compose up -d --build
|
||||||
|
# 4. ssh ${NVIMIDE_USER}@nvimide
|
||||||
|
|
||||||
|
name: nvimide
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
ts-nvimide:
|
||||||
|
image: tailscale/tailscale:${TAILSCALE_VERSION:-v1.98.4}
|
||||||
|
hostname: ${NVIMIDE_MAGIC_NAME:-nvimide}
|
||||||
|
environment:
|
||||||
|
TS_AUTHKEY: ${TS_OAUTH_CLIENT_SECRET} # NO ?ephemeral=true → stable node
|
||||||
|
TS_EXTRA_ARGS: --advertise-tags=tag:nvimide
|
||||||
|
TS_HOSTNAME: ${NVIMIDE_MAGIC_NAME:-nvimide}
|
||||||
|
TS_ACCEPT_DNS: "true"
|
||||||
|
TS_AUTH_ONCE: "true"
|
||||||
|
TS_USERSPACE: "false"
|
||||||
|
TS_ENABLE_HEALTH_CHECK: "true"
|
||||||
|
TS_LOCAL_ADDR_PORT: "127.0.0.1:9009"
|
||||||
|
dns: [1.1.1.1, 1.0.0.1]
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun:/dev/net/tun
|
||||||
|
cap_add: [NET_ADMIN, NET_RAW]
|
||||||
|
healthcheck: # green only once the IDE's sshd is accepting on :22 in this netns
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9009/healthz"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 15s
|
||||||
|
retries: 6
|
||||||
|
start_period: 30s
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
nvimide:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
USER_UID: ${UID:-1000}
|
||||||
|
USER_GID: ${GID:-1000}
|
||||||
|
image: nvimide:latest
|
||||||
|
network_mode: "service:ts-nvimide" # share the sidecar's tailnet netns
|
||||||
|
user: root # sshd needs root (privsep + bind :22); drops to the user on login
|
||||||
|
environment:
|
||||||
|
- TERM=xterm-256color
|
||||||
|
- MOBILE=0
|
||||||
|
- NVIMIDE_USER=${NVIMIDE_USER:-dev}
|
||||||
|
- NVIMIDE_PASSWORD=${NVIMIDE_PASSWORD:-}
|
||||||
|
- NVIMIDE_SSH_PUBKEY=${NVIMIDE_SSH_PUBKEY:-}
|
||||||
|
# entrypoint seeds /home, then exec's this: provision auth declaratively from
|
||||||
|
# env (password enables sudo + fallback; pubkey is the real login), host
|
||||||
|
# keys, sshd in foreground.
|
||||||
|
command:
|
||||||
|
- /bin/bash
|
||||||
|
- -lc
|
||||||
|
- |
|
||||||
|
u="${NVIMIDE_USER:-dev}"
|
||||||
|
[ -n "$$NVIMIDE_PASSWORD" ] && echo "$$u:$$NVIMIDE_PASSWORD" | chpasswd
|
||||||
|
if [ -n "$$NVIMIDE_SSH_PUBKEY" ]; then
|
||||||
|
install -d -o "$$u" -g "$$u" -m 700 "/home/$$u/.ssh"
|
||||||
|
printf '%s\n' "$$NVIMIDE_SSH_PUBKEY" > "/home/$$u/.ssh/authorized_keys"
|
||||||
|
chown "$$u:$$u" "/home/$$u/.ssh/authorized_keys"; chmod 600 "/home/$$u/.ssh/authorized_keys"
|
||||||
|
fi
|
||||||
|
# Persist host keys on the home volume so client known_hosts stays valid
|
||||||
|
hk="/home/$$u/.ssh/host_keys"
|
||||||
|
install -d -o "$$u" -g "$$u" -m 700 "$$hk"
|
||||||
|
if [ ! -e "$$hk/ssh_host_ed25519_key" ]; then
|
||||||
|
ssh-keygen -q -t ed25519 -f "$$hk/ssh_host_ed25519_key" -N ""
|
||||||
|
ssh-keygen -q -t rsa -b 4096 -f "$$hk/ssh_host_rsa_key" -N ""
|
||||||
|
fi
|
||||||
|
exec /usr/bin/sshd -D -e \
|
||||||
|
-h "$$hk/ssh_host_ed25519_key" \
|
||||||
|
-h "$$hk/ssh_host_rsa_key"
|
||||||
|
volumes:
|
||||||
|
- nvimide-home:/home/dev # stateful home (roaming engine, history, dotfiles). Phase 2: rclone-sync to object storage.
|
||||||
|
- nvimide-workspace:/workspace # sshfs roaming mount points land here
|
||||||
|
cap_drop: [ALL]
|
||||||
|
cap_add:
|
||||||
|
- SETUID # sudo / sshd privsep
|
||||||
|
- SETGID
|
||||||
|
- AUDIT_WRITE
|
||||||
|
- CHOWN
|
||||||
|
- DAC_OVERRIDE
|
||||||
|
- FOWNER
|
||||||
|
- SYS_ADMIN # sshfs / FUSE roaming mounts
|
||||||
|
- NET_BIND_SERVICE # sshd binds :22
|
||||||
|
- SYS_CHROOT # sshd privilege-separation chroot (/usr/share/empty.sshd)
|
||||||
|
devices:
|
||||||
|
- /dev/fuse:/dev/fuse
|
||||||
|
security_opt:
|
||||||
|
- apparmor:unconfined
|
||||||
|
depends_on:
|
||||||
|
ts-nvimide:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
nvimide-home:
|
||||||
|
external: true
|
||||||
|
name: nvimide-ts_arch-dev-home
|
||||||
|
nvimide-workspace:
|
||||||
89
dotfiles/.aliases
Normal file
89
dotfiles/.aliases
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
# ║ arch-dev :: aliases ║
|
||||||
|
# ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
# ── Navigation ────────────────────────────────────────────────────────────────
|
||||||
|
alias ..='cd ..'
|
||||||
|
alias ...='cd ../..'
|
||||||
|
alias ....='cd ../../..'
|
||||||
|
alias ~='cd ~'
|
||||||
|
alias -- -='cd -'
|
||||||
|
|
||||||
|
# ── ls / eza ──────────────────────────────────────────────────────────────────
|
||||||
|
alias ls='eza --icons --group-directories-first --color=always'
|
||||||
|
alias ll='eza -la --icons --git --group-directories-first --color=always'
|
||||||
|
alias lt='eza --tree --icons --level=2 --color=always'
|
||||||
|
alias ltt='eza --tree --icons --level=3 --color=always'
|
||||||
|
|
||||||
|
# ── bat (better cat) ──────────────────────────────────────────────────────────
|
||||||
|
alias cat='bat --style=plain --paging=never --color=never'
|
||||||
|
alias catn='bat --style=numbers --paging=never --color=never'
|
||||||
|
alias catp='bat --style=full --paging=never'
|
||||||
|
|
||||||
|
# ── Editor ────────────────────────────────────────────────────────────────────
|
||||||
|
alias v='nvim'
|
||||||
|
alias vi='nvim'
|
||||||
|
alias vim='nvim'
|
||||||
|
alias vdiff='nvim -d'
|
||||||
|
|
||||||
|
# ── Git (tearless mobile ref bindings) ────────────────────────────────────────
|
||||||
|
alias g='git'
|
||||||
|
alias gs='git status'
|
||||||
|
alias gl='git log --oneline --graph --decorate --color'
|
||||||
|
alias glo='git log --oneline -20'
|
||||||
|
alias gd='git diff'
|
||||||
|
alias gds='git diff --staged'
|
||||||
|
alias ga='git add'
|
||||||
|
alias gaa='git add -A'
|
||||||
|
alias gc='git commit'
|
||||||
|
alias gcm='git commit -m'
|
||||||
|
alias gp='git push'
|
||||||
|
alias gpl='git pull'
|
||||||
|
alias gco='git checkout'
|
||||||
|
alias gcb='git checkout -b'
|
||||||
|
alias gst='git stash'
|
||||||
|
alias gstp='git stash pop'
|
||||||
|
alias lg='lazygit'
|
||||||
|
|
||||||
|
# ── Shell utils ───────────────────────────────────────────────────────────────
|
||||||
|
alias grep='grep --color=auto'
|
||||||
|
alias rg='ripgrep'
|
||||||
|
alias src='source ~/.zshrc && echo "↻ zshrc reloaded"'
|
||||||
|
alias cls='clear'
|
||||||
|
alias path='echo $PATH | tr ":" "\n"'
|
||||||
|
|
||||||
|
# ── System (btop > top) ───────────────────────────────────────────────────────
|
||||||
|
alias top='btop'
|
||||||
|
alias df='df -h'
|
||||||
|
alias du='du -h'
|
||||||
|
alias free='free -h'
|
||||||
|
|
||||||
|
# ── Safety nets ───────────────────────────────────────────────────────────────
|
||||||
|
# trash-cli installed from AUR — rm is still available for scripts
|
||||||
|
alias rm='echo "Use trash or /bin/rm"; false'
|
||||||
|
alias tp='trash-put'
|
||||||
|
alias tl='trash-list'
|
||||||
|
|
||||||
|
# ── Python ────────────────────────────────────────────────────────────────────
|
||||||
|
alias py='python'
|
||||||
|
alias py3='python3'
|
||||||
|
alias pip='pip3'
|
||||||
|
alias venv='python -m venv'
|
||||||
|
alias va='source .venv/bin/activate'
|
||||||
|
alias vd='deactivate'
|
||||||
|
|
||||||
|
# ── Perl ──────────────────────────────────────────────────────────────────────
|
||||||
|
alias pl='perl'
|
||||||
|
alias plcheck='perl -wc' # syntax check a file
|
||||||
|
alias plcritic='perlcritic'
|
||||||
|
|
||||||
|
# ── arch-dev meta ─────────────────────────────────────────────────────────────
|
||||||
|
alias pacman-list='pacman -Qqe' # list explicitly installed
|
||||||
|
alias pacman-orphans='pacman -Qtdq' # list orphaned packages
|
||||||
|
alias aur-update='yay -Syu --aur'
|
||||||
|
|
||||||
|
# ── Snapshot shortcuts ────────────────────────────────────────────────────────
|
||||||
|
alias snap='snapshot'
|
||||||
|
alias snaps='snapshots'
|
||||||
|
alias snapd='diff-state'
|
||||||
|
alias snapr='rollback'
|
||||||
8
dotfiles/.config/nvim/init.lua
Normal file
8
dotfiles/.config/nvim/init.lua
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-- ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
-- ║ arch-dev :: neovim ║
|
||||||
|
-- ║ Kanagawa Wave · lazy.nvim · LSP/lint/format · riced IDE ║
|
||||||
|
-- ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
require("options")
|
||||||
|
require("keymaps")
|
||||||
|
require("plugins")
|
||||||
103
dotfiles/.config/nvim/lua/keymaps.lua
Normal file
103
dotfiles/.config/nvim/lua/keymaps.lua
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
-- ── keymaps.lua ───────────────────────────────────────────────────────────────
|
||||||
|
local map = vim.keymap.set
|
||||||
|
local opts = { silent = true }
|
||||||
|
|
||||||
|
-- ── Escape (tearless mobile ref: jj) ─────────────────────────────────────────
|
||||||
|
map("i", "jj", "<Esc>", { desc = "Escape insert mode" })
|
||||||
|
map("i", "jk", "<Esc>", { desc = "Escape insert mode" })
|
||||||
|
|
||||||
|
-- ── File ops (tearless mobile ref) ───────────────────────────────────────────
|
||||||
|
map("n", "<leader>w", "<cmd>w<cr>", { desc = "Save" })
|
||||||
|
map("n", "<leader>q", "<cmd>q<cr>", { desc = "Quit" })
|
||||||
|
map("n", "<leader>x", "<cmd>wq<cr>", { desc = "Save + quit" })
|
||||||
|
map("n", "<leader>Q", "<cmd>qa!<cr>",{ desc = "Force quit all" })
|
||||||
|
|
||||||
|
-- ── Clear search highlight ────────────────────────────────────────────────────
|
||||||
|
map("n", "<Esc>", "<cmd>noh<cr>", opts)
|
||||||
|
|
||||||
|
-- ── Better movement ───────────────────────────────────────────────────────────
|
||||||
|
map("n", "j", "gj", opts) -- visual line movement
|
||||||
|
map("n", "k", "gk", opts)
|
||||||
|
map("n", "H", "^", opts) -- start/end of line
|
||||||
|
map("n", "L", "$", opts)
|
||||||
|
map("n", "<C-d>", "<C-d>zz", opts) -- keep cursor centred on scroll
|
||||||
|
map("n", "<C-u>", "<C-u>zz", opts)
|
||||||
|
map("n", "n", "nzzzv", opts) -- keep cursor centred on search
|
||||||
|
map("n", "N", "Nzzzv", opts)
|
||||||
|
|
||||||
|
-- ── Window navigation (matches tmux bindings) ─────────────────────────────────
|
||||||
|
map("n", "<C-h>", "<C-w>h", opts)
|
||||||
|
map("n", "<C-j>", "<C-w>j", opts)
|
||||||
|
map("n", "<C-k>", "<C-w>k", opts)
|
||||||
|
map("n", "<C-l>", "<C-w>l", opts)
|
||||||
|
|
||||||
|
-- ── Window resize ─────────────────────────────────────────────────────────────
|
||||||
|
map("n", "<C-Up>", "<cmd>resize +2<cr>", opts)
|
||||||
|
map("n", "<C-Down>", "<cmd>resize -2<cr>", opts)
|
||||||
|
map("n", "<C-Left>", "<cmd>vertical resize -2<cr>", opts)
|
||||||
|
map("n", "<C-Right>", "<cmd>vertical resize +2<cr>", opts)
|
||||||
|
|
||||||
|
-- ── Buffer navigation ─────────────────────────────────────────────────────────
|
||||||
|
map("n", "<S-h>", "<cmd>bprev<cr>", opts)
|
||||||
|
map("n", "<S-l>", "<cmd>bnext<cr>", opts)
|
||||||
|
map("n", "<leader>bd", "<cmd>bdelete<cr>", { desc = "Delete buffer" })
|
||||||
|
|
||||||
|
-- ── Indenting keeps selection (tearless mobile ref) ──────────────────────────
|
||||||
|
map("v", "<", "<gv", opts)
|
||||||
|
map("v", ">", ">gv", opts)
|
||||||
|
|
||||||
|
-- ── Move lines ────────────────────────────────────────────────────────────────
|
||||||
|
map("v", "J", ":m '>+1<cr>gv=gv", opts)
|
||||||
|
map("v", "K", ":m '<-2<cr>gv=gv", opts)
|
||||||
|
|
||||||
|
-- ── Paste without losing register ─────────────────────────────────────────────
|
||||||
|
map("v", "p", '"_dP', opts)
|
||||||
|
|
||||||
|
-- ── File explorer (tearless mobile ref: Ctrl+n) ───────────────────────────────
|
||||||
|
map("n", "<C-n>", "<cmd>Oil<cr>", { desc = "File explorer (oil)" })
|
||||||
|
map("n", "<leader>e", "<cmd>Oil<cr>", { desc = "File explorer (oil)" })
|
||||||
|
|
||||||
|
-- ── Telescope (tearless mobile ref) ──────────────────────────────────────────
|
||||||
|
map("n", "<leader>ff", "<cmd>Telescope find_files<cr>", { desc = "Find files" })
|
||||||
|
map("n", "<leader>fg", "<cmd>Telescope live_grep<cr>", { desc = "Live grep" })
|
||||||
|
map("n", "<leader>fb", "<cmd>Telescope buffers<cr>", { desc = "Buffers" })
|
||||||
|
map("n", "<leader>fh", "<cmd>Telescope help_tags<cr>", { desc = "Help tags" })
|
||||||
|
map("n", "<leader>fd", "<cmd>Telescope diagnostics<cr>", { desc = "Diagnostics" })
|
||||||
|
map("n", "<leader>fr", "<cmd>Telescope oldfiles<cr>", { desc = "Recent files" })
|
||||||
|
map("n", "<leader>fs", "<cmd>Telescope lsp_document_symbols<cr>",{ desc = "Symbols" })
|
||||||
|
|
||||||
|
-- ── LSP (set globally; lsp.lua on_attach adds buffer-local too) ───────────────
|
||||||
|
map("n", "gd", vim.lsp.buf.definition, { desc = "Go to definition" })
|
||||||
|
map("n", "gD", vim.lsp.buf.declaration, { desc = "Go to declaration" })
|
||||||
|
map("n", "gr", "<cmd>Telescope lsp_references<cr>",{ desc = "References" })
|
||||||
|
map("n", "gi", vim.lsp.buf.implementation, { desc = "Implementation" })
|
||||||
|
map("n", "K", vim.lsp.buf.hover, { desc = "Hover docs" })
|
||||||
|
map("n", "<C-k>", vim.lsp.buf.signature_help, { desc = "Signature help" })
|
||||||
|
map("n", "<leader>rn", vim.lsp.buf.rename, { desc = "Rename symbol" })
|
||||||
|
map("n", "<leader>ca", vim.lsp.buf.code_action, { desc = "Code action" })
|
||||||
|
map("n", "<leader>f", function()
|
||||||
|
vim.lsp.buf.format({ async = true })
|
||||||
|
end, { desc = "Format buffer" })
|
||||||
|
|
||||||
|
-- ── Diagnostics ───────────────────────────────────────────────────────────────
|
||||||
|
map("n", "[d", vim.diagnostic.goto_prev, { desc = "Prev diagnostic" })
|
||||||
|
map("n", "]d", vim.diagnostic.goto_next, { desc = "Next diagnostic" })
|
||||||
|
map("n", "<leader>dl", vim.diagnostic.open_float, { desc = "Line diagnostics" })
|
||||||
|
|
||||||
|
-- ── Leap (info only — sets its own mappings) ─────────────────────────────────
|
||||||
|
-- s → leap (n/x/o)
|
||||||
|
-- S → leap-from-window (n)
|
||||||
|
-- Surround defaults (v4): ys{motion}{char}, ds{char}, cs{old}{new}
|
||||||
|
|
||||||
|
-- ── LazyGit ───────────────────────────────────────────────────────────────────
|
||||||
|
map("n", "<leader>g", "<cmd>LazyGit<cr>", { desc = "LazyGit" })
|
||||||
|
|
||||||
|
-- ── Which-key group labels ────────────────────────────────────────────────────
|
||||||
|
local ok, wk = pcall(require, "which-key")
|
||||||
|
if ok then
|
||||||
|
wk.add({
|
||||||
|
{ "<leader>f", group = "find / telescope" },
|
||||||
|
{ "<leader>d", group = "diagnostics" },
|
||||||
|
{ "<leader>b", group = "buffer" },
|
||||||
|
})
|
||||||
|
end
|
||||||
91
dotfiles/.config/nvim/lua/options.lua
Normal file
91
dotfiles/.config/nvim/lua/options.lua
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
-- ── options.lua ───────────────────────────────────────────────────────────────
|
||||||
|
local o = vim.opt
|
||||||
|
local g = vim.g
|
||||||
|
|
||||||
|
-- Leader (set before plugins load)
|
||||||
|
g.mapleader = " "
|
||||||
|
g.maplocalleader = "\\"
|
||||||
|
|
||||||
|
-- ── Appearance ────────────────────────────────────────────────────────────────
|
||||||
|
-- termguicolors emits RGB escape codes. Termius on mobile renders these
|
||||||
|
-- as purple/magenta backgrounds. Disable when MOBILE=1.
|
||||||
|
o.termguicolors = (vim.env.MOBILE ~= "1")
|
||||||
|
o.number = true
|
||||||
|
o.relativenumber = true
|
||||||
|
o.cursorline = true
|
||||||
|
o.cursorcolumn = false
|
||||||
|
o.signcolumn = "yes:1"
|
||||||
|
o.scrolloff = 8
|
||||||
|
o.sidescrolloff = 8
|
||||||
|
o.wrap = false
|
||||||
|
o.showmode = false -- lualine handles this
|
||||||
|
o.showcmd = false
|
||||||
|
o.cmdheight = 1
|
||||||
|
o.pumheight = 12 -- max completion menu items
|
||||||
|
o.conceallevel = 0
|
||||||
|
|
||||||
|
-- ── Editing ───────────────────────────────────────────────────────────────────
|
||||||
|
o.expandtab = true
|
||||||
|
o.shiftwidth = 4
|
||||||
|
o.tabstop = 4
|
||||||
|
o.softtabstop = 4
|
||||||
|
o.smartindent = true
|
||||||
|
o.autoindent = true
|
||||||
|
o.breakindent = true
|
||||||
|
|
||||||
|
-- ── Search ────────────────────────────────────────────────────────────────────
|
||||||
|
o.ignorecase = true
|
||||||
|
o.smartcase = true
|
||||||
|
o.hlsearch = true
|
||||||
|
o.incsearch = true
|
||||||
|
|
||||||
|
-- ── Files ─────────────────────────────────────────────────────────────────────
|
||||||
|
o.undofile = true
|
||||||
|
o.undodir = vim.fn.stdpath("data") .. "/undo"
|
||||||
|
o.backup = false
|
||||||
|
o.swapfile = false
|
||||||
|
o.autoread = true
|
||||||
|
|
||||||
|
-- ── Behavior ──────────────────────────────────────────────────────────────────
|
||||||
|
o.clipboard = "unnamedplus"
|
||||||
|
o.updatetime = 250
|
||||||
|
o.timeoutlen = 300
|
||||||
|
o.splitright = true
|
||||||
|
o.splitbelow = true
|
||||||
|
o.completeopt = "menu,menuone,noselect"
|
||||||
|
o.mouse = "a"
|
||||||
|
o.virtualedit = "block"
|
||||||
|
|
||||||
|
-- ── Folding (treesitter) ──────────────────────────────────────────────────────
|
||||||
|
o.foldmethod = "expr"
|
||||||
|
o.foldexpr = "nvim_treesitter#foldexpr()"
|
||||||
|
o.foldenable = false -- open by default, fold manually
|
||||||
|
|
||||||
|
-- ── Netrw (disabled — using oil.nvim) ────────────────────────────────────────
|
||||||
|
g.loaded_netrw = 1
|
||||||
|
g.loaded_netrwPlugin = 1
|
||||||
|
|
||||||
|
-- ── Per-filetype tweaks ───────────────────────────────────────────────────────
|
||||||
|
vim.api.nvim_create_autocmd("FileType", {
|
||||||
|
pattern = { "lua", "json", "yaml", "toml" },
|
||||||
|
callback = function()
|
||||||
|
vim.opt_local.shiftwidth = 2
|
||||||
|
vim.opt_local.tabstop = 2
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Perl: use perltidy indenting convention
|
||||||
|
vim.api.nvim_create_autocmd("FileType", {
|
||||||
|
pattern = { "perl" },
|
||||||
|
callback = function()
|
||||||
|
vim.opt_local.shiftwidth = 4
|
||||||
|
vim.opt_local.tabstop = 4
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- ── Highlight on yank ─────────────────────────────────────────────────────────
|
||||||
|
vim.api.nvim_create_autocmd("TextYankPost", {
|
||||||
|
callback = function()
|
||||||
|
vim.highlight.on_yank({ higroup = "IncSearch", timeout = 150 })
|
||||||
|
end,
|
||||||
|
})
|
||||||
48
dotfiles/.config/nvim/lua/plugins.lua
Normal file
48
dotfiles/.config/nvim/lua/plugins.lua
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
-- ── plugins.lua — lazy.nvim bootstrap ────────────────────────────────────────
|
||||||
|
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
|
||||||
|
|
||||||
|
if not vim.loop.fs_stat(lazypath) then
|
||||||
|
vim.fn.system({
|
||||||
|
"git", "clone", "--filter=blob:none",
|
||||||
|
"https://github.com/folke/lazy.nvim.git",
|
||||||
|
"--branch=stable",
|
||||||
|
lazypath,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
vim.opt.rtp:prepend(lazypath)
|
||||||
|
|
||||||
|
require("lazy").setup({
|
||||||
|
{ import = "plugins.ui" },
|
||||||
|
{ import = "plugins.lsp" },
|
||||||
|
{ import = "plugins.treesitter" },
|
||||||
|
{ import = "plugins.tools" },
|
||||||
|
}, {
|
||||||
|
-- Baked into the image — don't check for updates at runtime
|
||||||
|
checker = { enabled = false },
|
||||||
|
change_detection = { enabled = false, notify = false },
|
||||||
|
-- Performance
|
||||||
|
performance = {
|
||||||
|
rtp = {
|
||||||
|
disabled_plugins = {
|
||||||
|
"gzip", "matchit", "matchparen",
|
||||||
|
"netrwPlugin", "tarPlugin", "tohtml",
|
||||||
|
"tutor", "zipPlugin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ui = {
|
||||||
|
border = "rounded",
|
||||||
|
-- Kanagawa Wave colors in lazy UI
|
||||||
|
icons = {
|
||||||
|
cmd = "⌘ ",
|
||||||
|
config = " ",
|
||||||
|
event = " ",
|
||||||
|
ft = " ",
|
||||||
|
init = " ",
|
||||||
|
plugin = " ",
|
||||||
|
source = " ",
|
||||||
|
start = "▶ ",
|
||||||
|
task = "✔ ",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
269
dotfiles/.config/nvim/lua/plugins/lsp.lua
Normal file
269
dotfiles/.config/nvim/lua/plugins/lsp.lua
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
-- ── plugins/lsp.lua ───────────────────────────────────────────────────────────
|
||||||
|
-- nvim 0.11+ native LSP API: vim.lsp.config / vim.lsp.enable
|
||||||
|
-- lspconfig still used as a config data source (server cmd/root detection)
|
||||||
|
-- but we drive it through vim.lsp.config, not lspconfig.setup()
|
||||||
|
-- No vim.lsp.with() — border config goes through vim.lsp.config globals
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
-- ── LSP ───────────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"neovim/nvim-lspconfig",
|
||||||
|
dependencies = {
|
||||||
|
"hrsh7th/nvim-cmp",
|
||||||
|
"hrsh7th/cmp-nvim-lsp",
|
||||||
|
"hrsh7th/cmp-buffer",
|
||||||
|
"hrsh7th/cmp-path",
|
||||||
|
"hrsh7th/cmp-cmdline",
|
||||||
|
"L3MON4D3/LuaSnip",
|
||||||
|
"saadparwaiz1/cmp_luasnip",
|
||||||
|
"rafamadriz/friendly-snippets",
|
||||||
|
"onsails/lspkind.nvim",
|
||||||
|
},
|
||||||
|
config = function()
|
||||||
|
|
||||||
|
-- ── Diagnostics ───────────────────────────────────────────────────
|
||||||
|
vim.diagnostic.config({
|
||||||
|
virtual_text = { prefix = "●", spacing = 4 },
|
||||||
|
signs = true,
|
||||||
|
underline = true,
|
||||||
|
update_in_insert = false,
|
||||||
|
severity_sort = true,
|
||||||
|
float = { border = "rounded", source = true },
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Diagnostic gutter icons
|
||||||
|
local signs = { Error = " ", Warn = " ", Hint = " ", Info = " " }
|
||||||
|
for type, icon in pairs(signs) do
|
||||||
|
local hl = "DiagnosticSign" .. type
|
||||||
|
vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Borders on hover/signature (0.11 way, no vim.lsp.with) ───────
|
||||||
|
vim.lsp.config("*", {
|
||||||
|
handlers = {
|
||||||
|
["textDocument/hover"] = function(err, result, ctx, config)
|
||||||
|
config = config or {}
|
||||||
|
config.border = "rounded"
|
||||||
|
vim.lsp.handlers.hover(err, result, ctx, config)
|
||||||
|
end,
|
||||||
|
["textDocument/signatureHelp"] = function(err, result, ctx, config)
|
||||||
|
config = config or {}
|
||||||
|
config.border = "rounded"
|
||||||
|
vim.lsp.handlers.signature_help(err, result, ctx, config)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
-- ── Capabilities (nvim-cmp) ───────────────────────────────────────
|
||||||
|
local caps = vim.tbl_deep_extend(
|
||||||
|
"force",
|
||||||
|
vim.lsp.protocol.make_client_capabilities(),
|
||||||
|
require("cmp_nvim_lsp").default_capabilities()
|
||||||
|
)
|
||||||
|
|
||||||
|
-- ── on_attach via LspAttach autocmd (0.11 pattern) ───────────────
|
||||||
|
vim.api.nvim_create_autocmd("LspAttach", {
|
||||||
|
group = vim.api.nvim_create_augroup("arch-dev-lsp", { clear = true }),
|
||||||
|
callback = function(event)
|
||||||
|
local buf = event.buf
|
||||||
|
local m = function(k, fn, d)
|
||||||
|
vim.keymap.set("n", k, fn, { buffer = buf, desc = d, silent = true })
|
||||||
|
end
|
||||||
|
m("gd", vim.lsp.buf.definition, "Definition")
|
||||||
|
m("gD", vim.lsp.buf.declaration, "Declaration")
|
||||||
|
m("gi", vim.lsp.buf.implementation, "Implementation")
|
||||||
|
m("gt", vim.lsp.buf.type_definition, "Type definition")
|
||||||
|
m("K", vim.lsp.buf.hover, "Hover docs")
|
||||||
|
m("<C-k>", vim.lsp.buf.signature_help, "Signature")
|
||||||
|
m("<leader>rn", vim.lsp.buf.rename, "Rename")
|
||||||
|
m("<leader>ca", vim.lsp.buf.code_action, "Code action")
|
||||||
|
m("<leader>f", function() vim.lsp.buf.format({ async = true }) end, "Format")
|
||||||
|
m("gr", "<cmd>Telescope lsp_references<cr>", "References")
|
||||||
|
m("<leader>ds", "<cmd>Telescope lsp_document_symbols<cr>", "Doc symbols")
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- ── Servers ───────────────────────────────────────────────────────
|
||||||
|
-- pyright — from pacman
|
||||||
|
vim.lsp.config("pyright", {
|
||||||
|
capabilities = caps,
|
||||||
|
settings = {
|
||||||
|
python = {
|
||||||
|
analysis = {
|
||||||
|
typeCheckingMode = "basic",
|
||||||
|
autoSearchPaths = true,
|
||||||
|
useLibraryCodeForTypes = true,
|
||||||
|
diagnosticMode = "openFilesOnly",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
vim.lsp.enable("pyright")
|
||||||
|
|
||||||
|
-- bash-language-server — from pacman
|
||||||
|
vim.lsp.config("bashls", {
|
||||||
|
capabilities = caps,
|
||||||
|
filetypes = { "sh", "bash", "zsh" },
|
||||||
|
})
|
||||||
|
vim.lsp.enable("bashls")
|
||||||
|
|
||||||
|
-- lua_ls — for editing neovim config inside arch-dev
|
||||||
|
vim.lsp.config("lua_ls", {
|
||||||
|
capabilities = caps,
|
||||||
|
settings = {
|
||||||
|
Lua = {
|
||||||
|
runtime = { version = "LuaJIT" },
|
||||||
|
diagnostics = { globals = { "vim" } },
|
||||||
|
workspace = {
|
||||||
|
library = vim.api.nvim_get_runtime_file("", true),
|
||||||
|
checkThirdParty = false,
|
||||||
|
},
|
||||||
|
telemetry = { enable = false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
vim.lsp.enable("lua_ls")
|
||||||
|
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Completion ────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"hrsh7th/nvim-cmp",
|
||||||
|
event = { "InsertEnter", "CmdlineEnter" },
|
||||||
|
dependencies = {
|
||||||
|
"L3MON4D3/LuaSnip",
|
||||||
|
"saadparwaiz1/cmp_luasnip",
|
||||||
|
"rafamadriz/friendly-snippets",
|
||||||
|
"onsails/lspkind.nvim",
|
||||||
|
"hrsh7th/cmp-nvim-lsp",
|
||||||
|
"hrsh7th/cmp-buffer",
|
||||||
|
"hrsh7th/cmp-path",
|
||||||
|
"hrsh7th/cmp-cmdline",
|
||||||
|
},
|
||||||
|
config = function()
|
||||||
|
local cmp = require("cmp")
|
||||||
|
local luasnip = require("luasnip")
|
||||||
|
local lspkind = require("lspkind")
|
||||||
|
|
||||||
|
require("luasnip.loaders.from_vscode").lazy_load()
|
||||||
|
|
||||||
|
cmp.setup({
|
||||||
|
snippet = {
|
||||||
|
expand = function(args) luasnip.lsp_expand(args.body) end,
|
||||||
|
},
|
||||||
|
window = {
|
||||||
|
completion = cmp.config.window.bordered(),
|
||||||
|
documentation = cmp.config.window.bordered(),
|
||||||
|
},
|
||||||
|
mapping = cmp.mapping.preset.insert({
|
||||||
|
["<C-b>"] = cmp.mapping.scroll_docs(-4),
|
||||||
|
["<C-f>"] = cmp.mapping.scroll_docs(4),
|
||||||
|
["<C-Space>"] = cmp.mapping.complete(),
|
||||||
|
["<C-e>"] = cmp.mapping.abort(),
|
||||||
|
["<CR>"] = cmp.mapping.confirm({ select = false }),
|
||||||
|
["<Tab>"] = cmp.mapping(function(fallback)
|
||||||
|
if cmp.visible() then
|
||||||
|
cmp.select_next_item()
|
||||||
|
elseif luasnip.expand_or_jumpable() then
|
||||||
|
luasnip.expand_or_jump()
|
||||||
|
else
|
||||||
|
fallback()
|
||||||
|
end
|
||||||
|
end, { "i", "s" }),
|
||||||
|
["<S-Tab>"] = cmp.mapping(function(fallback)
|
||||||
|
if cmp.visible() then
|
||||||
|
cmp.select_prev_item()
|
||||||
|
elseif luasnip.jumpable(-1) then
|
||||||
|
luasnip.jump(-1)
|
||||||
|
else
|
||||||
|
fallback()
|
||||||
|
end
|
||||||
|
end, { "i", "s" }),
|
||||||
|
}),
|
||||||
|
sources = cmp.config.sources({
|
||||||
|
{ name = "nvim_lsp", priority = 1000 },
|
||||||
|
{ name = "luasnip", priority = 750 },
|
||||||
|
{ name = "buffer", priority = 500 },
|
||||||
|
{ name = "path", priority = 250 },
|
||||||
|
}),
|
||||||
|
formatting = {
|
||||||
|
format = lspkind.cmp_format({
|
||||||
|
mode = "symbol_text",
|
||||||
|
maxwidth = 50,
|
||||||
|
ellipsis_char = "…",
|
||||||
|
menu = {
|
||||||
|
nvim_lsp = "[LSP]",
|
||||||
|
luasnip = "[Snip]",
|
||||||
|
buffer = "[Buf]",
|
||||||
|
path = "[Path]",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmp.setup.cmdline({ "/", "?" }, {
|
||||||
|
mapping = cmp.mapping.preset.cmdline(),
|
||||||
|
sources = { { name = "buffer" } },
|
||||||
|
})
|
||||||
|
cmp.setup.cmdline(":", {
|
||||||
|
mapping = cmp.mapping.preset.cmdline(),
|
||||||
|
sources = cmp.config.sources(
|
||||||
|
{ { name = "path" } },
|
||||||
|
{ { name = "cmdline" } }
|
||||||
|
),
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Linting ───────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"mfussenegger/nvim-lint",
|
||||||
|
event = { "BufReadPre", "BufNewFile" },
|
||||||
|
config = function()
|
||||||
|
local lint = require("lint")
|
||||||
|
lint.linters_by_ft = {
|
||||||
|
python = { "ruff" },
|
||||||
|
sh = { "shellcheck" },
|
||||||
|
bash = { "shellcheck" },
|
||||||
|
zsh = { "shellcheck" },
|
||||||
|
}
|
||||||
|
vim.api.nvim_create_autocmd(
|
||||||
|
{ "BufWritePost", "BufReadPost", "InsertLeave" },
|
||||||
|
{ callback = function() lint.try_lint() end }
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Formatting ────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"stevearc/conform.nvim",
|
||||||
|
event = { "BufWritePre" },
|
||||||
|
config = function()
|
||||||
|
require("conform").setup({
|
||||||
|
formatters_by_ft = {
|
||||||
|
python = { "black" },
|
||||||
|
sh = { "shfmt" },
|
||||||
|
bash = { "shfmt" },
|
||||||
|
lua = { "stylua" },
|
||||||
|
},
|
||||||
|
format_on_save = { timeout_ms = 1000, lsp_fallback = true },
|
||||||
|
notify_on_error = false,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Trouble: diagnostics panel ────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"folke/trouble.nvim",
|
||||||
|
dependencies = { "nvim-tree/nvim-web-devicons" },
|
||||||
|
config = function()
|
||||||
|
require("trouble").setup({ use_diagnostic_signs = true })
|
||||||
|
vim.keymap.set("n", "<leader>xx", "<cmd>TroubleToggle<cr>",
|
||||||
|
{ desc = "Toggle trouble" })
|
||||||
|
vim.keymap.set("n", "<leader>xd", "<cmd>TroubleToggle document_diagnostics<cr>",
|
||||||
|
{ desc = "Document diagnostics" })
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
104
dotfiles/.config/nvim/lua/plugins/tools.lua
Normal file
104
dotfiles/.config/nvim/lua/plugins/tools.lua
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
-- ── plugins/tools.lua ─────────────────────────────────────────────────────────
|
||||||
|
return {
|
||||||
|
|
||||||
|
-- ── LazyGit inside neovim ─────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"kdheepak/lazygit.nvim",
|
||||||
|
dependencies = { "nvim-lua/plenary.nvim" },
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Diffview ──────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"sindrets/diffview.nvim",
|
||||||
|
dependencies = { "nvim-lua/plenary.nvim" },
|
||||||
|
config = function()
|
||||||
|
require("diffview").setup()
|
||||||
|
vim.keymap.set("n", "<leader>gd", "<cmd>DiffviewOpen<cr>", { desc = "Diff view" })
|
||||||
|
vim.keymap.set("n", "<leader>gh", "<cmd>DiffviewFileHistory<cr>", { desc = "File history" })
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Leap: 2-char jump ─────────────────────────────────────────────────────
|
||||||
|
-- Repo moved GitHub → Codeberg Jan 2026. url= required.
|
||||||
|
-- API: plain <Plug> mappings. No setup(), no add/set_default_mappings().
|
||||||
|
{
|
||||||
|
url = "https://codeberg.org/andyg/leap.nvim",
|
||||||
|
config = function()
|
||||||
|
vim.keymap.set({ "n", "x", "o" }, "s", "<Plug>(leap)")
|
||||||
|
vim.keymap.set("n", "S", "<Plug>(leap-from-window)")
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Auto pairs ────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"windwp/nvim-autopairs",
|
||||||
|
event = "InsertEnter",
|
||||||
|
config = function()
|
||||||
|
require("nvim-autopairs").setup({
|
||||||
|
check_ts = true,
|
||||||
|
fast_wrap = { map = "<M-e>" },
|
||||||
|
})
|
||||||
|
local cmp_autopairs = require("nvim-autopairs.completion.cmp")
|
||||||
|
require("cmp").event:on("confirm_done", cmp_autopairs.on_confirm_done())
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Comment toggling ──────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"numToStr/Comment.nvim",
|
||||||
|
event = "BufReadPost",
|
||||||
|
dependencies = { "JoosepAlviste/nvim-ts-context-commentstring" },
|
||||||
|
config = function()
|
||||||
|
require("Comment").setup({
|
||||||
|
pre_hook = require("ts_context_commentstring.integrations.comment_nvim").create_pre_hook(),
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Surround ──────────────────────────────────────────────────────────────
|
||||||
|
-- v4: keymaps table removed from setup(). Use default ys/ds/cs mappings.
|
||||||
|
-- leap owns `s` but ys/ds/cs don't conflict with it.
|
||||||
|
{
|
||||||
|
"kylechui/nvim-surround",
|
||||||
|
event = "VeryLazy",
|
||||||
|
config = function()
|
||||||
|
require("nvim-surround").setup()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Todo comments ─────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"folke/todo-comments.nvim",
|
||||||
|
dependencies = { "nvim-lua/plenary.nvim" },
|
||||||
|
config = function()
|
||||||
|
require("todo-comments").setup()
|
||||||
|
vim.keymap.set("n", "<leader>ft", "<cmd>TodoTelescope<cr>", { desc = "Find TODOs" })
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Marks ─────────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"chentoast/marks.nvim",
|
||||||
|
event = "BufReadPost",
|
||||||
|
config = true,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Better quickfix ───────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"kevinhwang91/nvim-bqf",
|
||||||
|
ft = "qf",
|
||||||
|
config = true,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Zen mode ──────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"folke/zen-mode.nvim",
|
||||||
|
config = function()
|
||||||
|
require("zen-mode").setup({ window = { width = 0.85 } })
|
||||||
|
vim.keymap.set("n", "<leader>z", "<cmd>ZenMode<cr>", { desc = "Zen mode" })
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Plenary ───────────────────────────────────────────────────────────────
|
||||||
|
{ "nvim-lua/plenary.nvim", lazy = true },
|
||||||
|
}
|
||||||
81
dotfiles/.config/nvim/lua/plugins/treesitter.lua
Normal file
81
dotfiles/.config/nvim/lua/plugins/treesitter.lua
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
-- ── plugins/treesitter.lua ────────────────────────────────────────────────────
|
||||||
|
-- nvim-treesitter rewrote its API (Oct 2025).
|
||||||
|
-- - nvim-treesitter.configs is GONE
|
||||||
|
-- - New API: require("nvim-treesitter").setup{}
|
||||||
|
-- - Does NOT support lazy loading: lazy = false required
|
||||||
|
-- - treesitter-textobjects is now a fully separate module
|
||||||
|
|
||||||
|
return {
|
||||||
|
-- ── Core treesitter ───────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"nvim-treesitter/nvim-treesitter",
|
||||||
|
branch = "main",
|
||||||
|
lazy = false,
|
||||||
|
build = ":TSUpdate",
|
||||||
|
config = function()
|
||||||
|
require("nvim-treesitter").setup({
|
||||||
|
ensure_installed = {
|
||||||
|
"python",
|
||||||
|
"bash", "lua", "luadoc",
|
||||||
|
"json", "yaml", "toml",
|
||||||
|
"markdown", "markdown_inline",
|
||||||
|
"regex",
|
||||||
|
"vim", "vimdoc",
|
||||||
|
"diff", "gitcommit", "git_rebase",
|
||||||
|
"ini", "comment",
|
||||||
|
},
|
||||||
|
auto_install = false, -- stateless image, no runtime installs
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Highlight: enable per filetype via autocmd (new API pattern)
|
||||||
|
vim.api.nvim_create_autocmd("FileType", {
|
||||||
|
callback = function(ev)
|
||||||
|
-- Skip very large files
|
||||||
|
local ok, stats = pcall(vim.loop.fs_stat, vim.api.nvim_buf_get_name(ev.buf))
|
||||||
|
if ok and stats and stats.size > 500 * 1024 then return end
|
||||||
|
pcall(vim.treesitter.start)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Textobjects: separate plugin, separate module ─────────────────────────
|
||||||
|
{
|
||||||
|
"nvim-treesitter/nvim-treesitter-textobjects",
|
||||||
|
branch = "main",
|
||||||
|
lazy = false,
|
||||||
|
dependencies = {
|
||||||
|
{ "nvim-treesitter/nvim-treesitter", branch = "main" },
|
||||||
|
},
|
||||||
|
config = function()
|
||||||
|
require("nvim-treesitter-textobjects").setup({
|
||||||
|
select = {
|
||||||
|
enable = true,
|
||||||
|
lookahead = true,
|
||||||
|
keymaps = {
|
||||||
|
["af"] = "@function.outer",
|
||||||
|
["if"] = "@function.inner",
|
||||||
|
["ac"] = "@class.outer",
|
||||||
|
["ic"] = "@class.inner",
|
||||||
|
["aa"] = "@parameter.outer",
|
||||||
|
["ia"] = "@parameter.inner",
|
||||||
|
["ab"] = "@block.outer",
|
||||||
|
["ib"] = "@block.inner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
move = {
|
||||||
|
enable = true,
|
||||||
|
set_jumps = true,
|
||||||
|
goto_next_start = {
|
||||||
|
["]f"] = "@function.outer",
|
||||||
|
["]c"] = "@class.outer",
|
||||||
|
},
|
||||||
|
goto_previous_start = {
|
||||||
|
["[f"] = "@function.outer",
|
||||||
|
["[c"] = "@class.outer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
285
dotfiles/.config/nvim/lua/plugins/ui.lua
Normal file
285
dotfiles/.config/nvim/lua/plugins/ui.lua
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
-- ── plugins/ui.lua ────────────────────────────────────────────────────────────
|
||||||
|
return {
|
||||||
|
|
||||||
|
-- ── Colorscheme: Kanagawa Wave (desktop) / habamax (mobile) ───────────────
|
||||||
|
-- kanagawa requires termguicolors which Termius mangles as purple.
|
||||||
|
-- habamax is built-in, renders cleanly in 256-color (retrobox caused
|
||||||
|
-- redraw corruption on Termius, habamax does not).
|
||||||
|
{
|
||||||
|
"rebelot/kanagawa.nvim",
|
||||||
|
priority = 1000,
|
||||||
|
config = function()
|
||||||
|
if vim.env.MOBILE == "1" then
|
||||||
|
vim.cmd("colorscheme habamax")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
require("kanagawa").setup({
|
||||||
|
compile = true,
|
||||||
|
undercurl = true,
|
||||||
|
theme = "wave",
|
||||||
|
background = {
|
||||||
|
dark = "wave",
|
||||||
|
light = "lotus",
|
||||||
|
},
|
||||||
|
colors = {
|
||||||
|
theme = {
|
||||||
|
wave = {
|
||||||
|
ui = {
|
||||||
|
bg_gutter = "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overrides = function(colors)
|
||||||
|
local theme = colors.theme
|
||||||
|
return {
|
||||||
|
Pmenu = { fg = theme.ui.shade0, bg = theme.ui.bg_p1 },
|
||||||
|
PmenuSbar = { bg = theme.ui.bg_m1 },
|
||||||
|
PmenuThumb = { bg = theme.ui.bg_p2 },
|
||||||
|
CursorLine = { bg = theme.ui.bg_p1 },
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
vim.cmd("colorscheme kanagawa-wave")
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Statusline: lualine ───────────────────────────────────────────────────
|
||||||
|
-- theme = "auto" derives from active colorscheme — works for both
|
||||||
|
-- kanagawa (desktop) and habamax (mobile) without hex colors
|
||||||
|
{
|
||||||
|
"nvim-lualine/lualine.nvim",
|
||||||
|
dependencies = { "nvim-tree/nvim-web-devicons" },
|
||||||
|
config = function()
|
||||||
|
require("lualine").setup({
|
||||||
|
options = {
|
||||||
|
theme = "auto",
|
||||||
|
component_separators = { left = "│", right = "│" },
|
||||||
|
section_separators = { left = "", right = "" },
|
||||||
|
globalstatus = true,
|
||||||
|
},
|
||||||
|
sections = {
|
||||||
|
lualine_a = { { "mode", icon = "" } },
|
||||||
|
lualine_b = {
|
||||||
|
{ "branch", icon = "" },
|
||||||
|
{ "diff",
|
||||||
|
symbols = { added = " ", modified = " ", removed = " " },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lualine_c = {
|
||||||
|
{ "filename", path = 1, symbols = { modified = "●", readonly = "" } },
|
||||||
|
},
|
||||||
|
lualine_x = {
|
||||||
|
{ "diagnostics",
|
||||||
|
symbols = { error = " ", warn = " ", info = " ", hint = " " },
|
||||||
|
},
|
||||||
|
"filetype",
|
||||||
|
},
|
||||||
|
lualine_y = { "progress" },
|
||||||
|
lualine_z = { { "location", icon = "" } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Bufferline (desktop only) ─────────────────────────────────────────────
|
||||||
|
-- Renders truecolor backgrounds for tabs which Termius shows as purple bars.
|
||||||
|
{
|
||||||
|
"akinsho/bufferline.nvim",
|
||||||
|
enabled = function() return vim.env.MOBILE ~= "1" end,
|
||||||
|
dependencies = { "nvim-tree/nvim-web-devicons" },
|
||||||
|
config = function()
|
||||||
|
require("bufferline").setup({
|
||||||
|
options = {
|
||||||
|
mode = "buffers",
|
||||||
|
separator_style = "slant",
|
||||||
|
show_close_icon = false,
|
||||||
|
show_buffer_close_icons = false,
|
||||||
|
diagnostics = "nvim_lsp",
|
||||||
|
diagnostics_indicator = function(_, _, diag)
|
||||||
|
local icons = { error = " ", warning = " " }
|
||||||
|
local ret = (diag.error and icons.error .. diag.error .. " " or "")
|
||||||
|
.. (diag.warning and icons.warning .. diag.warning or "")
|
||||||
|
return vim.trim(ret)
|
||||||
|
end,
|
||||||
|
offsets = {
|
||||||
|
{ filetype = "oil", text = "File Explorer", separator = true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── File explorer: oil.nvim ───────────────────────────────────────────────
|
||||||
|
-- Edit the filesystem like a buffer. Very Arch: does one thing, perfectly.
|
||||||
|
{
|
||||||
|
"stevearc/oil.nvim",
|
||||||
|
dependencies = { "nvim-tree/nvim-web-devicons" },
|
||||||
|
config = function()
|
||||||
|
require("oil").setup({
|
||||||
|
default_file_explorer = true,
|
||||||
|
columns = {
|
||||||
|
"icon",
|
||||||
|
"permissions",
|
||||||
|
"size",
|
||||||
|
"mtime",
|
||||||
|
},
|
||||||
|
view_options = {
|
||||||
|
show_hidden = true,
|
||||||
|
},
|
||||||
|
keymaps = {
|
||||||
|
["<C-h>"] = false, -- don't hijack window nav
|
||||||
|
["<C-l>"] = false,
|
||||||
|
["q"] = "actions.close",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Fuzzy finder: Telescope ───────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"nvim-telescope/telescope.nvim",
|
||||||
|
dependencies = {
|
||||||
|
"nvim-lua/plenary.nvim",
|
||||||
|
-- Native FZF sorter — faster than the lua one
|
||||||
|
{ "nvim-telescope/telescope-fzf-native.nvim", build = "make" },
|
||||||
|
},
|
||||||
|
config = function()
|
||||||
|
local telescope = require("telescope")
|
||||||
|
local actions = require("telescope.actions")
|
||||||
|
|
||||||
|
telescope.setup({
|
||||||
|
defaults = {
|
||||||
|
prompt_prefix = "❯ ",
|
||||||
|
selection_caret = "▶ ",
|
||||||
|
-- Kanagawa-inspired layout
|
||||||
|
layout_strategy = "horizontal",
|
||||||
|
layout_config = { prompt_position = "top", width = 0.9 },
|
||||||
|
sorting_strategy = "ascending",
|
||||||
|
mappings = {
|
||||||
|
i = {
|
||||||
|
["<Esc>"] = actions.close,
|
||||||
|
["<C-j>"] = actions.move_selection_next,
|
||||||
|
["<C-k>"] = actions.move_selection_previous,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
telescope.load_extension("fzf")
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Which-key ─────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"folke/which-key.nvim",
|
||||||
|
event = "VeryLazy",
|
||||||
|
config = function()
|
||||||
|
require("which-key").setup({
|
||||||
|
preset = "modern",
|
||||||
|
delay = 400,
|
||||||
|
icons = { separator = "→" },
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Git signs in gutter ───────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"lewis6991/gitsigns.nvim",
|
||||||
|
config = function()
|
||||||
|
require("gitsigns").setup({
|
||||||
|
signs = {
|
||||||
|
add = { text = "▎" },
|
||||||
|
change = { text = "▎" },
|
||||||
|
delete = { text = "" },
|
||||||
|
topdelete = { text = "" },
|
||||||
|
changedelete = { text = "▎" },
|
||||||
|
untracked = { text = "▎" },
|
||||||
|
},
|
||||||
|
on_attach = function(buf)
|
||||||
|
local gs = require("gitsigns")
|
||||||
|
local m = function(k, fn, d)
|
||||||
|
vim.keymap.set("n", k, fn, { buffer = buf, desc = d })
|
||||||
|
end
|
||||||
|
m("]c", gs.next_hunk, "Next hunk")
|
||||||
|
m("[c", gs.prev_hunk, "Prev hunk")
|
||||||
|
m("<leader>hs", gs.stage_hunk, "Stage hunk")
|
||||||
|
m("<leader>hr", gs.reset_hunk, "Reset hunk")
|
||||||
|
m("<leader>hp", gs.preview_hunk, "Preview hunk")
|
||||||
|
m("<leader>hb", gs.blame_line, "Blame line")
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Indent guides ─────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"lukas-reineke/indent-blankline.nvim",
|
||||||
|
main = "ibl",
|
||||||
|
config = function()
|
||||||
|
require("ibl").setup({
|
||||||
|
indent = { char = "│" },
|
||||||
|
scope = { char = "│" },
|
||||||
|
})
|
||||||
|
-- Link to existing highlight groups instead of hex (mobile-safe)
|
||||||
|
vim.cmd("hi! link IblIndent Comment")
|
||||||
|
vim.cmd("hi! link IblScope LineNr")
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
-- ── Dashboard ─────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"goolord/alpha-nvim",
|
||||||
|
event = "VimEnter",
|
||||||
|
config = function()
|
||||||
|
local alpha = require("alpha")
|
||||||
|
local dashboard = require("alpha.themes.dashboard")
|
||||||
|
|
||||||
|
-- Mobile-friendly ASCII (heavy blocks render as red bars in Termius)
|
||||||
|
dashboard.section.header.val = {
|
||||||
|
" ",
|
||||||
|
" ┌─┐┬─┐┌─┐┬ ┬ ┌┬┐┌─┐┬ ┬ ",
|
||||||
|
" ├─┤├┬┘│ ├─┤───│││├┤ └┐┌┘ ",
|
||||||
|
" ┴ ┴┴└─└─┘┴ ┴ ─┴┘└─┘ └┘ ",
|
||||||
|
" ",
|
||||||
|
" riced neovim ide · arch linux · v1.7 ",
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard.section.buttons.val = {
|
||||||
|
dashboard.button("f", " Find file", "<cmd>Telescope find_files<cr>"),
|
||||||
|
dashboard.button("r", " Recent files", "<cmd>Telescope oldfiles<cr>"),
|
||||||
|
dashboard.button("g", " Live grep", "<cmd>Telescope live_grep<cr>"),
|
||||||
|
dashboard.button("e", " Explorer", "<cmd>Oil<cr>"),
|
||||||
|
dashboard.button("l", " Lazy", "<cmd>Lazy<cr>"),
|
||||||
|
dashboard.button("q", " Quit", "<cmd>qa<cr>"),
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard.section.header.opts.hl = "Comment"
|
||||||
|
dashboard.section.footer.opts.hl = "NonText"
|
||||||
|
|
||||||
|
alpha.setup(dashboard.opts)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Smooth scrolling ──────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"karb94/neoscroll.nvim",
|
||||||
|
config = function()
|
||||||
|
require("neoscroll").setup({ mappings = { "<C-u>","<C-d>","<C-b>","<C-f>" } })
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- ── Color previews inline (desktop only) ──────────────────────────────────
|
||||||
|
-- Renders hex as RGB which Termius mangles. Skip on mobile.
|
||||||
|
{
|
||||||
|
"NvChad/nvim-colorizer.lua",
|
||||||
|
enabled = function() return vim.env.MOBILE ~= "1" end,
|
||||||
|
config = function()
|
||||||
|
require("colorizer").setup({ "*" }, { mode = "virtualtext" })
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Web devicons (used everywhere)
|
||||||
|
{ "nvim-tree/nvim-web-devicons", lazy = true },
|
||||||
|
}
|
||||||
17
dotfiles/.config/roaming/hosts
Normal file
17
dotfiles/.config/roaming/hosts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# ── arch-dev roaming :: host registry ─────────────────────────────────────────
|
||||||
|
# One host per line. Lines starting with # are ignored.
|
||||||
|
#
|
||||||
|
# Fields (whitespace-separated):
|
||||||
|
# name short alias used for the shell function (e.g. `mail`)
|
||||||
|
# fqdn tailnet hostname or any reachable host (e.g. mail.tailnet.ts)
|
||||||
|
# user ssh user (optional, defaults to $ROAMING_DEFAULT_USER or current user)
|
||||||
|
# mount remote path to mount via sshfs (optional, e.g. / or /srv)
|
||||||
|
#
|
||||||
|
# Examples (delete these, add your own):
|
||||||
|
#
|
||||||
|
# name fqdn user mount
|
||||||
|
# mail mail.tailnet.ts root /
|
||||||
|
# proxy proxy.tailnet.ts admin /etc/caddy
|
||||||
|
# dev dev-server.tailnet.ts youruser /srv
|
||||||
|
|
||||||
|
# name fqdn user mount
|
||||||
153
dotfiles/.config/roaming/roaming.zsh
Normal file
153
dotfiles/.config/roaming/roaming.zsh
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
# ║ arch-dev roaming :: engine ║
|
||||||
|
# ║ Roaming ~ over the tailnet. Source this from .zshrc. ║
|
||||||
|
# ║ ║
|
||||||
|
# ║ Reads ~/.config/roaming/hosts and generates a function per ║
|
||||||
|
# ║ host so you can run: ║
|
||||||
|
# ║ mail → interactive shell on mail ║
|
||||||
|
# ║ mail tail -f /var/log/x → run with TTY (works for -f, sudo) ║
|
||||||
|
# ║ mount_host mail → sshfs the host's mount at ║
|
||||||
|
# ║ /workspace/mail ║
|
||||||
|
# ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
export ROAMING_DIR="${ROAMING_DIR:-$HOME/.config/roaming}"
|
||||||
|
export ROAMING_HOSTS="${ROAMING_HOSTS:-$ROAMING_DIR/hosts}"
|
||||||
|
export ROAMING_MOUNT_BASE="${ROAMING_MOUNT_BASE:-/workspace}"
|
||||||
|
export ROAMING_DEFAULT_USER="${ROAMING_DEFAULT_USER:-$USER}"
|
||||||
|
|
||||||
|
# sshfs options that prevent the dreaded indefinite-freeze on network blips.
|
||||||
|
# ServerAliveInterval=15 + CountMax=3 → I/O errors out after ~45s instead of
|
||||||
|
# hanging forever. reconnect re-establishes when the host comes back.
|
||||||
|
export ROAMING_SSHFS_OPTS="reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,follow_symlinks"
|
||||||
|
|
||||||
|
# ── Internal: resolve a host name to its registry fields ──────────────────────
|
||||||
|
# Sets globals: _RH_NAME _RH_FQDN _RH_USER _RH_MOUNT
|
||||||
|
# Returns 1 if not found.
|
||||||
|
_roaming_lookup() {
|
||||||
|
local want="$1"
|
||||||
|
[[ -f "$ROAMING_HOSTS" ]] || return 1
|
||||||
|
local name fqdn user mount
|
||||||
|
while read -r name fqdn user mount; do
|
||||||
|
[[ -z "$name" || "$name" == \#* ]] && continue
|
||||||
|
if [[ "$name" == "$want" ]]; then
|
||||||
|
_RH_NAME="$name"
|
||||||
|
_RH_FQDN="$fqdn"
|
||||||
|
_RH_USER="${user:-$ROAMING_DEFAULT_USER}"
|
||||||
|
_RH_MOUNT="$mount"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done < "$ROAMING_HOSTS"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Internal: the actual remote-exec logic ────────────────────────────────────
|
||||||
|
# No args → interactive shell. Args → run with a TTY (so tail -f / sudo work).
|
||||||
|
# "Dumb" by design: pipes/redirects run LOCALLY, exactly like plain ssh.
|
||||||
|
_roaming_exec() {
|
||||||
|
local name="$1"; shift
|
||||||
|
if ! _roaming_lookup "$name"; then
|
||||||
|
echo "roaming: unknown host '$name' (check $ROAMING_HOSTS)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local target="${_RH_USER}@${_RH_FQDN}"
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
ssh -t "$target"
|
||||||
|
else
|
||||||
|
ssh -t "$target" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── mount_host <name> ─────────────────────────────────────────────────────────
|
||||||
|
mount_host() {
|
||||||
|
local name="$1"
|
||||||
|
if [[ -z "$name" ]]; then
|
||||||
|
echo "usage: mount_host <name>" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! _roaming_lookup "$name"; then
|
||||||
|
echo "roaming: unknown host '$name'" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -z "$_RH_MOUNT" ]]; then
|
||||||
|
echo "roaming: '$name' has no mount path in registry" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local mp="$ROAMING_MOUNT_BASE/$name"
|
||||||
|
# Already mounted?
|
||||||
|
if mountpoint -q "$mp" 2>/dev/null; then
|
||||||
|
echo "roaming: $name already mounted at $mp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
mkdir -p "$mp"
|
||||||
|
echo "roaming: mounting ${_RH_USER}@${_RH_FQDN}:${_RH_MOUNT} → $mp"
|
||||||
|
sshfs "${_RH_USER}@${_RH_FQDN}:${_RH_MOUNT}" "$mp" \
|
||||||
|
-o "$ROAMING_SSHFS_OPTS" \
|
||||||
|
&& echo "roaming: ✓ mounted" \
|
||||||
|
|| echo "roaming: ✗ mount failed" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── unmount_host <name> ───────────────────────────────────────────────────────
|
||||||
|
unmount_host() {
|
||||||
|
local name="$1"
|
||||||
|
if [[ -z "$name" ]]; then
|
||||||
|
echo "usage: unmount_host <name>" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local mp="$ROAMING_MOUNT_BASE/$name"
|
||||||
|
if ! mountpoint -q "$mp" 2>/dev/null; then
|
||||||
|
echo "roaming: $name not mounted"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# fusermount3 first; -z (lazy) as fallback for stale mounts
|
||||||
|
fusermount3 -u "$mp" 2>/dev/null \
|
||||||
|
|| fusermount3 -uz "$mp" 2>/dev/null \
|
||||||
|
|| umount "$mp" 2>/dev/null
|
||||||
|
if mountpoint -q "$mp" 2>/dev/null; then
|
||||||
|
echo "roaming: ✗ failed to unmount $name (stale? try: fusermount3 -uz $mp)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rmdir "$mp" 2>/dev/null
|
||||||
|
echo "roaming: ✓ unmounted $name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── roaming-status ────────────────────────────────────────────────────────────
|
||||||
|
roaming-status() {
|
||||||
|
echo "── registered hosts ──"
|
||||||
|
if [[ -f "$ROAMING_HOSTS" ]]; then
|
||||||
|
local name fqdn user mount
|
||||||
|
while read -r name fqdn user mount; do
|
||||||
|
[[ -z "$name" || "$name" == \#* ]] && continue
|
||||||
|
local mp="$ROAMING_MOUNT_BASE/$name"
|
||||||
|
local mflag=""
|
||||||
|
mountpoint -q "$mp" 2>/dev/null && mflag=" [mounted: $mp]"
|
||||||
|
printf " %-10s %s@%s%s\n" "$name" "${user:-$ROAMING_DEFAULT_USER}" "$fqdn" "$mflag"
|
||||||
|
done < "$ROAMING_HOSTS"
|
||||||
|
else
|
||||||
|
echo " (no registry at $ROAMING_HOSTS)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Generate a function per host ──────────────────────────────────────────────
|
||||||
|
# After this loop, each registry entry `mail` becomes a `mail` command.
|
||||||
|
_roaming_generate() {
|
||||||
|
[[ -f "$ROAMING_HOSTS" ]] || return 0
|
||||||
|
local name fqdn user mount
|
||||||
|
while read -r name fqdn user mount; do
|
||||||
|
[[ -z "$name" || "$name" == \#* ]] && continue
|
||||||
|
# Skip if it would clobber an existing command (safety)
|
||||||
|
if command -v "$name" >/dev/null 2>&1 && ! typeset -f "$name" >/dev/null 2>&1; then
|
||||||
|
echo "roaming: skipping '$name' — conflicts with existing command" >&2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
eval "$name() { _roaming_exec '$name' \"\$@\"; }"
|
||||||
|
done < "$ROAMING_HOSTS"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reload registry + regenerate functions (call after editing hosts)
|
||||||
|
roaming-reload() {
|
||||||
|
_roaming_generate
|
||||||
|
echo "roaming: reloaded $(grep -cvE '^\s*#|^\s*$' "$ROAMING_HOSTS" 2>/dev/null || echo 0) host(s)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate on source
|
||||||
|
_roaming_generate
|
||||||
45
dotfiles/.config/starship-mobile.toml
Normal file
45
dotfiles/.config/starship-mobile.toml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
# ║ arch-dev :: starship-mobile.toml ║
|
||||||
|
# ║ Single line · no icons · 256-color · Termius-safe ║
|
||||||
|
# ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
format = "$directory$git_branch$git_status$python $character"
|
||||||
|
right_format = "$cmd_duration"
|
||||||
|
|
||||||
|
[directory]
|
||||||
|
style = "bold fg:173"
|
||||||
|
truncation_length = 3
|
||||||
|
truncate_to_repo = true
|
||||||
|
read_only = "[ro]"
|
||||||
|
format = "[$path]($style)[$read_only](fg:167) "
|
||||||
|
|
||||||
|
[git_branch]
|
||||||
|
symbol = ""
|
||||||
|
style = "fg:106"
|
||||||
|
format = "[$symbol$branch]($style) "
|
||||||
|
|
||||||
|
[git_status]
|
||||||
|
style = "fg:167"
|
||||||
|
conflicted = "!"
|
||||||
|
ahead = "+${count}"
|
||||||
|
behind = "-${count}"
|
||||||
|
modified = "*"
|
||||||
|
untracked = "?"
|
||||||
|
staged = "+"
|
||||||
|
format = "([$all_status$ahead_behind]($style)) "
|
||||||
|
|
||||||
|
[python]
|
||||||
|
symbol = "py:"
|
||||||
|
style = "fg:66"
|
||||||
|
format = "[$symbol$version]($style) "
|
||||||
|
detect_files = ["*.py", "requirements.txt", "pyproject.toml"]
|
||||||
|
|
||||||
|
[cmd_duration]
|
||||||
|
min_time = 3000
|
||||||
|
style = "fg:242"
|
||||||
|
format = "[$duration]($style)"
|
||||||
|
|
||||||
|
[character]
|
||||||
|
success_symbol = "[>](bold fg:106)"
|
||||||
|
error_symbol = "[>](bold fg:167)"
|
||||||
|
vimcmd_symbol = "[<](bold fg:139)"
|
||||||
76
dotfiles/.config/starship.toml
Normal file
76
dotfiles/.config/starship.toml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
# ║ arch-dev :: starship.toml (desktop) ║
|
||||||
|
# ║ Kanagawa Wave · 256-color only · no hex RGB ║
|
||||||
|
# ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
# 256-color Kanagawa approximations:
|
||||||
|
# 110 = crystalBlue 106 = springGreen 139 = oniViolet
|
||||||
|
# 173 = boatYellow 167 = peachRed 66 = waveAqua
|
||||||
|
# 242 = fujiGray 250 = fujiWhite 236 = waveBlue
|
||||||
|
|
||||||
|
format = """
|
||||||
|
╭─$os$username$hostname$directory$git_branch$git_status$python
|
||||||
|
╰─$character"""
|
||||||
|
|
||||||
|
right_format = """$cmd_duration$time"""
|
||||||
|
|
||||||
|
[os]
|
||||||
|
disabled = false
|
||||||
|
style = "bold fg:139"
|
||||||
|
|
||||||
|
[os.symbols]
|
||||||
|
Arch = " "
|
||||||
|
|
||||||
|
[username]
|
||||||
|
show_always = true
|
||||||
|
style_user = "bold fg:110"
|
||||||
|
style_root = "bold fg:167"
|
||||||
|
format = "[$user]($style)"
|
||||||
|
|
||||||
|
[hostname]
|
||||||
|
ssh_only = false
|
||||||
|
style = "fg:242"
|
||||||
|
format = "@[$hostname]($style) "
|
||||||
|
|
||||||
|
[directory]
|
||||||
|
style = "bold fg:173"
|
||||||
|
truncation_length = 5
|
||||||
|
truncate_to_repo = true
|
||||||
|
read_only = " "
|
||||||
|
format = "in [$path]($style)[$read_only](fg:167) "
|
||||||
|
|
||||||
|
[git_branch]
|
||||||
|
symbol = " "
|
||||||
|
style = "bold fg:106"
|
||||||
|
format = "on [$symbol$branch]($style) "
|
||||||
|
|
||||||
|
[git_status]
|
||||||
|
style = "fg:167"
|
||||||
|
conflicted = "✖ "
|
||||||
|
ahead = "⇡${count} "
|
||||||
|
behind = "⇣${count} "
|
||||||
|
modified = "✎${count} "
|
||||||
|
untracked = "?${count} "
|
||||||
|
staged = "✔${count} "
|
||||||
|
format = "([$all_status$ahead_behind]($style))"
|
||||||
|
|
||||||
|
[python]
|
||||||
|
symbol = " "
|
||||||
|
style = "fg:66"
|
||||||
|
format = " [$symbol$version( $virtualenv)]($style)"
|
||||||
|
detect_files = ["*.py", "requirements.txt", "pyproject.toml"]
|
||||||
|
|
||||||
|
[cmd_duration]
|
||||||
|
min_time = 2000
|
||||||
|
style = "fg:242"
|
||||||
|
format = " took [$duration]($style)"
|
||||||
|
|
||||||
|
[time]
|
||||||
|
disabled = false
|
||||||
|
style = "fg:236"
|
||||||
|
format = " [$time]($style)"
|
||||||
|
time_format = "%H:%M"
|
||||||
|
|
||||||
|
[character]
|
||||||
|
success_symbol = "[❯](bold fg:106)"
|
||||||
|
error_symbol = "[❯](bold fg:167)"
|
||||||
|
vimcmd_symbol = "[❮](bold fg:139)"
|
||||||
92
dotfiles/.config/tmux/tmux.conf
Normal file
92
dotfiles/.config/tmux/tmux.conf
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
# ║ arch-dev :: tmux.conf (desktop) ║
|
||||||
|
# ║ Kanagawa Wave · 256-color · Ctrl+Space prefix · vim-nav ║
|
||||||
|
# ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
# ── Prefix ────────────────────────────────────────────────────────────────────
|
||||||
|
unbind C-b
|
||||||
|
set -g prefix C-Space
|
||||||
|
bind C-Space send-prefix
|
||||||
|
|
||||||
|
# ── Core behavior ─────────────────────────────────────────────────────────────
|
||||||
|
set -g mouse on
|
||||||
|
set -g base-index 1
|
||||||
|
setw -g pane-base-index 1
|
||||||
|
set -g renumber-windows on
|
||||||
|
set -sg escape-time 5
|
||||||
|
set -g history-limit 50000
|
||||||
|
set -g focus-events on
|
||||||
|
set -g display-time 2000
|
||||||
|
|
||||||
|
# ── 256 color (no truecolor — Termius mangles it as purple) ───────────────────
|
||||||
|
set -g default-terminal "tmux-256color"
|
||||||
|
|
||||||
|
# ── Clipboard (OSC 52) ────────────────────────────────────────────────────────
|
||||||
|
set -g set-clipboard on
|
||||||
|
|
||||||
|
# ── Splits in cwd ─────────────────────────────────────────────────────────────
|
||||||
|
bind | split-window -h -c "#{pane_current_path}"
|
||||||
|
bind - split-window -v -c "#{pane_current_path}"
|
||||||
|
bind c new-window -c "#{pane_current_path}"
|
||||||
|
unbind '"'
|
||||||
|
unbind %
|
||||||
|
|
||||||
|
# ── Vim-style pane navigation ─────────────────────────────────────────────────
|
||||||
|
bind h select-pane -L
|
||||||
|
bind j select-pane -D
|
||||||
|
bind k select-pane -U
|
||||||
|
bind l select-pane -R
|
||||||
|
|
||||||
|
# ── Alt+number window switching ───────────────────────────────────────────────
|
||||||
|
bind -n M-1 select-window -t 1
|
||||||
|
bind -n M-2 select-window -t 2
|
||||||
|
bind -n M-3 select-window -t 3
|
||||||
|
bind -n M-4 select-window -t 4
|
||||||
|
bind -n M-5 select-window -t 5
|
||||||
|
|
||||||
|
# ── Pane resize ───────────────────────────────────────────────────────────────
|
||||||
|
bind -r H resize-pane -L 5
|
||||||
|
bind -r J resize-pane -D 5
|
||||||
|
bind -r K resize-pane -U 5
|
||||||
|
bind -r L resize-pane -R 5
|
||||||
|
|
||||||
|
# ── Zoom & scroll mode ────────────────────────────────────────────────────────
|
||||||
|
bind z resize-pane -Z
|
||||||
|
bind Enter copy-mode
|
||||||
|
bind -T copy-mode-vi v send -X begin-selection
|
||||||
|
bind -T copy-mode-vi y send -X copy-selection-and-cancel
|
||||||
|
setw -g mode-keys vi
|
||||||
|
|
||||||
|
# ── Reload & detach ───────────────────────────────────────────────────────────
|
||||||
|
bind r source-file ~/.tmux.conf \; display "↻ tmux reloaded"
|
||||||
|
bind d detach-client
|
||||||
|
|
||||||
|
# ── Status bar: 256-color Kanagawa Wave approximations ────────────────────────
|
||||||
|
# 234=sumiInk0, 236=sumiInk3, 237=waveBlue1, 110=crystalBlue
|
||||||
|
# 173=boatYellow2, 106=springGreen, 242=fujiGray
|
||||||
|
# 250=fujiWhite, 139=oniViolet
|
||||||
|
set -g status on
|
||||||
|
set -g status-interval 5
|
||||||
|
set -g status-position bottom
|
||||||
|
set -g status-style "bg=colour234,fg=colour250"
|
||||||
|
|
||||||
|
set -g status-left-length 40
|
||||||
|
set -g status-left \
|
||||||
|
"#[bold,fg=colour139,bg=colour234] arch-dev#[fg=colour236] │ #[default]"
|
||||||
|
|
||||||
|
set -g status-right-length 60
|
||||||
|
set -g status-right \
|
||||||
|
"#[fg=colour106]%H:%M #[fg=colour236]│ #[fg=colour110]%a %d %b"
|
||||||
|
|
||||||
|
# Window tabs
|
||||||
|
set -g window-status-format " #I #W "
|
||||||
|
set -g window-status-current-format "#[bold,fg=colour173,bg=colour237] #I #W #[default]"
|
||||||
|
set -g window-status-style "fg=colour242"
|
||||||
|
set -g window-status-current-style "fg=colour173,bg=colour237"
|
||||||
|
|
||||||
|
# Pane borders
|
||||||
|
set -g pane-border-style "fg=colour236"
|
||||||
|
set -g pane-active-border-style "fg=colour110"
|
||||||
|
|
||||||
|
# Message bar
|
||||||
|
set -g message-style "bg=colour237,fg=colour250,bold"
|
||||||
57
dotfiles/.screenrc
Normal file
57
dotfiles/.screenrc
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
# ║ arch-dev :: screenrc (mobile multiplexer) ║
|
||||||
|
# ║ Termius-optimized · clean status · keyboard-friendly ║
|
||||||
|
# ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
# ── Basics ────────────────────────────────────────────────────────────────────
|
||||||
|
startup_message off
|
||||||
|
vbell off
|
||||||
|
defscrollback 10000
|
||||||
|
defutf8 on
|
||||||
|
defencoding utf8
|
||||||
|
autodetach on
|
||||||
|
shell -zsh
|
||||||
|
|
||||||
|
# ── 256 color (no truecolor — Termius mangles RGB as purple) ─────────────────
|
||||||
|
term xterm-256color
|
||||||
|
termcapinfo xterm-256color|xterm 'Co#256:AB=\E[48;5;%dm:AF=\E[38;5;%dm'
|
||||||
|
termcapinfo xterm-256color ti@:te@
|
||||||
|
|
||||||
|
# ── Status bar ────────────────────────────────────────────────────────────────
|
||||||
|
# Keep it readable on a small screen
|
||||||
|
hardstatus alwayslastline
|
||||||
|
hardstatus string '%{= kw}[ %{= kc}arch-dev%{= kw} ][ %{= kg}%w%{= kw} ]%=%{= kw}[ %{= kY}%D %d %M%{= kw} ][ %{= kc}%0c%{= kw} ]'
|
||||||
|
|
||||||
|
# ── Keybindings (tearless mobile ref) ────────────────────────────────────────
|
||||||
|
# Prefix stays Ctrl+a (default) — familiar and easy on mobile
|
||||||
|
|
||||||
|
# New window: Ctrl+a c
|
||||||
|
# Next window: Ctrl+a n (or Ctrl+a Space)
|
||||||
|
# Prev window: Ctrl+a p
|
||||||
|
# List windows: Ctrl+a "
|
||||||
|
# Scroll mode: Ctrl+a [
|
||||||
|
# Exit scroll: q
|
||||||
|
# Detach: Ctrl+a d
|
||||||
|
# Reattach: screen -RD (alias: mux)
|
||||||
|
|
||||||
|
# Make scroll mode vi-friendly
|
||||||
|
markkeys h=^H:l=^L:$=^E
|
||||||
|
|
||||||
|
# ── Mouse ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Disabled — Termius touch handles this better directly
|
||||||
|
# mousetrack on
|
||||||
|
|
||||||
|
# ── Windows ───────────────────────────────────────────────────────────────────
|
||||||
|
# Number pad for quick window switch
|
||||||
|
bind 1 select 1
|
||||||
|
bind 2 select 2
|
||||||
|
bind 3 select 3
|
||||||
|
bind 4 select 4
|
||||||
|
bind 5 select 5
|
||||||
|
|
||||||
|
# Reload
|
||||||
|
bind r source ~/.screenrc
|
||||||
|
|
||||||
|
# ── Logging ───────────────────────────────────────────────────────────────────
|
||||||
|
# deflog on
|
||||||
|
# logfile $HOME/.screen/screen-%Y%m%d-%n.log
|
||||||
207
dotfiles/.zshrc
Normal file
207
dotfiles/.zshrc
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
# ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
# ║ arch-dev :: zshrc ║
|
||||||
|
# ║ Kanagawa Wave · 256-color · mobile-aware · AUR-powered ║
|
||||||
|
# ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
# ── Environment ───────────────────────────────────────────────────────────────
|
||||||
|
export EDITOR=nvim
|
||||||
|
export VISUAL=nvim
|
||||||
|
export PAGER="less"
|
||||||
|
export MANPAGER="nvim +Man!"
|
||||||
|
export TERM=xterm-256color
|
||||||
|
export PATH="$HOME/.local/bin:$HOME/perl5/bin:/usr/local/bin:$PATH"
|
||||||
|
export DOCKER_BUILDKIT=1
|
||||||
|
|
||||||
|
# NO truecolor — Termius renders hex RGB as purple
|
||||||
|
unset COLORTERM
|
||||||
|
|
||||||
|
# ── History ───────────────────────────────────────────────────────────────────
|
||||||
|
HISTSIZE=50000
|
||||||
|
SAVEHIST=50000
|
||||||
|
HISTFILE="${HISTFILE:-$HOME/.zsh_history}"
|
||||||
|
setopt share_history hist_ignore_dups hist_ignore_space
|
||||||
|
setopt hist_reduce_blanks extended_history
|
||||||
|
|
||||||
|
# ── ZSH Options ───────────────────────────────────────────────────────────────
|
||||||
|
setopt auto_cd correct interactive_comments no_beep
|
||||||
|
|
||||||
|
# ── Completion ────────────────────────────────────────────────────────────────
|
||||||
|
autoload -Uz compinit && compinit
|
||||||
|
zstyle ':completion:*' menu select
|
||||||
|
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
|
||||||
|
zstyle ':completion:*:descriptions' format '%F{yellow}── %d ──%f'
|
||||||
|
|
||||||
|
# ── Plugins ───────────────────────────────────────────────────────────────────
|
||||||
|
source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
|
||||||
|
source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh
|
||||||
|
source /usr/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh
|
||||||
|
|
||||||
|
bindkey '^[[A' history-substring-search-up
|
||||||
|
bindkey '^[[B' history-substring-search-down
|
||||||
|
|
||||||
|
# ── zsh-syntax-highlighting: 256-color only (no hex/truecolor) ────────────────
|
||||||
|
# Kanagawa Wave approximations in 256-color palette
|
||||||
|
# 110 = steel blue (crystalBlue) 106 = olive green (springGreen)
|
||||||
|
# 139 = medium purple (oniViolet) 173 = tan (boatYellow)
|
||||||
|
# 167 = indian red (peachRed) 66 = cadet blue (waveAqua)
|
||||||
|
ZSH_HIGHLIGHT_STYLES[default]='none'
|
||||||
|
ZSH_HIGHLIGHT_STYLES[unknown-token]='fg=167,bold'
|
||||||
|
ZSH_HIGHLIGHT_STYLES[reserved-word]='fg=139'
|
||||||
|
ZSH_HIGHLIGHT_STYLES[command]='fg=110'
|
||||||
|
ZSH_HIGHLIGHT_STYLES[builtin]='fg=110'
|
||||||
|
ZSH_HIGHLIGHT_STYLES[function]='fg=110'
|
||||||
|
ZSH_HIGHLIGHT_STYLES[alias]='fg=106'
|
||||||
|
ZSH_HIGHLIGHT_STYLES[path]='fg=66'
|
||||||
|
ZSH_HIGHLIGHT_STYLES[single-quoted-argument]='fg=173'
|
||||||
|
ZSH_HIGHLIGHT_STYLES[double-quoted-argument]='fg=173'
|
||||||
|
ZSH_HIGHLIGHT_STYLES[comment]='fg=242'
|
||||||
|
ZSH_HIGHLIGHT_STYLES[globbing]='fg=139'
|
||||||
|
|
||||||
|
# ── Autosuggestion style ──────────────────────────────────────────────────────
|
||||||
|
ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=242'
|
||||||
|
ZSH_AUTOSUGGEST_STRATEGY=(history completion)
|
||||||
|
ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=20
|
||||||
|
|
||||||
|
# ── Smart Tools ───────────────────────────────────────────────────────────────
|
||||||
|
eval "$(zoxide init zsh)"
|
||||||
|
source <(fzf --zsh)
|
||||||
|
|
||||||
|
# FZF — 256-color theme
|
||||||
|
export FZF_DEFAULT_OPTS="
|
||||||
|
--color=bg+:236,bg:234,spinner:139,hl:173
|
||||||
|
--color=fg:250,header:242,info:110,pointer:167
|
||||||
|
--color=marker:139,fg+:250,prompt:110,hl+:167
|
||||||
|
--border=rounded --prompt='❯ ' --pointer='▶' --marker='✔'
|
||||||
|
"
|
||||||
|
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
|
||||||
|
|
||||||
|
# ── Mobile Detection ──────────────────────────────────────────────────────────
|
||||||
|
if [[ "${MOBILE:-0}" == "1" ]]; then
|
||||||
|
export STARSHIP_CONFIG="$HOME/.config/starship-mobile.toml"
|
||||||
|
alias mux='screen -RD'
|
||||||
|
alias muxls='screen -ls'
|
||||||
|
if [[ -z "$STY" && -z "$TMUX" ]]; then
|
||||||
|
screen -RD arch-dev 2>/dev/null || screen -S arch-dev
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
export STARSHIP_CONFIG="$HOME/.config/starship.toml"
|
||||||
|
alias mux='tmux new-session -A -s main'
|
||||||
|
alias muxls='tmux ls'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Prompt ────────────────────────────────────────────────────────────────────
|
||||||
|
eval "$(starship init zsh)"
|
||||||
|
|
||||||
|
# ── Aliases ───────────────────────────────────────────────────────────────────
|
||||||
|
source ~/.aliases
|
||||||
|
|
||||||
|
# ── arch-dev snapshot system ──────────────────────────────────────────────────
|
||||||
|
# Git-backed rollback for /home/dev. State lives in ~/.arch-dev-state/
|
||||||
|
# (separate git dir so it doesn't conflict with project git repos).
|
||||||
|
export ARCH_DEV_GITDIR="$HOME/.arch-dev-state"
|
||||||
|
|
||||||
|
_archdev_git() {
|
||||||
|
git --git-dir="$ARCH_DEV_GITDIR" --work-tree="$HOME" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Take a named snapshot
|
||||||
|
snapshot() {
|
||||||
|
if [[ -z "$1" ]]; then
|
||||||
|
echo "usage: snapshot <name> [message]"
|
||||||
|
echo " e.g. snapshot node-working 'NodeJS environment with nvm + pnpm'"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local name="$1"
|
||||||
|
local msg="${2:-snapshot: $name}"
|
||||||
|
_archdev_git add -A
|
||||||
|
_archdev_git commit -q -m "$msg" || { echo "nothing to snapshot"; return 1; }
|
||||||
|
_archdev_git tag -f "$name"
|
||||||
|
echo "✓ snapshot '$name' saved"
|
||||||
|
}
|
||||||
|
|
||||||
|
# List snapshots
|
||||||
|
snapshots() {
|
||||||
|
echo "── arch-dev snapshots ──"
|
||||||
|
_archdev_git tag -l --format=' %(refname:short) %(taggerdate:short) %(subject)'
|
||||||
|
echo ""
|
||||||
|
echo "── recent commits ──"
|
||||||
|
_archdev_git log --oneline -10
|
||||||
|
}
|
||||||
|
|
||||||
|
# Roll back to a snapshot
|
||||||
|
rollback() {
|
||||||
|
if [[ -z "$1" ]]; then
|
||||||
|
echo "usage: rollback <name>"
|
||||||
|
snapshots
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local name="$1"
|
||||||
|
if ! _archdev_git rev-parse "$name" >/dev/null 2>&1; then
|
||||||
|
echo "✗ snapshot '$name' not found"
|
||||||
|
snapshots
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "⚠ rolling back to '$name' will discard uncommitted changes"
|
||||||
|
echo -n " proceed? [y/N] "
|
||||||
|
read -r reply
|
||||||
|
[[ "$reply" =~ ^[Yy]$ ]] || { echo "cancelled"; return 1; }
|
||||||
|
|
||||||
|
# Auto-snapshot current state before rollback
|
||||||
|
_archdev_git add -A 2>/dev/null
|
||||||
|
_archdev_git commit -q -m "auto: pre-rollback $(date +%Y%m%d-%H%M%S)" 2>/dev/null || true
|
||||||
|
|
||||||
|
_archdev_git reset --hard "$name"
|
||||||
|
echo "✓ rolled back to '$name'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# What's changed since last snapshot
|
||||||
|
diff-state() {
|
||||||
|
_archdev_git status --short
|
||||||
|
echo ""
|
||||||
|
_archdev_git diff --stat HEAD
|
||||||
|
}
|
||||||
|
|
||||||
|
# Delete a snapshot
|
||||||
|
unsnapshot() {
|
||||||
|
if [[ -z "$1" ]]; then
|
||||||
|
echo "usage: unsnapshot <name>"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
_archdev_git tag -d "$1" && echo "✓ snapshot '$1' removed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show what's in a snapshot
|
||||||
|
show-snapshot() {
|
||||||
|
if [[ -z "$1" ]]; then
|
||||||
|
echo "usage: show-snapshot <name>"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
_archdev_git show --stat "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Key Bindings ──────────────────────────────────────────────────────────────
|
||||||
|
bindkey -e
|
||||||
|
bindkey '^[[1;5C' forward-word
|
||||||
|
bindkey '^[[1;5D' backward-word
|
||||||
|
|
||||||
|
# ── nvm (lazy-loaded for fast shell startup) ──────────────────────────────────
|
||||||
|
export NVM_DIR="$HOME/.nvm"
|
||||||
|
if [[ -s "$NVM_DIR/nvm.sh" ]]; then
|
||||||
|
# Add default node to PATH immediately so node CLIs work pre-load
|
||||||
|
if [[ -f "$NVM_DIR/alias/default" ]]; then
|
||||||
|
_def="$(cat "$NVM_DIR/alias/default" 2>/dev/null)"
|
||||||
|
[[ -d "$NVM_DIR/versions/node/v$_def/bin" ]] && \
|
||||||
|
export PATH="$NVM_DIR/versions/node/v$_def/bin:$PATH"
|
||||||
|
fi
|
||||||
|
# Lazy-load full nvm on first explicit use
|
||||||
|
nvm() {
|
||||||
|
unset -f nvm
|
||||||
|
. "$NVM_DIR/nvm.sh"
|
||||||
|
nvm "$@"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Roaming ~ (v3) ────────────────────────────────────────────────────────────
|
||||||
|
# Fleet-wide remote control + sshfs mounts over the tailnet.
|
||||||
|
# Registry: ~/.config/roaming/hosts Add hosts: add_new_machine.sh
|
||||||
|
[[ -f "$HOME/.config/roaming/roaming.zsh" ]] && source "$HOME/.config/roaming/roaming.zsh"
|
||||||
83
entrypoint.sh
Normal file
83
entrypoint.sh
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
# ║ arch-dev :: entrypoint ║
|
||||||
|
# ║ Seeds /home/dev from skel template on first run. ║
|
||||||
|
# ║ Updates dotfiles when image is newer than home. ║
|
||||||
|
# ║ Initializes git for snapshot/rollback functionality. ║
|
||||||
|
# ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SKEL=/etc/skel-arch-dev
|
||||||
|
HOME_DIR=/home/dev
|
||||||
|
MARKER="$HOME_DIR/.arch-dev-initialized"
|
||||||
|
GITDIR="$HOME_DIR/.arch-dev-state"
|
||||||
|
|
||||||
|
# ── First run: seed everything from skel ──────────────────────────────────────
|
||||||
|
if [[ ! -f "$MARKER" ]]; then
|
||||||
|
echo "🌱 arch-dev: first run — seeding home from skeleton..."
|
||||||
|
# Copy everything except things that should not exist yet
|
||||||
|
rsync -a --chown=dev:dev "$SKEL/" "$HOME_DIR/"
|
||||||
|
|
||||||
|
# Initialize git repo for snapshots
|
||||||
|
cd "$HOME_DIR"
|
||||||
|
git init --quiet --separate-git-dir="$GITDIR" .
|
||||||
|
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" config user.name "arch-dev"
|
||||||
|
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" config user.email "dev@arch-dev.local"
|
||||||
|
|
||||||
|
# Write .gitignore to exclude noise
|
||||||
|
cat > "$HOME_DIR/.gitignore" <<'GITIGNORE'
|
||||||
|
# Caches and logs
|
||||||
|
.cache/
|
||||||
|
.local/state/
|
||||||
|
.local/share/nvim/log
|
||||||
|
.local/share/nvim/lazy-lock.json.bak
|
||||||
|
.npm/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# History (intentional — too noisy to track)
|
||||||
|
.zsh_history
|
||||||
|
.lesshst
|
||||||
|
.bash_history
|
||||||
|
.python_history
|
||||||
|
|
||||||
|
# Compiled neovim plugin caches
|
||||||
|
.local/share/nvim/lazy/*/plugin/packer_compiled.lua
|
||||||
|
|
||||||
|
# Trash
|
||||||
|
.local/share/Trash/
|
||||||
|
GITIGNORE
|
||||||
|
|
||||||
|
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" add -A
|
||||||
|
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" commit -q -m "initial: arch-dev skeleton baked"
|
||||||
|
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" tag -a "skeleton" -m "factory state"
|
||||||
|
|
||||||
|
touch "$MARKER"
|
||||||
|
echo "✓ arch-dev: home initialized with snapshot 'skeleton'"
|
||||||
|
|
||||||
|
# ── Subsequent runs: refresh dotfiles only if image is newer ──────────────────
|
||||||
|
else
|
||||||
|
SKEL_TIME=$(stat -c %Y "$SKEL/.zshrc" 2>/dev/null || echo 0)
|
||||||
|
HOME_TIME=$(stat -c %Y "$HOME_DIR/.zshrc" 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if [[ "$SKEL_TIME" -gt "$HOME_TIME" ]]; then
|
||||||
|
echo "🔄 arch-dev: image dotfiles newer than home — updating..."
|
||||||
|
# Auto-snapshot before updating, in case user wants to revert
|
||||||
|
if [[ -d "$GITDIR" ]]; then
|
||||||
|
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" add -A 2>/dev/null
|
||||||
|
git --git-dir="$GITDIR" --work-tree="$HOME_DIR" \
|
||||||
|
commit -q -m "auto: pre-update snapshot $(date +%Y%m%d-%H%M%S)" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
# Refresh only the dotfiles, not user data
|
||||||
|
rsync -a --chown=dev:dev \
|
||||||
|
--exclude='.local/share/nvim/lazy' \
|
||||||
|
--exclude='.cache' \
|
||||||
|
"$SKEL/" "$HOME_DIR/"
|
||||||
|
echo "✓ arch-dev: dotfiles updated (auto-snapshot taken first)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Hand off ──────────────────────────────────────────────────────────────────
|
||||||
|
cd "$HOME_DIR"
|
||||||
|
exec "$@"
|
||||||
33
nvm.zsh.snippet
Normal file
33
nvm.zsh.snippet
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# ── nvm (added in v2.0) ───────────────────────────────────────────────────────
|
||||||
|
# Lazy-load nvm to keep shell startup fast
|
||||||
|
export NVM_DIR="$HOME/.nvm"
|
||||||
|
if [[ -s "$NVM_DIR/nvm.sh" ]]; then
|
||||||
|
# Stub functions that load nvm on first use
|
||||||
|
nvm() {
|
||||||
|
unset -f nvm node npm npx
|
||||||
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
|
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
||||||
|
nvm "$@"
|
||||||
|
}
|
||||||
|
node() {
|
||||||
|
unset -f nvm node npm npx
|
||||||
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
|
node "$@"
|
||||||
|
}
|
||||||
|
npm() {
|
||||||
|
unset -f nvm node npm npx
|
||||||
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
|
npm "$@"
|
||||||
|
}
|
||||||
|
npx() {
|
||||||
|
unset -f nvm node npm npx
|
||||||
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
|
npx "$@"
|
||||||
|
}
|
||||||
|
# Add default node bin to PATH so node-installed CLIs work without lazy trigger
|
||||||
|
if [[ -d "$NVM_DIR/alias" ]] && [[ -f "$NVM_DIR/alias/default" ]]; then
|
||||||
|
DEFAULT_NODE="$(cat "$NVM_DIR/alias/default" 2>/dev/null)"
|
||||||
|
[[ -d "$NVM_DIR/versions/node/v$DEFAULT_NODE/bin" ]] && \
|
||||||
|
PATH="$NVM_DIR/versions/node/v$DEFAULT_NODE/bin:$PATH"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
4
roaming-zshrc.snippet
Normal file
4
roaming-zshrc.snippet
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# ── Roaming ~ (v3) ────────────────────────────────────────────────────────────
|
||||||
|
# Fleet-wide remote control + sshfs mounts over the tailnet.
|
||||||
|
# Registry: ~/.config/roaming/hosts Add hosts: add_new_machine.sh
|
||||||
|
[[ -f "$HOME/.config/roaming/roaming.zsh" ]] && source "$HOME/.config/roaming/roaming.zsh"
|
||||||
76
scripts/add_new_machine.sh
Executable file
76
scripts/add_new_machine.sh
Executable file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ╔══════════════════════════════════════════════════════════════════╗
|
||||||
|
# ║ arch-dev roaming :: add_new_machine ║
|
||||||
|
# ║ Onboard a new host into the roaming fleet: ║
|
||||||
|
# ║ 1. push your SSH key (ssh-copy-id) ║
|
||||||
|
# ║ 2. register it in ~/.config/roaming/hosts ║
|
||||||
|
# ║ 3. optionally test the connection + mount ║
|
||||||
|
# ╚══════════════════════════════════════════════════════════════════╝
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROAMING_DIR="${ROAMING_DIR:-$HOME/.config/roaming}"
|
||||||
|
ROAMING_HOSTS="${ROAMING_HOSTS:-$ROAMING_DIR/hosts}"
|
||||||
|
ROAMING_DEFAULT_USER="${ROAMING_DEFAULT_USER:-$USER}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: add_new_machine.sh <name> <fqdn> [user] [mount]
|
||||||
|
|
||||||
|
name short alias for the host (e.g. mail)
|
||||||
|
fqdn reachable hostname (e.g. mail.tailnet.ts)
|
||||||
|
user ssh user (default: $ROAMING_DEFAULT_USER)
|
||||||
|
mount remote path to expose via sshfs (optional, e.g. / or /srv)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
add_new_machine.sh mail mail.tailnet.ts root /
|
||||||
|
add_new_machine.sh proxy proxy.tailnet.ts admin /etc/caddy
|
||||||
|
add_new_machine.sh dev dev-server.tailnet.ts youruser
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ $# -lt 2 ]] && usage
|
||||||
|
|
||||||
|
NAME="$1"
|
||||||
|
FQDN="$2"
|
||||||
|
USER_ARG="${3:-$ROAMING_DEFAULT_USER}"
|
||||||
|
MOUNT="${4:-}"
|
||||||
|
|
||||||
|
mkdir -p "$ROAMING_DIR"
|
||||||
|
touch "$ROAMING_HOSTS"
|
||||||
|
|
||||||
|
# ── Guard: already registered? ────────────────────────────────────────────────
|
||||||
|
if grep -qE "^\s*${NAME}\s" "$ROAMING_HOSTS" 2>/dev/null; then
|
||||||
|
echo "✗ '$NAME' is already in $ROAMING_HOSTS"
|
||||||
|
echo " edit the file directly to change it"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Step 1: SSH key ───────────────────────────────────────────────────────────
|
||||||
|
echo "── Step 1: pushing SSH key to ${USER_ARG}@${FQDN} ──"
|
||||||
|
echo " (you'll be prompted for the remote password once)"
|
||||||
|
if ssh-copy-id "${USER_ARG}@${FQDN}"; then
|
||||||
|
echo " ✓ key installed"
|
||||||
|
else
|
||||||
|
echo " ✗ ssh-copy-id failed — aborting, nothing registered"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Step 2: register ──────────────────────────────────────────────────────────
|
||||||
|
echo "── Step 2: registering in $ROAMING_HOSTS ──"
|
||||||
|
printf "%-10s %-26s %-8s %s\n" "$NAME" "$FQDN" "$USER_ARG" "$MOUNT" >> "$ROAMING_HOSTS"
|
||||||
|
echo " ✓ registered"
|
||||||
|
|
||||||
|
# ── Step 3: verify ────────────────────────────────────────────────────────────
|
||||||
|
echo "── Step 3: verifying connection ──"
|
||||||
|
if ssh -o BatchMode=yes -o ConnectTimeout=8 "${USER_ARG}@${FQDN}" 'echo ok' >/dev/null 2>&1; then
|
||||||
|
echo " ✓ key-based SSH works"
|
||||||
|
else
|
||||||
|
echo " ⚠ key-based SSH test failed (host may still be fine — check manually)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Done. In a new shell (or after 'roaming-reload'):"
|
||||||
|
echo " $NAME # interactive shell"
|
||||||
|
echo " $NAME sudo systemctl status # run a command"
|
||||||
|
[[ -n "$MOUNT" ]] && echo " mount_host $NAME # sshfs ${MOUNT} → /workspace/$NAME"
|
||||||
Loading…
Reference in New Issue
Block a user