42 changed files with 2485 additions and 0 deletions
@ -0,0 +1,418 @@
|
||||
# shell-inits - Repository Plan |
||||
|
||||
One-command bootstrap of fully configured shell environments on fresh VMs and in |
||||
Docker containers, plus pre-baked Docker images. |
||||
|
||||
``` |
||||
# on any fresh VM (ubuntu 24.04/26.04, alpine 3.21): |
||||
curl -fsSL https://git.example.com/shell-inits/raw/main/sets/zsh-tmux-neovim.sh | bash |
||||
|
||||
# pre-baked image: |
||||
./dockerfiles/zsh-tmux-neovim.build.sh # -> local image |
||||
docker run -it shell-inits/zsh-tmux-neovim:ubuntu-24.04 # lands in tmux session 'main' |
||||
``` |
||||
|
||||
## 1. Goals |
||||
|
||||
1. Single command to get a working, configured environment on a new VM or in a |
||||
running container (tmux, zsh+fzf, lazyvim+lazygit, custom yazi build, ...). |
||||
2. Distro-agnostic entry points: same URL works on `ubuntu:24.04`, |
||||
`ubuntu:26.04`, `alpine:3.21` (auto-detected). |
||||
3. Layered composition: atomic core initializers, combined into toolsets, |
||||
reusable in Dockerfiles. |
||||
4. Pre-built, fully initialized Docker images (nvim plugins already synced, |
||||
tmux plugins installed) that need no network at runtime. A private Docker |
||||
registry will distribute them in the future; until then `build.sh` only |
||||
builds locally. |
||||
5. Source portability: scripts/configs can be served from GitHub, a private git |
||||
server, or a company internal HTTP server without changing any code. |
||||
6. Trivially extensible: adding a new tool, a new toolset, or a new distro |
||||
should each be a small, localized addition. |
||||
|
||||
Non-goals (for now): managing dotfiles unrelated to these tools, secret |
||||
management, multi-user provisioning, X/GUI tooling. |
||||
|
||||
## 2. Design principles |
||||
|
||||
- **Layered**: core (one tool) -> set (composition) -> image (docker). Each |
||||
layer only knows the one below it through stable contracts. |
||||
- **Idempotent**: every script can be re-run safely; already-satisfied steps |
||||
are skipped based on state markers. |
||||
- **Same URL everywhere**: distro differences never leak into entry points; |
||||
they live in `lib/distro.sh` + `lib/pkgmaps/`. |
||||
- **Everything is a parameter**: base URL, uid, force/unattended flags are env |
||||
vars/flags with sane defaults; nothing is hardcoded to one host. |
||||
- **Plain shell only**: bash + curl/wget. No build step, no dependencies beyond |
||||
what the target distro provides or the scripts themselves install. |
||||
- **Simple over featureful**: no zsh plugin manager, no elaborate framework. |
||||
These environments exist to get jobs done on remote servers - well-configured |
||||
and nice looking, with minimal moving parts. |
||||
- **Fail loudly and early**: `set -euo pipefail` everywhere, unsupported distros |
||||
abort with a clear message listing supported ones. |
||||
|
||||
## 3. Architecture |
||||
|
||||
``` |
||||
+--------------------------------------------+ |
||||
| lib/ (shared runtime, fetched on demand) | |
||||
| common, distro, pkg, fetch, asset, state | |
||||
+----------------------+---------------------+ |
||||
| |
||||
Layer 1: core/ Layer 2: sets/ Layer 3: dockerfiles/ |
||||
one tool per script ordered composition <name>.dockerfile |
||||
tmux, zsh, fzf, neovim, --> zsh-fzf, zsh-tmux, --> <name>.build.sh |
||||
lazygit, yazi ... zsh-tmux-neovim ... (http server + build) |
||||
``` |
||||
|
||||
- `core/*.sh` can be run standalone (`curl BASE/core/tmux.sh | bash`) or as |
||||
building blocks. |
||||
- `sets/*.sh` are thin, self-bootstrapping wrappers that declare an ordered |
||||
component list and delegate to a shared runner in `lib/`. Curl-able directly, |
||||
so each toolset keeps a stable URL. |
||||
- `dockerfiles/*` build images by running one set script inside the build, then |
||||
executing headless bootstraps (nvim `Lazy! sync`, tmux plugin install, ...) so |
||||
images work fully offline at runtime. |
||||
|
||||
## 4. Directory tree |
||||
|
||||
``` |
||||
shell-inits/ |
||||
├── README.md # usage, quickstart, supported matrix |
||||
├── PLAN.md # this document |
||||
├── Makefile # convenience: serve, build SET=.. DISTRO=.., test |
||||
├── versions.env # versions of tools fetched as release binaries |
||||
│ # (only where pinning is free; see 8.4) |
||||
│ |
||||
├── lib/ # shared shell library (sourced by every script) |
||||
│ ├── common.sh # strict mode, logging (log/warn/die), arg parsing, |
||||
│ │ # sudo/root abstraction (as_root), HOME resolution |
||||
│ │ # (honors SUDO_USER), BASE_URL resolution |
||||
│ ├── distro.sh # /etc/os-release -> DISTRO_SLUG (ubuntu-24.04, |
||||
│ │ # ubuntu-26.04, alpine-3.21); unsupported -> abort |
||||
│ ├── pkg.sh # pkg_update / pkg_install dispatching to apt|apk |
||||
│ ├── pkgmaps/ # per-distro package name tables (abstract -> real) |
||||
│ │ ├── ubuntu-24.04.sh |
||||
│ │ ├── ubuntu-26.04.sh |
||||
│ │ └── alpine-3.21.sh |
||||
│ ├── fetch.sh # fetch <relpath> [dest]: curl|wget fallback, |
||||
│ │ # local-checkout mode, optional checksum verify |
||||
│ ├── asset.sh # install GitHub release binaries, arch-aware |
||||
│ │ # (x86_64/aarch64), musl vs glibc asset selection |
||||
│ ├── state.sh # idempotency: mark/query component versions in |
||||
│ │ # ~/.local/state/shell-inits/ |
||||
│ └── run-set.sh # shared set runner (executes COMPONENTS in order, |
||||
│ # applies post hooks, prints summary) |
||||
│ |
||||
├── core/ # LAYER 1: one atomic initializer per tool |
||||
│ ├── tmux.sh # tmux + config (plugin-free pastel theme, |
||||
│ │ # vi copy mode, OSC52 clipboard over ssh) |
||||
│ ├── zsh.sh # zsh + config + vendored plugins via plain |
||||
│ │ # git clones, no plugin manager |
||||
│ # (autosuggestions, syntax-highlighting) |
||||
│ │ # prompt: cached git info, big-repo safe |
||||
│ ├── fzf.sh # fzf (distro pkg or release binary) |
||||
│ ├── neovim.sh # nvim + lazyvim starter + personal config layer |
||||
│ ├── lazygit.sh # lazygit binary + config |
||||
│ └── yazi.sh # custom yazi build: prebuilt release by default, |
||||
│ # SHELL_INITS_BUILD_FROM_SOURCE=1 -> cargo build |
||||
│ |
||||
├── sets/ # LAYER 2: toolsets (stable curl-able entry points) |
||||
│ ├── zsh-fzf.sh |
||||
│ ├── zsh-tmux.sh |
||||
│ ├── zsh-tmux-neovim.sh # + lazyvim + lazygit |
||||
│ └── zsh-tmux-neovim-yazi.sh # the "everything" example |
||||
│ |
||||
├── config/ # config payloads installed by core scripts |
||||
│ │ # (single source of truth: this repo) |
||||
│ ├── tmux/ # tmux.conf, tpm plugin list |
||||
│ ├── zsh/ # .zshrc, .zshenv, prompt, plugin list |
||||
│ ├── nvim/ # lazyvim starter + lua/ personal layer |
||||
│ ├── lazygit/ # config.yml |
||||
│ └── yazi/ # yazi.toml, keymap.toml, theme.toml |
||||
│ |
||||
├── dockerfiles/ # LAYER 3: image targets (set <-> dockerfile 1:1) |
||||
│ ├── _template.dockerfile # copy & edit to add a new image target |
||||
│ ├── _template.build.sh |
||||
│ ├── entrypoint.sh # shared entrypoint: exec tmux new -As main |
||||
│ ├── zsh-tmux.dockerfile |
||||
│ ├── zsh-tmux.build.sh |
||||
│ ├── zsh-tmux-neovim.dockerfile |
||||
│ ├── zsh-tmux-neovim.build.sh |
||||
│ └── ... |
||||
│ |
||||
└── tests/ # see section 10 for the strategy |
||||
├── run.sh # builds target image(s) + runs asserts, distro matrix |
||||
├── core/ # one dockerfile+test per core initializer |
||||
│ ├── tmux.dockerfile # minimal image: distro + ONE core initializer |
||||
│ ├── tmux.test.sh # asserts only that tool's outcomes |
||||
│ ├── zsh.dockerfile / zsh.test.sh |
||||
│ ├── fzf.dockerfile / fzf.test.sh |
||||
│ ├── neovim.dockerfile / neovim.test.sh |
||||
│ ├── lazygit.dockerfile / lazygit.test.sh |
||||
│ └── yazi.dockerfile / yazi.test.sh |
||||
└── sets/ # ONLY sets with steps beyond the sum of cores |
||||
├── zsh-tmux-neovim.dockerfile |
||||
└── zsh-tmux-neovim.test.sh # asserts only set extras (plugin sync, session boot) |
||||
``` |
||||
|
||||
## 5. Layer 1: core initializers (contract) |
||||
|
||||
Every `core/<tool>.sh`: |
||||
|
||||
1. Starts with the standard preamble (see 8.1): resolves BASE_URL, ensures a |
||||
downloader exists, fetches and sources `lib/common.sh`, `lib/distro.sh`, |
||||
`lib/pkg.sh`, `lib/fetch.sh`, `lib/state.sh`. |
||||
2. Declares its contract in a header comment: what it installs, env vars it |
||||
honors, which distros need special handling. |
||||
3. Is idempotent: checks state markers first, skips if satisfied unless |
||||
`SHELL_INITS_FORCE=1`. |
||||
4. Declares dependencies via `ensure_core <other>` (e.g. `zsh.sh` ensures |
||||
`fzf.sh`; `neovim.sh` ensures `lazygit.sh` as a lazyvim dependency) so |
||||
standalone execution always converges. |
||||
5. Installs distro-specific bits behind `case "$DISTRO_SLUG"` guards: |
||||
- `fzf`: apt/apk package where new enough, otherwise release binary. |
||||
- `neovim`: release binary or distro package. |
||||
- `yazi`: release binary (musl/glibc asset per distro, arch-aware) or |
||||
optional from-source cargo build. |
||||
6. Deploys config from `${BASE_URL}/config/<tool>/` (configs live in this repo, |
||||
no external dotfiles source) into the target user's home, never root's home |
||||
when invoked with sudo. |
||||
7. Ends with `state_mark <tool> <version>` and a one-line summary. |
||||
|
||||
## 6. Layer 2: sets / toolsets |
||||
|
||||
`sets/<name>.sh` is a thin curl-able wrapper with the same preamble, then: |
||||
|
||||
```sh |
||||
SET_NAME="zsh-tmux-neovim" |
||||
COMPONENTS=(fzf zsh tmux lazygit neovim) # order matters |
||||
# optional: SET_POSTHOOKS=(set_default_shell print_next_steps) |
||||
source_run_set # fetches lib/run-set.sh and executes COMPONENTS |
||||
``` |
||||
|
||||
- The runner reuses core state markers, so sets over already-partially |
||||
provisioned machines only fill gaps. |
||||
- Post hooks handle cross-cutting finishers: `chsh -s zsh`, shell-completion |
||||
warmup, motd/summary with next steps. |
||||
- Naming convention: components joined with `-` (`zsh-tmux-neovim`). The set |
||||
name doubles as the docker image target name in `dockerfiles/`. |
||||
|
||||
## 7. Layer 3: docker images |
||||
|
||||
Every image target is a pair (exact naming as requested): |
||||
|
||||
- `dockerfiles/<set>.dockerfile` |
||||
- `dockerfiles/<set>.build.sh` |
||||
|
||||
### 7.1 Dockerfile sketch (template) |
||||
|
||||
```dockerfile |
||||
ARG DISTRO=ubuntu-24.04 |
||||
FROM ${DISTRO} # ubuntu:24.04 | ubuntu:26.04 | alpine:3.21 |
||||
ARG SHELL_INITS_BASE_URL |
||||
ARG USERNAME=dev # non-root user, never root-only images |
||||
ARG USER_UID=1000 # owner of everything the build produces |
||||
ARG USER_GID=1000 # override for company setups (1001, ...) |
||||
# 1. downloader + ca-certificates (apt or apk depending on DISTRO) |
||||
# 2. create group/user with USER_UID:USER_GID + sudo/doas equivalent |
||||
# 3. RUN curl -fsSL ${SHELL_INITS_BASE_URL}/sets/<set>.sh | \ |
||||
# SHELL_INITS_BASE_URL=${SHELL_INITS_BASE_URL} bash (as USERNAME) |
||||
# 4. headless bootstraps (as USERNAME) so the image is fully initialized: |
||||
# nvim --headless "+Lazy! sync" +qa |
||||
# tmux new-session -d '~/.tmux/plugins/tpm/bin/install_plugins' || true |
||||
# yazi --version; lazygit --version # warm first-run dirs |
||||
# 5. cleanup package caches to slim the image |
||||
COPY dockerfiles/entrypoint.sh /usr/local/bin/shell-inits-entrypoint |
||||
USER ${USERNAME} |
||||
WORKDIR /home/${USERNAME} |
||||
ENTRYPOINT ["/usr/local/bin/shell-inits-entrypoint"] # exec tmux new -As main |
||||
``` |
||||
|
||||
The UID contract: because the user and every file in its home are created with |
||||
`USER_UID`/`USER_GID` at build time, anything the container later produces |
||||
(volumes, mounted workspaces, files shared with the host) is owned by the uid |
||||
the image was built for. |
||||
|
||||
### 7.2 build.sh responsibilities |
||||
|
||||
1. Resolve repo root; refuse to run outside a checkout. |
||||
2. Start a local HTTP server on a free port serving the repo root |
||||
(`python3 -m http.server <port>`), `trap` cleanup. |
||||
3. `docker build --network=host |
||||
--build-arg SHELL_INITS_BASE_URL=http://127.0.0.1:<port> |
||||
--build-arg USER_UID=<uid> --build-arg USER_GID=<gid> |
||||
--build-arg DISTRO=<arg or matrix>` (host networking avoids |
||||
`host.docker.internal` issues on Linux; documented fallback for |
||||
Docker Desktop). |
||||
4. `--uid N` flag (default 1000) so images can be baked for the user's real uid; |
||||
`--distro` repeatable, `--all` for the whole matrix. |
||||
5. Done: image is tagged locally as `shell-inits/<set>:<distro-slug>`. Print the |
||||
exact `docker run` command. No export step - a private registry will be added |
||||
later and publishing becomes a `--push` flag (naming is already |
||||
registry-ready). |
||||
|
||||
## 8. Cross-cutting mechanics |
||||
|
||||
### 8.1 Base URL resolution (portability core) |
||||
|
||||
Resolution order, evaluated in every entry script: |
||||
|
||||
1. `--base-url <url>` CLI arg (passed via `bash -s -- ...` when piped), |
||||
2. `SHELL_INITS_BASE_URL` env, |
||||
3. local-checkout autodetect: if the running script exists on disk, use repo |
||||
root directly (`fetch` then reads files instead of HTTP). This makes |
||||
`./sets/zsh-tmux.sh` work for development and inside `COPY`-based builds |
||||
without a server. |
||||
4. embedded `DEFAULT_BASE_URL` constant (set once per deployment of this repo; |
||||
points at the private git server raw endpoint by default). |
||||
|
||||
`lib/fetch.sh` normalizes the difference between "raw file root" URL shapes |
||||
(github raw, gitea/gitlab `raw/main`, plain http dir) with a small |
||||
`raw_url <base> <path>` helper; base is always stored as the raw root. Configs |
||||
are always fetched relative to BASE_URL from this repo's `config/`. |
||||
|
||||
### 8.2 Distro detection and package abstraction |
||||
|
||||
- `lib/distro.sh` parses `/etc/os-release` -> `DISTRO_SLUG`: |
||||
`ubuntu-24.04`, `ubuntu-26.04`, `alpine-3.21`. Unknown -> hard error listing |
||||
supported slugs and where to add a new `lib/pkgmaps/<slug>.sh`. |
||||
- `pkg_install <abstract...>` looks up `lib/pkgmaps/${DISTRO_SLUG}.sh` |
||||
(associative `PKG[abstract]=real-package`) and calls apt/apk. When a package |
||||
is missing or too old, core scripts fall back to `asset_install_github` |
||||
from `lib/asset.sh`. |
||||
- Alpine specifics handled in one place: musl asset selection, busybox wget |
||||
quirks, root-by-default (no sudo), openrc absence. |
||||
|
||||
### 8.3 Root vs user |
||||
|
||||
- Containers: run as root, config goes to the created user's home (with the |
||||
requested `USER_UID`/`USER_GID`). |
||||
- VMs: run as regular user; privileged ops go through `as_root` (sudo on |
||||
ubuntu, doas/su on alpine), config always lands in the invoking user's home |
||||
(via `SUDO_USER`), default shell switched for that user only. |
||||
|
||||
### 8.4 State, idempotency, versions |
||||
|
||||
- Markers: `~/.local/state/shell-inits/<tool>.ver` (content = installed |
||||
version) for user-scope, `/var/lib/shell-inits/` for system-scope. |
||||
- `SHELL_INITS_FORCE=1` reinstalls; `SHELL_INITS_SKIP` list excludes |
||||
components from a set run. |
||||
- Pinning is kept only where it costs nothing: `versions.env` pins just the |
||||
GitHub-release binaries that `asset.sh` downloads (where the version is a |
||||
natural part of the URL anyway). apt/apk packages are used as-is from the |
||||
distro - pinning those would add maintenance overhead for no real benefit. |
||||
|
||||
### 8.5 Logging and robustness |
||||
|
||||
- Uniform prefix `[[set|tool] distro]` per line; `SHELL_INITS_LOG` writes a |
||||
full transcript next to state markers. |
||||
- Unattended mode (`SHELL_INITS_UNATTENDED=1`, default inside docker builds) |
||||
suppresses prompts; interactive confirmations only for destructive steps. |
||||
|
||||
## 9. End-to-end flows |
||||
|
||||
### 9.1 Fresh VM (online) |
||||
|
||||
``` |
||||
curl -fsSL $BASE/sets/zsh-tmux-neovim.sh | bash -s -- --base-url $BASE |
||||
-> preamble resolves BASE_URL, ensures downloader, sources lib |
||||
-> distro detect (e.g. ubuntu-26.04) |
||||
-> for c in fzf zsh tmux lazygit neovim: fetch+run core/$c.sh (idempotent) |
||||
-> post hooks: chsh, summary + next steps |
||||
``` |
||||
|
||||
No args needed when the embedded DEFAULT_BASE_URL is correct; `--base-url` (or |
||||
env) covers company-internal mirrors without any code change. |
||||
|
||||
### 9.2 Inside a running container (online) |
||||
|
||||
Same command as 9.1; scripts detect root, skip sudo, install into root or |
||||
`SHELL_INITS_USER` if provided. |
||||
|
||||
### 9.3 Building images |
||||
|
||||
``` |
||||
./dockerfiles/zsh-tmux-neovim.build.sh --distro ubuntu-24.04 --distro alpine-3.21 --uid 1001 |
||||
-> local http.server serves repo (trap cleanup) |
||||
-> docker build --network=host (set script + headless bootstraps inside, |
||||
everything owned by uid 1001) |
||||
-> tagged shell-inits/zsh-tmux-neovim:ubuntu-24.04 etc., run command printed |
||||
``` |
||||
|
||||
### 9.4 Running pre-built images |
||||
|
||||
``` |
||||
docker run -it shell-inits/zsh-tmux-neovim:ubuntu-24.04 |
||||
-> entrypoint execs: tmux new -As main (attaches to or creates session 'main') |
||||
``` |
||||
|
||||
Plugins, configs, binaries: all already baked in; no network needed at runtime. |
||||
Until the registry exists, ad-hoc offline transfer with a manual `docker save` |
||||
is still possible - it is just not build.sh's job. |
||||
|
||||
### 9.5 Development / testing loop |
||||
|
||||
``` |
||||
./sets/zsh-tmux.sh # local mode, no HTTP server needed |
||||
make test-core TOOL=tmux # core assert for every distro |
||||
make test-set SET=zsh-tmux-neovim # set-specific extras only |
||||
``` |
||||
|
||||
## 10. Testing strategy |
||||
|
||||
Principle: **test each behavior exactly once, at the lowest layer that owns |
||||
it.** Overlapping set tests would re-verify what core tests already cover. |
||||
|
||||
- **Core tests (always, full distro matrix)**: `tests/core/<tool>.dockerfile` |
||||
builds a minimal image (base distro + that single core initializer, served |
||||
via the same local-http flow). `tests/core/<tool>.test.sh` asserts only that |
||||
tool's outcomes: binary present and runs (`tmux -V`, `zsh -c true`, |
||||
`nvim --headless +qa`, `lazygit --version`, `yazi --version`), config files |
||||
deployed into the right home, and correct file ownership. |
||||
- **Set tests (only sets with steps beyond the sum of their cores)**: |
||||
currently just `zsh-tmux-neovim`, because of the headless plugin |
||||
initialization. Its test asserts only the extras: lazyvim plugins exist under |
||||
`~/.local/share/nvim`, nvim starts clean headlessly, tmux session named |
||||
`main` boots via the entrypoint. It never re-checks tmux/zsh/fzf basics - if |
||||
those broke, the corresponding core test fails first. |
||||
- **Sets without extra steps** (e.g. `zsh-fzf`, `zsh-tmux`) get no image and no |
||||
test of their own; their correctness is composition, already implied by core |
||||
tests, and they remain curl-able for interactive use. |
||||
- Runner: `tests/run.sh <core|set>/<name> [--distro ...]` orchestrates build + |
||||
assert + cleanup for the whole matrix; Makefile wraps the common cases. |
||||
|
||||
## 11. Extensibility recipes |
||||
|
||||
- **New core initializer**: add `core/<tool>.sh` following the contract (5), |
||||
optionally `config/<tool>/`, pkgmap entries, a line in `versions.env` if it |
||||
uses release binaries, and `tests/core/<tool>.{dockerfile,test.sh}`. |
||||
Immediately usable standalone and from any set. |
||||
- **New toolset**: add `sets/<name>.sh` wrapper listing `COMPONENTS`; stable |
||||
URL appears automatically. Add a dockerfile pair / tests only if the set |
||||
performs extra initialization steps. |
||||
- **New image target**: copy `dockerfiles/_template.*` to `<set>.*`. |
||||
- **New distro**: add `lib/pkgmaps/<slug>.sh`, extend `lib/distro.sh` allow |
||||
list, add alpine-style special cases only where truly needed. |
||||
|
||||
## 12. Naming conventions summary |
||||
|
||||
| Thing | Pattern | Example | |
||||
|-------------------|-------------------------------------------|----------------------------------| |
||||
| distro slug | `<id>-<version>` | `ubuntu-24.04`, `alpine-3.21` | |
||||
| core script | `core/<tool>.sh` | `core/lazygit.sh` | |
||||
| core test | `tests/core/<tool>.{dockerfile,test.sh}` | `tests/core/yazi.test.sh` | |
||||
| set script | `sets/<components-joined>.sh` | `sets/zsh-tmux-neovim.sh` | |
||||
| set test | `tests/sets/<set>.{dockerfile,test.sh}` | only for sets with extra steps | |
||||
| dockerfile pair | `dockerfiles/<set>.{dockerfile,build.sh}` | `dockerfiles/zsh-tmux.build.sh` | |
||||
| image | `shell-inits/<set>:<distro-slug>` | `shell-inits/dev:alpine-3.21` | |
||||
| env prefix | `SHELL_INITS_*` | `SHELL_INITS_BASE_URL` | |
||||
|
||||
## 13. Future work |
||||
|
||||
- Private Docker registry: `build.sh --push` once the registry exists; image |
||||
naming (12) is already registry-ready. |
||||
- Async prompt git info (zle -F / zpty worker) instead of the current cached |
||||
synchronous approach; big-repo performance is already covered by |
||||
core.untrackedCache + core.fsmonitor. |
||||
- Multi-arch image builds (x86_64 + aarch64) if arm hosts or apple silicon |
||||
docker desktops become a target. |
||||
@ -0,0 +1,110 @@
|
||||
# shell-inits |
||||
|
||||
One-command bootstrap of a configured shell environment (tmux + zsh/fzf + |
||||
LazyVim/lazygit) on fresh VMs and containers. See [PLAN.md](PLAN.md) for the |
||||
full architecture. |
||||
|
||||
## Supported distros |
||||
|
||||
| distro | status | |
||||
|---------------|--------------| |
||||
| ubuntu-26.04 | implemented | |
||||
| ubuntu-24.04 | planned | |
||||
| alpine-3.21 | planned | |
||||
|
||||
## Usage |
||||
|
||||
On a fresh VM (as a regular user with sudo, or as root): |
||||
|
||||
```sh |
||||
curl -fsSL <base-url>/sets/zsh-tmux-neovim.sh | bash -s -- --base-url <base-url> |
||||
``` |
||||
|
||||
`<base-url>` is the raw-files root of this repository (private git server, |
||||
GitHub mirror or any HTTP server serving a checkout). Alternative forms: |
||||
|
||||
```sh |
||||
SHELL_INITS_BASE_URL=<base-url> curl -fsSL <base-url>/sets/zsh-tmux-neovim.sh | bash |
||||
# or from a checkout: |
||||
./sets/zsh-tmux-neovim.sh |
||||
``` |
||||
|
||||
Individual components can be installed the same way (`core/tmux.sh`, |
||||
`core/zsh.sh`, `core/fzf.sh`, `core/lazygit.sh`, `core/neovim.sh`). |
||||
|
||||
### Environment variables |
||||
|
||||
| var | effect | |
||||
|------------------------------|---------------------------------------------------| |
||||
| `SHELL_INITS_BASE_URL` | base URL of the scripts (raw-files root) | |
||||
| `SHELL_INITS_USER` | configure this user when running as root (docker) | |
||||
| `SHELL_INITS_SKIP` | comma-separated components to skip (e.g. `tmux`) | |
||||
| `SHELL_INITS_FORCE` | reinstall even if state markers say "done" | |
||||
| `SHELL_INITS_NVIM_BOOTSTRAP` | `1` = sync nvim plugins headlessly right away | |
||||
|
||||
## Docker images |
||||
|
||||
Build a fully initialized image (nvim plugins synced at build time, works |
||||
offline at runtime) and drop straight into tmux session `main`: |
||||
|
||||
```sh |
||||
./dockerfiles/zsh-tmux-neovim.build.sh # -> shell-inits/zsh-tmux-neovim:ubuntu-26.04 |
||||
./dockerfiles/zsh-tmux-neovim.build.sh --uid 1001 # bake everything for uid 1001 |
||||
docker run -it shell-inits/zsh-tmux-neovim:ubuntu-26.04 |
||||
``` |
||||
|
||||
The image is fully initialized at build time (all lazyvim plugins, the |
||||
treesitter parser set compiled in, spell files and the tree-sitter CLI), so it |
||||
needs no network at runtime. For offline distribution: |
||||
|
||||
```sh |
||||
./dockerfiles/zsh-tmux-neovim.build.sh --save # also writes dist/zsh-tmux-neovim_ubuntu-26.04.tar.gz |
||||
docker load -i dist/zsh-tmux-neovim_ubuntu-26.04.tar.gz |
||||
``` |
||||
|
||||
The build script starts a temporary local HTTP server serving this repository |
||||
and builds with `--network=host`, so the image is produced by the exact same |
||||
`curl | bash` flow used on VMs. Useful options: `--distro <slug>` (repeatable), |
||||
`--all`, `--gid`, `--tag`, `--no-cache`, and `--base-url <url>` for remote |
||||
docker hosts or Docker Desktop (where `--network=host` does not work, serve the |
||||
repo yourself and pass `http://host.docker.internal:<port>`). |
||||
|
||||
Inside the container: non-root user `dev` with passwordless sudo, entrypoint |
||||
runs `tmux new -As main` (set `SHELL_INITS_NO_TMUX=1` for a plain zsh), and |
||||
because the user and all files are created with the requested uid/gid, anything |
||||
the container writes (mounted volumes included) matches your host user. |
||||
|
||||
### Clipboard sharing (no X11/Wayland needed) |
||||
|
||||
Copy/paste between the container/VM and your workstation uses the OSC 52 |
||||
terminal escape sequence, which travels through the same byte stream as the |
||||
terminal display - it works over ssh, `docker run -it` and inside tmux without |
||||
mounting any sockets. Requirements: |
||||
|
||||
- your terminal must support OSC 52 (kitty, wezterm, iTerm2, alacritty, |
||||
windows terminal do; kitty has it enabled by default), |
||||
- tmux runs with a UTF-8 locale (set automatically; an already-running tmux |
||||
server keeps its original environment - restart it after upgrading). |
||||
|
||||
How it works: |
||||
|
||||
- tmux copy mode: `y` copies into the tmux buffer (paste with `prefix ]`) and |
||||
writes the selection to your workstation clipboard via |
||||
`~/.config/tmux/osc52-copy.sh`, which talks to the client terminals directly, |
||||
- nvim: yanks go to the `+` register through neovim's built-in OSC 52 provider |
||||
(activated whenever no local clipboard tool exists); tmux forwards it. |
||||
|
||||
## What you get |
||||
|
||||
- **zsh**: two-line pastel prompt (`login@host ../end/of/path <branch>`), git |
||||
info cached per command (refreshed only after commands or directory changes) |
||||
and kept fast in huge repos via `core.untrackedCache` + `core.fsmonitor`, |
||||
autosuggestions, syntax highlighting, fzf bindings - no plugin manager, no |
||||
nerd fonts required. |
||||
- **tmux**: vi copy mode, clipboard shared with your workstation over ssh via |
||||
OSC 52, pastel (catppuccin macchiato) status theme, no plugins. |
||||
- **LazyVim**: catppuccin theme, snacks explorer tweaks, OSC52 clipboard over |
||||
ssh, 42 treesitter parsers + spell files precompiled during image builds |
||||
(works fully offline). Language-specific plugins are intentionally out of |
||||
scope for now. |
||||
- **lazygit**: release binary with matching pastel theme. |
||||
@ -0,0 +1,25 @@
|
||||
# shell-inits lazygit config - catppuccin macchiato pastel theme, no icons |
||||
gui: |
||||
showRandomTip: false |
||||
border: rounded |
||||
theme: |
||||
activeBorderColor: |
||||
- "#c6a0f6" # mauve |
||||
searchingActiveBorderColor: |
||||
- "#8aadf4" # blue |
||||
inactiveBorderColor: |
||||
- "#6e738d" # overlay0 |
||||
optionsTextColor: |
||||
- "#8aadf4" # blue |
||||
selectedLineBgColor: |
||||
- "#363a4f" # surface0 |
||||
selectedRangeBgColor: |
||||
- "#494d64" # surface1 |
||||
cherryPickedCommitBgColor: |
||||
- "#494d64" # surface1 |
||||
cherryPickedCommitFgColor: |
||||
- "#a6da95" # green |
||||
unstagedChangesColor: |
||||
- "#ed8796" # red |
||||
defaultFgColor: |
||||
- "#cad3f5" # text |
||||
@ -0,0 +1,26 @@
|
||||
-- shell-inits bootstrap.lua - headless initialization used by image builds |
||||
-- and SHELL_INITS_NVIM_BOOTSTRAP=1 (see core/neovim.sh). Lives outside lua/ |
||||
-- so it never loads during normal startup. Run AFTER "+Lazy! sync". |
||||
local parsers = require("config.treesitter_parsers") |
||||
|
||||
local ok_ts, ts = pcall(require, "nvim-treesitter") |
||||
if ok_ts then |
||||
pcall(ts.install, parsers) |
||||
end |
||||
|
||||
-- prebuilt parsers download asynchronously; wait (max 5 min) until every |
||||
-- language on the list is usable |
||||
local missing = #parsers |
||||
local timed_out = not vim.wait(480000, function() |
||||
missing = 0 |
||||
for _, lang in ipairs(parsers) do |
||||
if not pcall(vim.treesitter.language.inspect, lang) then |
||||
missing = missing + 1 |
||||
end |
||||
end |
||||
return missing == 0 |
||||
end, 3000) |
||||
|
||||
local found = #vim.api.nvim_get_runtime_file("parser/*.so", true) |
||||
print(("shell-inits bootstrap: %d/%d parsers ready, %d parser files in runtime, timed out: %s") |
||||
:format(#parsers - missing, #parsers, found, tostring(timed_out))) |
||||
@ -0,0 +1,3 @@
|
||||
-- shell-inits: early OSC52 clipboard setup must run before lazy.nvim |
||||
require("config.clipboard") |
||||
require("config.lazy") |
||||
@ -0,0 +1 @@
|
||||
-- shell-inits: autocmds on top of LazyVim defaults |
||||
@ -0,0 +1,53 @@
|
||||
-- shell-inits: OSC 52 clipboard whenever there is no local clipboard tool - |
||||
-- that covers ssh sessions (SSH_TTY) and containers/VMs without xclip/wl-copy. |
||||
-- |
||||
-- Inside tmux, copies are routed through ~/.config/tmux/osc52-copy.sh, which |
||||
-- writes the escape sequence to the attached client terminals directly: |
||||
-- tmux's own pane-OSC52 forwarding is unreliable across versions. Outside |
||||
-- tmux the sequence simply goes to stdout (the terminal itself). |
||||
local has_clipboard_tool = function() |
||||
local tools = { "xclip", "xsel", "wl-copy", "wl-paste", "pbcopy", "win32yank", "clip.exe" } |
||||
for _, tool in ipairs(tools) do |
||||
if vim.fn.executable(tool) == 1 then |
||||
return true |
||||
end |
||||
end |
||||
return false |
||||
end |
||||
|
||||
if vim.env.SSH_TTY or not has_clipboard_tool() then |
||||
local osc52 = require("vim.ui.clipboard.osc52") |
||||
local helper = vim.fn.expand("~/.config/tmux/osc52-copy.sh") |
||||
|
||||
local make_copy |
||||
if vim.env.TMUX and vim.fn.executable(helper) == 1 and vim.system then |
||||
make_copy = function() |
||||
return function(lines, _) |
||||
-- errors here would break yanking; the helper is best effort |
||||
pcall(function() |
||||
vim.system({ helper }, { stdin = table.concat(lines, "\n") }):wait() |
||||
end) |
||||
end |
||||
end |
||||
else |
||||
make_copy = function(reg) |
||||
return osc52.copy(reg) |
||||
end |
||||
end |
||||
|
||||
vim.g.clipboard = { |
||||
name = "OSC 52", |
||||
copy = { |
||||
["+"] = make_copy("+"), |
||||
["*"] = make_copy("*"), |
||||
}, |
||||
paste = { |
||||
-- paste needs the terminal to answer an OSC 52 query; unsupported |
||||
-- terminals just yield an empty paste. |
||||
["+"] = osc52.paste("+"), |
||||
["*"] = osc52.paste("*"), |
||||
}, |
||||
} |
||||
end |
||||
|
||||
vim.opt.clipboard = "unnamedplus" |
||||
@ -0,0 +1,2 @@
|
||||
-- shell-inits: keymaps on top of LazyVim defaults |
||||
-- (terminal keymaps are registered by lua/plugins/terminal.lua) |
||||
@ -0,0 +1,43 @@
|
||||
-- shell-inits: lazy.nvim bootstrap + LazyVim |
||||
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" |
||||
if not (vim.uv or vim.loop).fs_stat(lazypath) then |
||||
local lazyrepo = "https://github.com/folke/lazy.nvim.git" |
||||
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath }) |
||||
if vim.v.shell_error ~= 0 then |
||||
vim.api.nvim_echo({ |
||||
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" }, |
||||
{ out, "WarningMsg" }, |
||||
{ "\nPress any key to exit..." }, |
||||
}, true, {}) |
||||
vim.fn.getchar() |
||||
os.exit(1) |
||||
end |
||||
end |
||||
vim.opt.rtp:prepend(lazypath) |
||||
|
||||
require("lazy").setup({ |
||||
spec = { |
||||
{ "LazyVim/LazyVim", import = "lazyvim.plugins" }, |
||||
{ import = "plugins" }, |
||||
}, |
||||
defaults = { |
||||
lazy = false, |
||||
version = false, |
||||
}, |
||||
install = { colorscheme = { "catppuccin", "habamax" } }, |
||||
checker = { |
||||
enabled = true, |
||||
notify = false, |
||||
}, |
||||
performance = { |
||||
rtp = { |
||||
disabled_plugins = { |
||||
"gzip", |
||||
"tarPlugin", |
||||
"tohtml", |
||||
"tutor", |
||||
"zipPlugin", |
||||
}, |
||||
}, |
||||
}, |
||||
}) |
||||
@ -0,0 +1,32 @@
|
||||
-- shell-inits: options on top of LazyVim defaults |
||||
|
||||
-- sync clipboard with the + register by default (OSC52 over ssh, see clipboard.lua) |
||||
vim.opt.clipboard = "unnamedplus" |
||||
|
||||
-- spellcheck: polish + english, change runtime by :opt.spelllang = { "en_gb" } |
||||
vim.opt.spelllang = { "pl", "en" } |
||||
|
||||
-- do not hide markup (**, ``` etc) |
||||
vim.opt.conceallevel = 0 |
||||
|
||||
-- break lines at word boundaries |
||||
vim.opt.wrap = true |
||||
vim.opt.linebreak = true |
||||
|
||||
-- copy the indent of the current line when inserting a new line |
||||
vim.opt.autoindent = true |
||||
|
||||
-- disable inlay hints (they clutter the view) |
||||
vim.g.lazyvim_no_inlay_hints = true |
||||
|
||||
-- disable autoformatting not to generate large diffs |
||||
vim.g.autoformat = false |
||||
|
||||
-- list mode characters: dots for spaces/tabs and trailing spaces |
||||
vim.opt.listchars = { |
||||
tab = "· ", |
||||
trail = "·", |
||||
nbsp = "+", |
||||
extends = ">", |
||||
precedes = "<", |
||||
} |
||||
@ -0,0 +1,47 @@
|
||||
-- shell-inits: single source of truth for treesitter parsers to install. |
||||
-- LazyVim's default set; kept explicit so image builds can pre-install |
||||
-- exactly this list and nvim works fully offline. |
||||
return { |
||||
"bash", |
||||
"c", |
||||
"cpp", |
||||
"css", |
||||
"diff", |
||||
"dockerfile", |
||||
"gitcommit", |
||||
"go", |
||||
"gomod", |
||||
"gosum", |
||||
"gowork", |
||||
"gpg", |
||||
"html", |
||||
"http", |
||||
"ini", |
||||
"json", |
||||
"json5", |
||||
"lua", |
||||
"luadoc", |
||||
"luap", |
||||
"make", |
||||
"markdown", |
||||
"markdown_inline", |
||||
"nginx", |
||||
"passwd", |
||||
"pem", |
||||
"printf", |
||||
"python", |
||||
"query", |
||||
"readline", |
||||
"regex", |
||||
"requirements", |
||||
"rust", |
||||
"sql", |
||||
"toml", |
||||
"tsx", |
||||
"javascript", |
||||
"typescript", |
||||
"vim", |
||||
"vimdoc", |
||||
"xml", |
||||
"yaml", |
||||
} |
||||
@ -0,0 +1,28 @@
|
||||
-- shell-inits: catppuccin (macchiato) as the default colorscheme |
||||
return { |
||||
{ |
||||
"catppuccin/nvim", |
||||
name = "catppuccin", |
||||
lazy = false, |
||||
priority = 1000, |
||||
opts = { |
||||
flavour = "macchiato", |
||||
integrations = { |
||||
cmp = true, |
||||
gitsigns = true, |
||||
treesitter = true, |
||||
notify = true, |
||||
mini = true, |
||||
snacks = true, |
||||
telescope = { enabled = true }, |
||||
which_key = true, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
"LazyVim/LazyVim", |
||||
opts = { |
||||
colorscheme = "catppuccin", |
||||
}, |
||||
}, |
||||
} |
||||
@ -0,0 +1,34 @@
|
||||
-- shell-inits: subtle pastel rainbow indent guides, no scope line. |
||||
-- The IblIndentN groups are defined here (catppuccin does not ship them and |
||||
-- ibl refuses to start when they are missing). Static catppuccin macchiato |
||||
-- colors, re-applied on every colorscheme switch. |
||||
local define_highlights = function() |
||||
local palette = { "#8aadf4", "#c6a0f6", "#f5bde6", "#a6da95", "#eed49f", "#8bd5ca" } |
||||
for i, color in ipairs(palette) do |
||||
vim.api.nvim_set_hl(0, ("IblIndent%d"):format(i), { fg = color, nocombine = true }) |
||||
end |
||||
vim.api.nvim_set_hl(0, "IblIndent", { fg = "#6e738d", nocombine = true }) |
||||
end |
||||
|
||||
return { |
||||
"lukas-reineke/indent-blankline.nvim", |
||||
main = "ibl", |
||||
init = function() |
||||
define_highlights() |
||||
vim.api.nvim_create_autocmd("ColorScheme", { callback = define_highlights }) |
||||
end, |
||||
opts = { |
||||
indent = { |
||||
highlight = { |
||||
"IblIndent", |
||||
"IblIndent1", |
||||
"IblIndent2", |
||||
"IblIndent3", |
||||
"IblIndent4", |
||||
"IblIndent5", |
||||
"IblIndent6", |
||||
}, |
||||
}, |
||||
scope = { enabled = false }, |
||||
}, |
||||
} |
||||
@ -0,0 +1,51 @@
|
||||
-- shell-inits: snacks.nvim tweaks - explorer/picker visibility, notifications |
||||
return { |
||||
"folke/snacks.nvim", |
||||
opts = { |
||||
notifier = { |
||||
timeout = 10000, |
||||
}, |
||||
picker = { |
||||
hidden = true, |
||||
ignored = true, |
||||
sources = { |
||||
explorer = { |
||||
lazy = true, |
||||
hidden = true, |
||||
ignored = true, |
||||
exclude = { |
||||
"**/.git", |
||||
"**/.DS_Store", |
||||
}, |
||||
-- Yank paths from the explorer: Y relative, gy absolute |
||||
win = { |
||||
list = { |
||||
keys = { |
||||
["Y"] = "copy_relative", |
||||
["gy"] = "copy_absolute", |
||||
["L"] = { { "pick_win", "jump" }, mode = { "n", "i" } }, |
||||
}, |
||||
}, |
||||
}, |
||||
actions = { |
||||
copy_absolute = function(_, item) |
||||
local path = item.file |
||||
vim.fn.setreg('"', path) |
||||
vim.fn.setreg("+", path) |
||||
Snacks.notify.info("Copied absolute path: " .. path) |
||||
end, |
||||
copy_relative = function(_, item) |
||||
local path = vim.fn.fnamemodify(item.file, ":.") |
||||
vim.fn.setreg('"', path) |
||||
vim.fn.setreg("+", path) |
||||
Snacks.notify.info("Copied relative path: " .. path) |
||||
end, |
||||
}, |
||||
}, |
||||
files = { |
||||
hidden = true, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
@ -0,0 +1,8 @@
|
||||
-- shell-inits: terminal UX layered on snacks (<leader>tt, <leader>ft, <C-/>) |
||||
return { |
||||
"chodak166/snacks-terminal-ctl.nvim", |
||||
name = "snacks-terminal-ctl.nvim", |
||||
dependencies = { "folke/snacks.nvim" }, |
||||
event = "VeryLazy", |
||||
opts = {}, |
||||
} |
||||
@ -0,0 +1,8 @@
|
||||
-- shell-inits: pin the treesitter parser list (see config/treesitter_parsers); |
||||
-- image builds pre-install all of them so no downloads are needed at runtime. |
||||
return { |
||||
"nvim-treesitter/nvim-treesitter", |
||||
opts = { |
||||
ensure_installed = require("config.treesitter_parsers"), |
||||
}, |
||||
} |
||||
@ -0,0 +1,24 @@
|
||||
#!/bin/sh |
||||
# shell-inits osc52-copy.sh - copy stdin to the workstation clipboard via OSC 52. |
||||
# Works over ssh, docker run -it and nested sessions: the escape sequence is |
||||
# plain terminal output, no X11/Wayland sockets required (the terminal itself |
||||
# must support OSC 52 - kitty, wezterm, iterm2, alacritty, windows terminal). |
||||
# |
||||
# Used by the tmux copy-mode `y` binding. Writing directly to the tmux client |
||||
# ttys makes it independent of tmux's own clipboard emission (which is buggy |
||||
# in some versions). |
||||
set -eu |
||||
|
||||
data=$(base64 | tr -d '\n') |
||||
seq=$(printf '\033]52;c;%s\007' "$data") |
||||
|
||||
if [ -n "${TMUX:-}" ] && command -v tmux >/dev/null 2>&1; then |
||||
# write to every attached tmux client's terminal |
||||
for tty in $(tmux list-clients -F '#{client_tty}' 2>/dev/null); do |
||||
if [ -w "$tty" ]; then |
||||
printf '%s' "$seq" >"$tty" 2>/dev/null || true |
||||
fi |
||||
done |
||||
elif [ -w /dev/tty ]; then |
||||
printf '%s' "$seq" >/dev/tty 2>/dev/null || true |
||||
fi |
||||
@ -0,0 +1,77 @@
|
||||
# shell-inits tmux.conf |
||||
# Pastel theme (catppuccin macchiato palette), no plugins, no patched fonts. |
||||
# Works over ssh: clipboard is shared with the local terminal via OSC 52 |
||||
# (supported by kitty, wezterm, iterm2, alacritty, windows terminal, ...). |
||||
|
||||
# --- base -------------------------------------------------------------------- |
||||
set -g default-command /usr/bin/zsh |
||||
set -g default-terminal "tmux-256color" |
||||
# UTF-8: the tmux server must inherit a UTF-8 locale (zshenv exports a |
||||
# C.UTF-8 fallback); for tmux < 3.1 force the flag explicitly (3.1+ dropped it) |
||||
set -gq utf on |
||||
# truecolor + OSC52 passthrough for terminals whose terminfo lacks the entries |
||||
set -ga terminal-overrides ",*:Tc" |
||||
set -as terminal-overrides ',*:Ms=\E]52;c;%p2%s\7' |
||||
|
||||
set -g escape-time 10 |
||||
set -g history-limit 100000 |
||||
set -g focus-events on |
||||
set -g display-time 2000 |
||||
|
||||
# --- copy mode (vi) ------------------------------------------------------------ |
||||
setw -g mode-keys vi |
||||
set -g set-clipboard on |
||||
set -g allow-passthrough on |
||||
|
||||
bind -T copy-mode-vi v send -X begin-selection |
||||
# yank -> tmux buffer (paste with prefix ]) AND the workstation clipboard via |
||||
# OSC 52; the helper writes to the client terminals directly, so it does not |
||||
# depend on tmux's own (version-dependent) clipboard emission |
||||
bind -T copy-mode-vi y send -X copy-pipe-and-cancel "~/.config/tmux/osc52-copy.sh" |
||||
|
||||
# single-line scrolling in copy mode |
||||
bind -T copy-mode-vi WheelUpPane send-keys -X scroll-up |
||||
bind -T copy-mode-vi WheelDownPane send-keys -X scroll-down |
||||
|
||||
# scrolling with shift + pgup/pgdn |
||||
bind -n S-Pageup copy-mode -u |
||||
bind -n S-Pagedown send-keys Pagedown |
||||
|
||||
# Home/End for line navigation (emacs-style regardless of app) |
||||
bind -n Home send-key C-a |
||||
bind -n End send-key C-e |
||||
|
||||
# --- windows / panes ----------------------------------------------------------- |
||||
set -g mouse off |
||||
set -g status-interval 5 |
||||
set -g automatic-rename on |
||||
set -g automatic-rename-format '#{b:pane_current_command}:#{b:pane_current_path}' |
||||
set -g renumber-windows on |
||||
|
||||
# --- pastel theme (catppuccin macchiato) --------------------------------------- |
||||
# base #24273a mantle #1e2030 crust #181926 surface0 #363a4f surface1 #494d64 |
||||
# text #cad3f5 muted #6e738d mauve #c6a0f6 blue #8aadf4 green #a6da95 |
||||
# peach #f5a97f rosewater #f4dbd6 |
||||
set -g status on |
||||
set -g status-position bottom |
||||
set -g status-style "bg=#24273a,fg=#cad3f5" |
||||
|
||||
set -g status-left-length 40 |
||||
set -g status-left "#[fg=#181926,bg=#c6a0f6,bold] #S #[fg=#c6a0f6,bg=#24273a] " |
||||
|
||||
set -g status-right-length 80 |
||||
set -g status-right "#[fg=#6e738d]#{s|$HOME|~|:pane_current_path} #[fg=#8aadf4]%H #[fg=#a6da95]%H:%M " |
||||
|
||||
setw -g window-status-format "#[fg=#6e738d] #I #W " |
||||
setw -g window-status-current-format "#[fg=#181926,bg=#8aadf4,bold] #I #W " |
||||
setw -g window-status-separator "" |
||||
setw -g window-status-activity-style "fg=#f5a97f,bg=#24273a" |
||||
setw -g monitor-activity on |
||||
set -g visual-activity off |
||||
|
||||
set -g mode-style "bg=#363a4f,fg=#cad3f5" |
||||
set -g message-style "bg=#363a4f,fg=#c6a0f6" |
||||
set -g message-command-style "bg=#363a4f,fg=#8aadf4" |
||||
set -g pane-border-style "fg=#6e738d" |
||||
set -g pane-active-border-style "fg=#c6a0f6" |
||||
set -g clock-mode-colour "#a6da95" |
||||
@ -0,0 +1,79 @@
|
||||
# shell-inits prompt.zsh - two-line pastel prompt with cached git info. |
||||
# |
||||
# Big-repo safety strategy (synchronous by design, async planned for later): |
||||
# - git info is computed only when the directory changed or a command ran |
||||
# (preexec), never on plain prompt redraws; |
||||
# - a single `git status --porcelain=v2 --branch` call provides branch + |
||||
# dirty (incl. untracked) + ahead/behind at once; |
||||
# - core/zsh.sh enables core.untrackedCache and core.fsmonitor globally, which |
||||
# is what keeps `git status` fast even in very large repositories. |
||||
# |
||||
# No patched/nerd fonts needed (plain glyphs: box drawing, ‹› ↑↓ ✱ ❯ …). |
||||
# NOTE: prompt strings must never nest %F{...} (or any braced escape) inside |
||||
# ${var:-...} substitutions - the prompt parser ends the substitution at the |
||||
# first '}'. Colored segments are precomputed in precmd instead. |
||||
|
||||
setopt PROMPT_SUBST |
||||
|
||||
typeset -g _si_git_info="" |
||||
typeset -g _si_git_pwd="" |
||||
typeset -g _si_git_refresh=1 |
||||
typeset -g _si_venv="" |
||||
|
||||
_si_git_render() { |
||||
local input=$1 branch="" oid="" line dirty=0 ahead=0 behind=0 |
||||
while IFS= read -r line; do |
||||
case $line in |
||||
'# branch.head '*) branch=${line##'# branch.head '} ;; |
||||
'# branch.oid '*) oid=${line##'# branch.oid '} ;; |
||||
'# branch.ab +'*) |
||||
ahead=${line#'# branch.ab +'} |
||||
ahead=${ahead%% -*} |
||||
behind=${line##*' -'} |
||||
;; |
||||
[12u?]*) dirty=1 ;; |
||||
esac |
||||
done <<<"$input" |
||||
|
||||
local out=" %F{176}" |
||||
if [[ -n $branch && $branch != "(detached)" ]]; then |
||||
out+=${branch//\%/%%} |
||||
elif [[ -n $oid ]]; then |
||||
out+="@${oid[1,8]}" |
||||
else |
||||
printf '' |
||||
return |
||||
fi |
||||
out+="%f" |
||||
(( dirty )) && out+=" %F{210}✱%f" |
||||
(( ahead )) && out+=" %F{158}↑${ahead}%f" |
||||
(( behind )) && out+=" %F{216}↓${behind}%f" |
||||
printf '%s' "$out" |
||||
} |
||||
|
||||
_si_prompt_precmd() { |
||||
if [[ -n ${VIRTUAL_ENV:-} ]]; then |
||||
_si_venv=" %F{114}‹${VIRTUAL_ENV:t}›%f" |
||||
else |
||||
_si_venv="" |
||||
fi |
||||
|
||||
if (( _si_git_refresh )) || [[ $PWD != $_si_git_pwd ]]; then |
||||
_si_git_refresh=0 |
||||
_si_git_pwd=$PWD |
||||
_si_git_info=$(_si_git_render "$(git status --porcelain=v2 --branch 2>/dev/null)") |
||||
fi |
||||
} |
||||
|
||||
_si_prompt_preexec() { |
||||
_si_git_refresh=1 |
||||
} |
||||
|
||||
autoload -Uz add-zsh-hook |
||||
add-zsh-hook precmd _si_prompt_precmd |
||||
add-zsh-hook preexec _si_prompt_preexec |
||||
|
||||
# pastel palette (256-color): 110 sapphire, 75 blue, 176 mauve, 114 mint, |
||||
# 158 spring, 216 peach, 213 pink, 210 rose |
||||
PROMPT=$'╭─%(!.%F{210}.%F{110})%n@%m%f %F{75}%40<…<%~%<<%f${_si_venv}${_si_git_info}\n╰─%F{213}%(!.#.❯)%f ' |
||||
RPROMPT='%(?..%F{210}exit %?%f)' |
||||
@ -0,0 +1,19 @@
|
||||
# shell-inits zshenv - minimal, sourced by every zsh instance (keep it fast) |
||||
|
||||
# UTF-8 fallback: fresh VMs/containers have no locale set, and a non-UTF-8 |
||||
# locale makes tmux render box-drawing/unicode prompts as underscores. |
||||
# C.UTF-8 is always available (glibc >= 2.35, musl) and needs no locale-gen. |
||||
if [[ -z ${LANG:-} ]]; then |
||||
export LANG=C.UTF-8 |
||||
fi |
||||
|
||||
typeset -U path |
||||
for _d in "$HOME/.local/bin" "$HOME/.cargo/bin" "$HOME/go/bin"; do |
||||
[[ -d "$_d" ]] && path=("$_d" $path) |
||||
done |
||||
unset _d |
||||
|
||||
if (( $+commands[nvim] )); then |
||||
export EDITOR=nvim |
||||
export VISUAL=nvim |
||||
fi |
||||
@ -0,0 +1,67 @@
|
||||
# shell-inits zshrc - interactive configuration (no plugin manager) |
||||
|
||||
# --- input -------------------------------------------------------------------- |
||||
bindkey -e |
||||
WORDCHARS=${WORDCHARS//[\/]} |
||||
|
||||
# --- history -------------------------------------------------------------------- |
||||
setopt CLOBBER |
||||
setopt EXTENDED_HISTORY |
||||
setopt INC_APPEND_HISTORY |
||||
setopt HIST_IGNORE_ALL_DUPS |
||||
unsetopt SHARE_HISTORY |
||||
|
||||
export HISTTIMEFORMAT="[%F %T] " |
||||
export HISTFILE=$HOME/.zsh_history |
||||
export HISTSIZE=1000000 |
||||
export SAVEHIST=1000000 |
||||
|
||||
# --- completion ------------------------------------------------------------------- |
||||
autoload -Uz compinit |
||||
compinit |
||||
|
||||
# --- fzf (key bindings + completion + pastel look) --------------------------------- |
||||
if (( $+commands[fzf] )); then |
||||
# fzf >= 0.48 ships its own integration |
||||
if ! source <(fzf --zsh 2>/dev/null); then |
||||
[[ -r /usr/share/doc/fzf/examples/key-bindings.zsh ]] && \ |
||||
source /usr/share/doc/fzf/examples/key-bindings.zsh |
||||
[[ -r /usr/share/doc/fzf/examples/completion.zsh ]] && \ |
||||
source /usr/share/doc/fzf/examples/completion.zsh |
||||
fi |
||||
export FZF_DEFAULT_OPTS="\ |
||||
--height=40% --layout=reverse --border=rounded --info=inline \ |
||||
--color=fg:#cad3f5,bg:#24273a,hl:#c6a0f6 \ |
||||
--color=fg+:#cad3f5,bg+:#363a4f,hl+:#c6a0f6 \ |
||||
--color=info:#91d7e3,prompt:#8aadf4,pointer:#f5bde6 \ |
||||
--color=marker:#a6da95,spinner:#f5a97f,header:#8bd5ca" |
||||
fi |
||||
|
||||
# --- vendored plugins (plain git clones, see core/zsh.sh) --------------------------- |
||||
if [[ -f $HOME/.zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh ]]; then |
||||
ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=242' |
||||
source $HOME/.zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh |
||||
fi |
||||
|
||||
# --- prompt -------------------------------------------------------------------------- |
||||
[[ -f $HOME/.zsh/prompt.zsh ]] && source $HOME/.zsh/prompt.zsh |
||||
|
||||
# zsh-syntax-highlighting must be sourced last |
||||
if [[ -f $HOME/.zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]]; then |
||||
ZSH_HIGHLIGHT_HIGHLIGHTERS=(main brackets) |
||||
typeset -A ZSH_HIGHLIGHT_STYLES |
||||
ZSH_HIGHLIGHT_STYLES[command]='fg=#666666' |
||||
source $HOME/.zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh |
||||
fi |
||||
|
||||
# --- aliases ---------------------------------------------------------------------------- |
||||
alias ..='cd ..' |
||||
alias ...='cd ../..' |
||||
if (( $+commands[eza] )); then |
||||
alias ll='eza -lah --group-directories-first --color=always | less' |
||||
alias l='eza -lah --group-directories-first --color=always | less' |
||||
else |
||||
alias ll='ls -lah --group-directories-first --color=auto' |
||||
alias l='ls -lah --group-directories-first --color=auto' |
||||
fi |
||||
alias gl="git log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all" |
||||
@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash |
||||
# core/fzf.sh - fuzzy finder, deployed from distro packages when available. |
||||
# Standalone: curl -fsSL <base>/core/fzf.sh | bash -s -- --base-url <base> |
||||
|
||||
set -euo pipefail |
||||
|
||||
# --- shell-inits preamble v1 (identical across core/ and sets/ scripts) ----- |
||||
DEFAULT_BASE_URL="" # per-deployment fallback, e.g. https://git.example.com/shell-inits/raw/main |
||||
SI_BASE_URL=${SHELL_INITS_BASE_URL:-} |
||||
_si_self=${BASH_SOURCE[0]:-} |
||||
while [[ $# -gt 0 ]]; do |
||||
case $1 in |
||||
--base-url) SI_BASE_URL=${2:?--base-url needs a value}; shift 2 ;; |
||||
*) shift ;; |
||||
esac |
||||
done |
||||
if [[ -z $SI_BASE_URL && -n $_si_self && -f $_si_self ]]; then |
||||
SI_MODE=local |
||||
SI_ROOT=$(cd -- "$(dirname -- "$_si_self")/.." && pwd) |
||||
elif [[ -z $SI_BASE_URL && -n $DEFAULT_BASE_URL ]]; then |
||||
SI_MODE=remote |
||||
SI_BASE_URL=$DEFAULT_BASE_URL |
||||
else |
||||
SI_MODE=remote |
||||
fi |
||||
export SI_BASE_URL SI_MODE SI_ROOT |
||||
if [[ -z $SI_BASE_URL && $SI_MODE != local ]]; then |
||||
echo "shell-inits: base URL required (--base-url URL, SHELL_INITS_BASE_URL env, or run from a checkout)" >&2 |
||||
exit 1 |
||||
fi |
||||
if [[ $SI_MODE == local ]]; then |
||||
source "$SI_ROOT/lib/common.sh" |
||||
else |
||||
command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 \ |
||||
|| { echo "shell-inits: curl or wget required" >&2; exit 1; } |
||||
_si_tmp=$(mktemp -d) |
||||
for _si_lib in common distro fetch pkg state asset; do |
||||
if command -v curl >/dev/null 2>&1; then |
||||
curl -fsSL "${SI_BASE_URL%/}/lib/${_si_lib}.sh" -o "${_si_tmp}/${_si_lib}.sh" |
||||
else |
||||
wget -qO "${_si_tmp}/${_si_lib}.sh" "${SI_BASE_URL%/}/lib/${_si_lib}.sh" |
||||
fi |
||||
source "${_si_tmp}/${_si_lib}.sh" |
||||
done |
||||
unset _si_lib _si_tmp |
||||
fi |
||||
# --- end preamble ------------------------------------------------------------ |
||||
|
||||
FZF_REV=1 |
||||
|
||||
si_banner "core/fzf" |
||||
si_distro_detect |
||||
si_pkgmap_load |
||||
|
||||
if si_state_has fzf "$FZF_REV" && si_have_cmd fzf; then |
||||
si_log "fzf already initialized (rev ${FZF_REV}), skipping" |
||||
exit 0 |
||||
fi |
||||
|
||||
si_pkg_install fzf |
||||
|
||||
si_state_mark fzf "$FZF_REV" |
||||
si_log "fzf $(fzf --version | cut -d' ' -f1) ready at $(command -v fzf)" |
||||
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash |
||||
# core/lazygit.sh - lazygit release binary + pastel config (terminal git UI). |
||||
# Standalone: curl -fsSL <base>/core/lazygit.sh | bash -s -- --base-url <base> |
||||
|
||||
set -euo pipefail |
||||
|
||||
# --- shell-inits preamble v1 (identical across core/ and sets/ scripts) ----- |
||||
DEFAULT_BASE_URL="" # per-deployment fallback, e.g. https://git.example.com/shell-inits/raw/main |
||||
SI_BASE_URL=${SHELL_INITS_BASE_URL:-} |
||||
_si_self=${BASH_SOURCE[0]:-} |
||||
while [[ $# -gt 0 ]]; do |
||||
case $1 in |
||||
--base-url) SI_BASE_URL=${2:?--base-url needs a value}; shift 2 ;; |
||||
*) shift ;; |
||||
esac |
||||
done |
||||
if [[ -z $SI_BASE_URL && -n $_si_self && -f $_si_self ]]; then |
||||
SI_MODE=local |
||||
SI_ROOT=$(cd -- "$(dirname -- "$_si_self")/.." && pwd) |
||||
elif [[ -z $SI_BASE_URL && -n $DEFAULT_BASE_URL ]]; then |
||||
SI_MODE=remote |
||||
SI_BASE_URL=$DEFAULT_BASE_URL |
||||
else |
||||
SI_MODE=remote |
||||
fi |
||||
export SI_BASE_URL SI_MODE SI_ROOT |
||||
if [[ -z $SI_BASE_URL && $SI_MODE != local ]]; then |
||||
echo "shell-inits: base URL required (--base-url URL, SHELL_INITS_BASE_URL env, or run from a checkout)" >&2 |
||||
exit 1 |
||||
fi |
||||
if [[ $SI_MODE == local ]]; then |
||||
source "$SI_ROOT/lib/common.sh" |
||||
else |
||||
command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 \ |
||||
|| { echo "shell-inits: curl or wget required" >&2; exit 1; } |
||||
_si_tmp=$(mktemp -d) |
||||
for _si_lib in common distro fetch pkg state asset; do |
||||
if command -v curl >/dev/null 2>&1; then |
||||
curl -fsSL "${SI_BASE_URL%/}/lib/${_si_lib}.sh" -o "${_si_tmp}/${_si_lib}.sh" |
||||
else |
||||
wget -qO "${_si_tmp}/${_si_lib}.sh" "${SI_BASE_URL%/}/lib/${_si_lib}.sh" |
||||
fi |
||||
source "${_si_tmp}/${_si_lib}.sh" |
||||
done |
||||
unset _si_lib _si_tmp |
||||
fi |
||||
# --- end preamble ------------------------------------------------------------ |
||||
|
||||
LAZYGIT_REV=1 |
||||
|
||||
si_banner "core/lazygit" |
||||
si_distro_detect |
||||
si_pkgmap_load |
||||
|
||||
# optional pinning: versions.env next to the scripts |
||||
_v=$(si_try_fetch_file versions.env) && source "$_v" |
||||
|
||||
if si_state_has lazygit && si_have_cmd lazygit; then |
||||
si_log "lazygit already initialized ($(si_state_file lazygit | xargs head -n1)), skipping" |
||||
exit 0 |
||||
fi |
||||
|
||||
si_pkg_install unzip tar |
||||
|
||||
_version=$(si_asset_github jesseduffield/lazygit "${LAZYGIT_VERSION:-latest}" \ |
||||
'lazygit_{ver}_linux_{arch}.tar.gz' 'checksums.txt') |
||||
|
||||
_home=$(si_target_home) |
||||
si_deploy_file config/lazygit/config.yml "${_home}/.config/lazygit/config.yml" |
||||
|
||||
si_state_mark lazygit "${LAZYGIT_REV}-${_version}" |
||||
si_log "lazygit ${_version} ready" |
||||
@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env bash |
||||
# core/neovim.sh - neovim + LazyVim with a personal, language-agnostic layer: |
||||
# catppuccin theme, snacks explorer tweaks, OSC52 clipboard over ssh. |
||||
# Language-specific plugins (C++/rust/go/debuggers) are intentionally out of scope. |
||||
# Standalone: curl -fsSL <base>/core/neovim.sh | bash -s -- --base-url <base> |
||||
# Env: SHELL_INITS_NVIM_BOOTSTRAP=1 -> also sync plugins headlessly (docker builds) |
||||
|
||||
set -euo pipefail |
||||
|
||||
# --- shell-inits preamble v1 (identical across core/ and sets/ scripts) ----- |
||||
DEFAULT_BASE_URL="" # per-deployment fallback, e.g. https://git.example.com/shell-inits/raw/main |
||||
SI_BASE_URL=${SHELL_INITS_BASE_URL:-} |
||||
_si_self=${BASH_SOURCE[0]:-} |
||||
while [[ $# -gt 0 ]]; do |
||||
case $1 in |
||||
--base-url) SI_BASE_URL=${2:?--base-url needs a value}; shift 2 ;; |
||||
*) shift ;; |
||||
esac |
||||
done |
||||
if [[ -z $SI_BASE_URL && -n $_si_self && -f $_si_self ]]; then |
||||
SI_MODE=local |
||||
SI_ROOT=$(cd -- "$(dirname -- "$_si_self")/.." && pwd) |
||||
elif [[ -z $SI_BASE_URL && -n $DEFAULT_BASE_URL ]]; then |
||||
SI_MODE=remote |
||||
SI_BASE_URL=$DEFAULT_BASE_URL |
||||
else |
||||
SI_MODE=remote |
||||
fi |
||||
export SI_BASE_URL SI_MODE SI_ROOT |
||||
if [[ -z $SI_BASE_URL && $SI_MODE != local ]]; then |
||||
echo "shell-inits: base URL required (--base-url URL, SHELL_INITS_BASE_URL env, or run from a checkout)" >&2 |
||||
exit 1 |
||||
fi |
||||
if [[ $SI_MODE == local ]]; then |
||||
source "$SI_ROOT/lib/common.sh" |
||||
else |
||||
command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 \ |
||||
|| { echo "shell-inits: curl or wget required" >&2; exit 1; } |
||||
_si_tmp=$(mktemp -d) |
||||
for _si_lib in common distro fetch pkg state asset; do |
||||
if command -v curl >/dev/null 2>&1; then |
||||
curl -fsSL "${SI_BASE_URL%/}/lib/${_si_lib}.sh" -o "${_si_tmp}/${_si_lib}.sh" |
||||
else |
||||
wget -qO "${_si_tmp}/${_si_lib}.sh" "${SI_BASE_URL%/}/lib/${_si_lib}.sh" |
||||
fi |
||||
source "${_si_tmp}/${_si_lib}.sh" |
||||
done |
||||
unset _si_lib _si_tmp |
||||
fi |
||||
# --- end preamble ------------------------------------------------------------ |
||||
|
||||
NEOVIM_REV=4 |
||||
NVIM_MIN_MAJOR=0 |
||||
NVIM_MIN_MINOR=10 |
||||
|
||||
si_banner "core/neovim" |
||||
si_distro_detect |
||||
si_pkgmap_load |
||||
|
||||
if si_state_has neovim "$NEOVIM_REV" && si_have_cmd nvim; then |
||||
si_log "neovim already initialized (rev ${NEOVIM_REV}), skipping" |
||||
exit 0 |
||||
fi |
||||
|
||||
# lazyvim opens lazygit; make sure it is there first |
||||
si_ensure_core lazygit |
||||
|
||||
# gcc + libc headers: nvim-treesitter compiles parsers locally (libc6-dev is |
||||
# only a Recommends of gcc, so it must be explicit with --no-install-recommends) |
||||
si_pkg_install neovim ripgrep fd-find gcc libc6-dev ca-certificates |
||||
|
||||
# LazyVim requires nvim >= 0.10 |
||||
read -r _maj _min <<<"$(nvim --version | head -n1 | sed -n 's/.*v\([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\1 \2/p')" |
||||
[[ ${_maj:-0} -gt $NVIM_MIN_MAJOR || (${_maj:-0} -eq $NVIM_MIN_MAJOR && ${_min:-0} -ge $NVIM_MIN_MINOR) ]] \ |
||||
|| si_die "neovim ${_maj:-?}.${_min:-?} is too old for LazyVim (needs >= ${NVIM_MIN_MAJOR}.${NVIM_MIN_MINOR})" |
||||
|
||||
_home=$(si_target_home) |
||||
|
||||
# ubuntu ships the binary as fdfind; LazyVim/telescope expect `fd` |
||||
if ! si_have_cmd fd && si_have_cmd fdfind; then |
||||
si_as_user 'mkdir -p ~/.local/bin && ln -sf "$(command -v fdfind)" ~/.local/bin/fd' |
||||
fi |
||||
|
||||
# treesitter parsers are compiled locally; without a C compiler they fail |
||||
if ! si_have_cmd cc && ! si_have_cmd gcc; then |
||||
si_warn "no C compiler found; nvim-treesitter parsers may fail to build (apt install gcc)" |
||||
fi |
||||
|
||||
# replace any pre-existing nvim config (backed up), keep plugin data as-is |
||||
if [[ -e ${_home}/.config/nvim ]]; then |
||||
si_backup_existing "${_home}/.config/nvim" |
||||
rm -rf "${_home}/.config/nvim" |
||||
fi |
||||
|
||||
_nvim_config=${_home}/.config/nvim |
||||
for _f in \ |
||||
init.lua \ |
||||
bootstrap.lua \ |
||||
lua/config/clipboard.lua \ |
||||
lua/config/lazy.lua \ |
||||
lua/config/options.lua \ |
||||
lua/config/keymaps.lua \ |
||||
lua/config/autocmds.lua \ |
||||
lua/config/treesitter_parsers.lua \ |
||||
lua/plugins/colorscheme.lua \ |
||||
lua/plugins/snacks.lua \ |
||||
lua/plugins/indent-blankline.lua \ |
||||
lua/plugins/terminal.lua \ |
||||
lua/plugins/treesitter.lua |
||||
do |
||||
si_deploy_file "config/nvim/${_f}" "${_nvim_config}/${_f}" |
||||
done |
||||
unset _f |
||||
|
||||
# pre-fetch spell files so the first open of a text/markdown file does not |
||||
# prompt "Download it?" (best effort - skipped silently when offline) |
||||
si_as_user 'mkdir -p ~/.local/share/nvim/site/spell && cd ~/.local/share/nvim/site/spell \ |
||||
&& for _f in en.utf-8.spl en.utf-8.sug pl.utf-8.spl; do \ |
||||
[ -f "$_f" ] || curl -fsSLO "https://ftp.nluug.nl/pub/vim/runtime/spell/${_f}" 2>/dev/null || true; \ |
||||
done' |
||||
|
||||
# tree-sitter CLI as a release binary: stops LazyVim from trying to install it |
||||
# through mason (which needs node) on every start; nvim-treesitter main branch |
||||
# ships prebuilt parsers, the CLI is only needed for edge cases |
||||
case $(uname -m) in |
||||
x86_64) _ts_asset='tree-sitter-linux-x64.gz' ;; |
||||
aarch64|arm64) _ts_asset='tree-sitter-linux-arm64.gz' ;; |
||||
*) _ts_asset='' ;; |
||||
esac |
||||
if [[ -n $_ts_asset ]] && ! si_have_cmd tree-sitter; then |
||||
si_asset_github tree-sitter/tree-sitter "${TREE_SITTER_VERSION:-latest}" "$_ts_asset" || \ |
||||
si_warn "tree-sitter CLI install failed; LazyVim may attempt a mason install" |
||||
fi |
||||
unset _ts_asset |
||||
|
||||
# optional headless plugin sync (docker builds / explicit request). |
||||
# Two separate nvim invocations on purpose: treesitter parser installs are |
||||
# unreliable when run in the same session as the very first Lazy sync. |
||||
if [[ ${SHELL_INITS_NVIM_BOOTSTRAP:-0} == 1 ]]; then |
||||
si_log "bootstrapping lazyvim + treesitter parsers headlessly (this can take a while)" |
||||
si_as_user 'nvim --headless "+Lazy! sync" +qa' \ |
||||
|| si_warn "headless plugin sync reported errors; first interactive start will retry" |
||||
si_as_user 'nvim --headless "+lua dofile(vim.fn.stdpath(\"config\")..\"/bootstrap.lua\")" +qa' \ |
||||
|| si_warn "headless bootstrap reported errors; first interactive start will retry" |
||||
fi |
||||
|
||||
si_state_mark neovim "$NEOVIM_REV" |
||||
si_log "neovim $(nvim --version | head -n1) ready with LazyVim at ${_nvim_config}" |
||||
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash |
||||
# core/tmux.sh - tmux with vi copy mode, OSC52 clipboard sharing over ssh and a |
||||
# plugin-free pastel theme (no patched/nerd fonts needed). |
||||
# Standalone: curl -fsSL <base>/core/tmux.sh | bash -s -- --base-url <base> |
||||
|
||||
set -euo pipefail |
||||
|
||||
# --- shell-inits preamble v1 (identical across core/ and sets/ scripts) ----- |
||||
DEFAULT_BASE_URL="" # per-deployment fallback, e.g. https://git.example.com/shell-inits/raw/main |
||||
SI_BASE_URL=${SHELL_INITS_BASE_URL:-} |
||||
_si_self=${BASH_SOURCE[0]:-} |
||||
while [[ $# -gt 0 ]]; do |
||||
case $1 in |
||||
--base-url) SI_BASE_URL=${2:?--base-url needs a value}; shift 2 ;; |
||||
*) shift ;; |
||||
esac |
||||
done |
||||
if [[ -z $SI_BASE_URL && -n $_si_self && -f $_si_self ]]; then |
||||
SI_MODE=local |
||||
SI_ROOT=$(cd -- "$(dirname -- "$_si_self")/.." && pwd) |
||||
elif [[ -z $SI_BASE_URL && -n $DEFAULT_BASE_URL ]]; then |
||||
SI_MODE=remote |
||||
SI_BASE_URL=$DEFAULT_BASE_URL |
||||
else |
||||
SI_MODE=remote |
||||
fi |
||||
export SI_BASE_URL SI_MODE SI_ROOT |
||||
if [[ -z $SI_BASE_URL && $SI_MODE != local ]]; then |
||||
echo "shell-inits: base URL required (--base-url URL, SHELL_INITS_BASE_URL env, or run from a checkout)" >&2 |
||||
exit 1 |
||||
fi |
||||
if [[ $SI_MODE == local ]]; then |
||||
source "$SI_ROOT/lib/common.sh" |
||||
else |
||||
command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 \ |
||||
|| { echo "shell-inits: curl or wget required" >&2; exit 1; } |
||||
_si_tmp=$(mktemp -d) |
||||
for _si_lib in common distro fetch pkg state asset; do |
||||
if command -v curl >/dev/null 2>&1; then |
||||
curl -fsSL "${SI_BASE_URL%/}/lib/${_si_lib}.sh" -o "${_si_tmp}/${_si_lib}.sh" |
||||
else |
||||
wget -qO "${_si_tmp}/${_si_lib}.sh" "${SI_BASE_URL%/}/lib/${_si_lib}.sh" |
||||
fi |
||||
source "${_si_tmp}/${_si_lib}.sh" |
||||
done |
||||
unset _si_lib _si_tmp |
||||
fi |
||||
# --- end preamble ------------------------------------------------------------ |
||||
|
||||
TMUX_REV=3 |
||||
|
||||
si_banner "core/tmux" |
||||
si_distro_detect |
||||
si_pkgmap_load |
||||
|
||||
if si_state_has tmux "$TMUX_REV" && si_have_cmd tmux; then |
||||
si_log "tmux already initialized (rev ${TMUX_REV}), skipping" |
||||
exit 0 |
||||
fi |
||||
|
||||
si_pkg_install tmux |
||||
|
||||
_home=$(si_target_home) |
||||
si_deploy_file config/tmux/osc52-copy.sh "${_home}/.config/tmux/osc52-copy.sh" |
||||
chmod +x "${_home}/.config/tmux/osc52-copy.sh" |
||||
si_chown_target "${_home}/.config/tmux/osc52-copy.sh" |
||||
si_backup_existing "${_home}/.tmux.conf" |
||||
si_deploy_file config/tmux/tmux.conf "${_home}/.tmux.conf" |
||||
|
||||
si_state_mark tmux "$TMUX_REV" |
||||
si_log "tmux $(tmux -V | cut -d' ' -f2) ready, config at ${_home}/.tmux.conf" |
||||
@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash |
||||
# core/zsh.sh - zsh with a fast pastel prompt (async git info, big-repo safe), |
||||
# fzf integration and two vendored plugins (autosuggestions, syntax highlighting) |
||||
# cloned as plain git repos. No plugin manager. |
||||
# Standalone: curl -fsSL <base>/core/zsh.sh | bash -s -- --base-url <base> |
||||
|
||||
set -euo pipefail |
||||
|
||||
# --- shell-inits preamble v1 (identical across core/ and sets/ scripts) ----- |
||||
DEFAULT_BASE_URL="" # per-deployment fallback, e.g. https://git.example.com/shell-inits/raw/main |
||||
SI_BASE_URL=${SHELL_INITS_BASE_URL:-} |
||||
_si_self=${BASH_SOURCE[0]:-} |
||||
while [[ $# -gt 0 ]]; do |
||||
case $1 in |
||||
--base-url) SI_BASE_URL=${2:?--base-url needs a value}; shift 2 ;; |
||||
*) shift ;; |
||||
esac |
||||
done |
||||
if [[ -z $SI_BASE_URL && -n $_si_self && -f $_si_self ]]; then |
||||
SI_MODE=local |
||||
SI_ROOT=$(cd -- "$(dirname -- "$_si_self")/.." && pwd) |
||||
elif [[ -z $SI_BASE_URL && -n $DEFAULT_BASE_URL ]]; then |
||||
SI_MODE=remote |
||||
SI_BASE_URL=$DEFAULT_BASE_URL |
||||
else |
||||
SI_MODE=remote |
||||
fi |
||||
export SI_BASE_URL SI_MODE SI_ROOT |
||||
if [[ -z $SI_BASE_URL && $SI_MODE != local ]]; then |
||||
echo "shell-inits: base URL required (--base-url URL, SHELL_INITS_BASE_URL env, or run from a checkout)" >&2 |
||||
exit 1 |
||||
fi |
||||
if [[ $SI_MODE == local ]]; then |
||||
source "$SI_ROOT/lib/common.sh" |
||||
else |
||||
command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 \ |
||||
|| { echo "shell-inits: curl or wget required" >&2; exit 1; } |
||||
_si_tmp=$(mktemp -d) |
||||
for _si_lib in common distro fetch pkg state asset; do |
||||
if command -v curl >/dev/null 2>&1; then |
||||
curl -fsSL "${SI_BASE_URL%/}/lib/${_si_lib}.sh" -o "${_si_tmp}/${_si_lib}.sh" |
||||
else |
||||
wget -qO "${_si_tmp}/${_si_lib}.sh" "${SI_BASE_URL%/}/lib/${_si_lib}.sh" |
||||
fi |
||||
source "${_si_tmp}/${_si_lib}.sh" |
||||
done |
||||
unset _si_lib _si_tmp |
||||
fi |
||||
# --- end preamble ------------------------------------------------------------ |
||||
|
||||
ZSH_REV=2 |
||||
ZSH_PLUGINS=( |
||||
zsh-users/zsh-autosuggestions |
||||
zsh-users/zsh-syntax-highlighting |
||||
) |
||||
|
||||
si_banner "core/zsh" |
||||
si_distro_detect |
||||
si_pkgmap_load |
||||
|
||||
if si_state_has zsh "$ZSH_REV" && si_have_cmd zsh; then |
||||
si_log "zsh already initialized (rev ${ZSH_REV}), skipping" |
||||
exit 0 |
||||
fi |
||||
|
||||
# fzf provides key bindings and completion integrated by the .zshrc below |
||||
si_ensure_core fzf |
||||
|
||||
si_pkg_install zsh git ca-certificates |
||||
|
||||
_home=$(si_target_home) |
||||
|
||||
# git: make `git status` cheap even in very large repositories |
||||
si_as_user 'git config --global core.untrackedCache true' |
||||
si_as_user 'git config --global core.fsmonitor true' |
||||
|
||||
# vendored plugins (plain clones, no manager) |
||||
for _repo in "${ZSH_PLUGINS[@]}"; do |
||||
_name=${_repo##*/} |
||||
si_as_user "mkdir -p ~/.zsh/plugins && \ |
||||
if [ -d ~/.zsh/plugins/${_name}/.git ]; then \ |
||||
git -C ~/.zsh/plugins/${_name} pull -q --ff-only || true; \ |
||||
else \ |
||||
git clone -q --depth 1 https://github.com/${_repo} ~/.zsh/plugins/${_name}; \ |
||||
fi" || si_warn "failed to clone ${_repo}" |
||||
done |
||||
unset _repo _name |
||||
|
||||
si_backup_existing "${_home}/.zshrc" |
||||
si_backup_existing "${_home}/.zshenv" |
||||
si_deploy_file config/zsh/zshenv "${_home}/.zshenv" |
||||
si_deploy_file config/zsh/zshrc "${_home}/.zshrc" |
||||
si_deploy_file config/zsh/prompt.zsh "${_home}/.zsh/prompt.zsh" |
||||
|
||||
si_state_mark zsh "$ZSH_REV" |
||||
si_log "zsh $(zsh --version | cut -d' ' -f2) ready, config in ${_home}" |
||||
@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash |
||||
# dockerfiles/{{SET}}.build.sh |
||||
# Build fully-initialized shell-inits/{{SET}}:<distro> images. |
||||
# |
||||
# Serves this repository over a temporary local HTTP server (so the image build |
||||
# uses the exact same curl | bash flow as a VM) and then runs docker build with |
||||
# --network=host so the container can reach it. |
||||
# |
||||
# Usage: |
||||
# ./dockerfiles/{{SET}}.build.sh [options] |
||||
# |
||||
# Options: |
||||
# --distro <slug> repeatable; default: ubuntu-26.04 |
||||
# --all build the whole supported distro matrix |
||||
# --uid <N> uid of the image user, owns everything (default 1000) |
||||
# --gid <N> gid of the image user (default: same as --uid) |
||||
# --tag <tag> image tag (default: distro slug) |
||||
# --no-cache pass --no-cache to docker build |
||||
# --base-url <url> fetch scripts from this URL instead of the local server |
||||
# (useful for remote docker hosts or Docker Desktop: |
||||
# serve the repo yourself and pass |
||||
# http://host.docker.internal:<port>) |
||||
# --save also export the image to dist/<set>_<distro>[_<tag>].tar.gz |
||||
# for offline use (docker load -i <file>) |
||||
# -h, --help show this help |
||||
# |
||||
# Note: --network=host does not exist on Docker Desktop (macOS/Windows); |
||||
# there use --base-url with a server reachable from the build container. |
||||
|
||||
set -euo pipefail |
||||
|
||||
SET="{{SET}}" |
||||
SUPPORTED_DISTROS=(ubuntu-26.04) |
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) |
||||
REPO_ROOT=$(cd -- "${SCRIPT_DIR}/.." && pwd) |
||||
|
||||
DISTROS=() |
||||
USER_UID=1000 |
||||
USER_GID=0 |
||||
TAG="" |
||||
NO_CACHE=0 |
||||
BASE_URL_OVERRIDE="" |
||||
SAVE=0 |
||||
|
||||
usage() { |
||||
grep '^#' "$0" | sed 's/^# \{0,1\}//' | tail -n +2 |
||||
exit 0 |
||||
} |
||||
|
||||
die() { echo "[shell-inits][error] $*" >&2; exit 1; } |
||||
|
||||
while [[ $# -gt 0 ]]; do |
||||
case $1 in |
||||
--distro) DISTROS+=("${2:?--distro needs a value}"); shift 2 ;; |
||||
--all) DISTROS=("${SUPPORTED_DISTROS[@]}"); shift ;; |
||||
--uid) USER_UID="${2:?--uid needs a value}"; shift 2 ;; |
||||
--gid) USER_GID="${2:?--gid needs a value}"; shift 2 ;; |
||||
--tag) TAG="${2:?--tag needs a value}"; shift 2 ;; |
||||
--no-cache) NO_CACHE=1; shift ;; |
||||
--base-url) BASE_URL_OVERRIDE="${2:?--base-url needs a value}"; shift 2 ;; |
||||
--save) SAVE=1; shift ;; |
||||
-h|--help) usage ;; |
||||
*) die "unknown option: $1 (see --help)" ;; |
||||
esac |
||||
done |
||||
|
||||
((${#DISTROS[@]})) || DISTROS=(ubuntu-26.04) |
||||
[[ $USER_GID -eq 0 ]] && USER_GID=$USER_UID |
||||
|
||||
command -v docker >/dev/null 2>&1 || die "docker is not available" |
||||
command -v python3 >/dev/null 2>&1 || die "python3 is not available" |
||||
|
||||
SRV_PID="" |
||||
cleanup() { [[ -n $SRV_PID ]] && kill "$SRV_PID" 2>/dev/null || true; } |
||||
trap cleanup EXIT |
||||
|
||||
BASE_URL=$BASE_URL_OVERRIDE |
||||
if [[ -z $BASE_URL ]]; then |
||||
PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()') |
||||
python3 -m http.server "$PORT" --bind 127.0.0.1 --directory "$REPO_ROOT" >/dev/null 2>&1 & |
||||
SRV_PID=$! |
||||
for _ in $(seq 1 50); do |
||||
if (exec 3<>"/dev/tcp/127.0.0.1/${PORT}") 2>/dev/null; then |
||||
exec 3>&- 3<&- |
||||
break |
||||
fi |
||||
sleep 0.1 |
||||
done |
||||
BASE_URL="http://127.0.0.1:${PORT}" |
||||
fi |
||||
|
||||
echo "==> building set '${SET}' (scripts served from: ${BASE_URL})" |
||||
for DISTRO in "${DISTROS[@]}"; do |
||||
# distro slug (ubuntu-26.04) -> image reference (ubuntu:26.04) |
||||
DISTRO_REF="${DISTRO/-/:}" |
||||
IMAGE="shell-inits/${SET}:${TAG:-${DISTRO}}" |
||||
echo "==> image: ${IMAGE} (distro: ${DISTRO_REF}, uid/gid: ${USER_UID}:${USER_GID})" |
||||
# shellcheck disable=SC2086 |
||||
docker build \ |
||||
${NO_CACHE:+--no-cache} \ |
||||
--network=host \ |
||||
--build-arg SHELL_INITS_BASE_URL="${BASE_URL}" \ |
||||
--build-arg DISTRO="${DISTRO_REF}" \ |
||||
--build-arg USER_UID="${USER_UID}" \ |
||||
--build-arg USER_GID="${USER_GID}" \ |
||||
-f "${SCRIPT_DIR}/${SET}.dockerfile" \ |
||||
-t "${IMAGE}" \ |
||||
"${REPO_ROOT}" |
||||
echo "==> built: ${IMAGE}" |
||||
echo " run: docker run -it ${IMAGE}" |
||||
if (( SAVE )); then |
||||
_suffix="${DISTRO}" |
||||
[[ -n $TAG ]] && _suffix="${DISTRO}_${TAG}" |
||||
_file="${REPO_ROOT}/dist/${SET}_${_suffix}.tar.gz" |
||||
mkdir -p "${REPO_ROOT}/dist" |
||||
docker save "${IMAGE}" | gzip >"${_file}" |
||||
echo " saved: ${_file}" |
||||
echo " load: docker load -i ${_file}" |
||||
fi |
||||
done |
||||
echo "==> done" |
||||
@ -0,0 +1,54 @@
|
||||
# dockerfiles/{{SET}}.dockerfile |
||||
# Fully initialized zsh + fzf + tmux + lazyvim + lazygit image, ready to use |
||||
# offline. Built by dockerfiles/{{SET}}.build.sh, which serves this |
||||
# repository over a local HTTP server and passes SHELL_INITS_BASE_URL. |
||||
|
||||
# DISTRO is a docker image reference (build.sh maps the distro slug, e.g. |
||||
# ubuntu-26.04, to ubuntu:26.04) |
||||
ARG DISTRO=ubuntu:26.04 |
||||
FROM ${DISTRO} |
||||
|
||||
ARG SHELL_INITS_BASE_URL |
||||
ARG USERNAME=dev |
||||
ARG USER_UID=1000 |
||||
ARG USER_GID=1000 |
||||
|
||||
# downloader + sudo for the dev user |
||||
RUN apt-get update -qq \ |
||||
&& apt-get install -y -qq --no-install-recommends curl ca-certificates sudo \ |
||||
&& rm -rf /var/lib/apt/lists/* |
||||
|
||||
# non-root user with the requested uid/gid; evict a colliding stock user |
||||
# (ubuntu:26.04 ships an 'ubuntu' user at 1000:1000) and make sure every file |
||||
# produced from here on is owned by the requested uid |
||||
RUN set -eu; \ |
||||
if getent passwd "${USER_UID}" >/dev/null; then \ |
||||
userdel -r "$(getent passwd "${USER_UID}" | cut -d: -f1)" || true; fi; \ |
||||
if getent group "${USER_GID}" >/dev/null; then \ |
||||
groupdel "$(getent group "${USER_GID}" | cut -d: -f1)" || true; fi; \ |
||||
groupadd -g "${USER_GID}" "${USERNAME}" \ |
||||
&& useradd -m -s /bin/bash -u "${USER_UID}" -g "${USER_GID}" "${USERNAME}" \ |
||||
&& echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/${USERNAME}" \ |
||||
&& chmod 440 "/etc/sudoers.d/${USERNAME}" |
||||
|
||||
# run the toolset as root, provisioning the user's environment; |
||||
# SHELL_INITS_NVIM_BOOTSTRAP=1 syncs lazyvim plugins headlessly inside the |
||||
# build so no network is needed when the container runs |
||||
RUN curl -fsSL "${SHELL_INITS_BASE_URL}/sets/{{SET}}.sh" \ |
||||
| env SHELL_INITS_BASE_URL="${SHELL_INITS_BASE_URL}" \ |
||||
SHELL_INITS_USER="${USERNAME}" \ |
||||
SHELL_INITS_NVIM_BOOTSTRAP=1 \ |
||||
bash |
||||
|
||||
# slim down |
||||
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* |
||||
|
||||
COPY dockerfiles/entrypoint.sh /usr/local/bin/shell-inits-entrypoint |
||||
RUN chmod 0755 /usr/local/bin/shell-inits-entrypoint |
||||
|
||||
USER ${USERNAME} |
||||
WORKDIR /home/${USERNAME} |
||||
ENV SHELL=/usr/bin/zsh |
||||
# UTF-8 locale for the tmux server and everything else in the container |
||||
ENV LANG=C.UTF-8 |
||||
ENTRYPOINT ["/usr/local/bin/shell-inits-entrypoint"] |
||||
@ -0,0 +1,8 @@
|
||||
#!/bin/sh |
||||
# shell-inits container entrypoint: attach to or create the default tmux |
||||
# session 'main'. Set SHELL_INITS_NO_TMUX=1 for a plain login zsh. |
||||
set -e |
||||
if [ "${SHELL_INITS_NO_TMUX:-0}" = "1" ]; then |
||||
exec "${SHELL:-/usr/bin/zsh}" -l |
||||
fi |
||||
exec tmux new -As main |
||||
@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash |
||||
# dockerfiles/zsh-tmux-neovim.build.sh |
||||
# Build fully-initialized shell-inits/zsh-tmux-neovim:<distro> images. |
||||
# |
||||
# Serves this repository over a temporary local HTTP server (so the image build |
||||
# uses the exact same curl | bash flow as a VM) and then runs docker build with |
||||
# --network=host so the container can reach it. |
||||
# |
||||
# Usage: |
||||
# ./dockerfiles/zsh-tmux-neovim.build.sh [options] |
||||
# |
||||
# Options: |
||||
# --distro <slug> repeatable; default: ubuntu-26.04 |
||||
# --all build the whole supported distro matrix |
||||
# --uid <N> uid of the image user, owns everything (default 1000) |
||||
# --gid <N> gid of the image user (default: same as --uid) |
||||
# --tag <tag> image tag (default: distro slug) |
||||
# --no-cache pass --no-cache to docker build |
||||
# --base-url <url> fetch scripts from this URL instead of the local server |
||||
# (useful for remote docker hosts or Docker Desktop: |
||||
# serve the repo yourself and pass |
||||
# http://host.docker.internal:<port>) |
||||
# --save also export the image to dist/<set>_<distro>[_<tag>].tar.gz |
||||
# for offline use (docker load -i <file>) |
||||
# -h, --help show this help |
||||
# |
||||
# Note: --network=host does not exist on Docker Desktop (macOS/Windows); |
||||
# there use --base-url with a server reachable from the build container. |
||||
|
||||
set -euo pipefail |
||||
|
||||
SET="zsh-tmux-neovim" |
||||
SUPPORTED_DISTROS=(ubuntu-26.04) |
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) |
||||
REPO_ROOT=$(cd -- "${SCRIPT_DIR}/.." && pwd) |
||||
|
||||
DISTROS=() |
||||
USER_UID=1000 |
||||
USER_GID=0 |
||||
TAG="" |
||||
NO_CACHE=0 |
||||
BASE_URL_OVERRIDE="" |
||||
SAVE=0 |
||||
|
||||
usage() { |
||||
grep '^#' "$0" | sed 's/^# \{0,1\}//' | tail -n +2 |
||||
exit 0 |
||||
} |
||||
|
||||
die() { echo "[shell-inits][error] $*" >&2; exit 1; } |
||||
|
||||
while [[ $# -gt 0 ]]; do |
||||
case $1 in |
||||
--distro) DISTROS+=("${2:?--distro needs a value}"); shift 2 ;; |
||||
--all) DISTROS=("${SUPPORTED_DISTROS[@]}"); shift ;; |
||||
--uid) USER_UID="${2:?--uid needs a value}"; shift 2 ;; |
||||
--gid) USER_GID="${2:?--gid needs a value}"; shift 2 ;; |
||||
--tag) TAG="${2:?--tag needs a value}"; shift 2 ;; |
||||
--no-cache) NO_CACHE=1; shift ;; |
||||
--base-url) BASE_URL_OVERRIDE="${2:?--base-url needs a value}"; shift 2 ;; |
||||
--save) SAVE=1; shift ;; |
||||
-h|--help) usage ;; |
||||
*) die "unknown option: $1 (see --help)" ;; |
||||
esac |
||||
done |
||||
|
||||
((${#DISTROS[@]})) || DISTROS=(ubuntu-26.04) |
||||
[[ $USER_GID -eq 0 ]] && USER_GID=$USER_UID |
||||
|
||||
command -v docker >/dev/null 2>&1 || die "docker is not available" |
||||
command -v python3 >/dev/null 2>&1 || die "python3 is not available" |
||||
|
||||
SRV_PID="" |
||||
cleanup() { [[ -n $SRV_PID ]] && kill "$SRV_PID" 2>/dev/null || true; } |
||||
trap cleanup EXIT |
||||
|
||||
BASE_URL=$BASE_URL_OVERRIDE |
||||
if [[ -z $BASE_URL ]]; then |
||||
PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()') |
||||
python3 -m http.server "$PORT" --bind 127.0.0.1 --directory "$REPO_ROOT" >/dev/null 2>&1 & |
||||
SRV_PID=$! |
||||
for _ in $(seq 1 50); do |
||||
if (exec 3<>"/dev/tcp/127.0.0.1/${PORT}") 2>/dev/null; then |
||||
exec 3>&- 3<&- |
||||
break |
||||
fi |
||||
sleep 0.1 |
||||
done |
||||
BASE_URL="http://127.0.0.1:${PORT}" |
||||
fi |
||||
|
||||
echo "==> building set '${SET}' (scripts served from: ${BASE_URL})" |
||||
for DISTRO in "${DISTROS[@]}"; do |
||||
# distro slug (ubuntu-26.04) -> image reference (ubuntu:26.04) |
||||
DISTRO_REF="${DISTRO/-/:}" |
||||
IMAGE="shell-inits/${SET}:${TAG:-${DISTRO}}" |
||||
echo "==> image: ${IMAGE} (distro: ${DISTRO_REF}, uid/gid: ${USER_UID}:${USER_GID})" |
||||
# shellcheck disable=SC2086 |
||||
docker build \ |
||||
${NO_CACHE:+--no-cache} \ |
||||
--network=host \ |
||||
--build-arg SHELL_INITS_BASE_URL="${BASE_URL}" \ |
||||
--build-arg DISTRO="${DISTRO_REF}" \ |
||||
--build-arg USER_UID="${USER_UID}" \ |
||||
--build-arg USER_GID="${USER_GID}" \ |
||||
-f "${SCRIPT_DIR}/${SET}.dockerfile" \ |
||||
-t "${IMAGE}" \ |
||||
"${REPO_ROOT}" |
||||
echo "==> built: ${IMAGE}" |
||||
echo " run: docker run -it ${IMAGE}" |
||||
if (( SAVE )); then |
||||
_suffix="${DISTRO}" |
||||
[[ -n $TAG ]] && _suffix="${DISTRO}_${TAG}" |
||||
_file="${REPO_ROOT}/dist/${SET}_${_suffix}.tar.gz" |
||||
mkdir -p "${REPO_ROOT}/dist" |
||||
docker save "${IMAGE}" | gzip >"${_file}" |
||||
echo " saved: ${_file}" |
||||
echo " load: docker load -i ${_file}" |
||||
fi |
||||
done |
||||
echo "==> done" |
||||
@ -0,0 +1,54 @@
|
||||
# dockerfiles/zsh-tmux-neovim.dockerfile |
||||
# Fully initialized zsh + fzf + tmux + lazyvim + lazygit image, ready to use |
||||
# offline. Built by dockerfiles/zsh-tmux-neovim.build.sh, which serves this |
||||
# repository over a local HTTP server and passes SHELL_INITS_BASE_URL. |
||||
|
||||
# DISTRO is a docker image reference (build.sh maps the distro slug, e.g. |
||||
# ubuntu-26.04, to ubuntu:26.04) |
||||
ARG DISTRO=ubuntu:26.04 |
||||
FROM ${DISTRO} |
||||
|
||||
ARG SHELL_INITS_BASE_URL |
||||
ARG USERNAME=dev |
||||
ARG USER_UID=1000 |
||||
ARG USER_GID=1000 |
||||
|
||||
# downloader + sudo for the dev user |
||||
RUN apt-get update -qq \ |
||||
&& apt-get install -y -qq --no-install-recommends curl ca-certificates sudo \ |
||||
&& rm -rf /var/lib/apt/lists/* |
||||
|
||||
# non-root user with the requested uid/gid; evict a colliding stock user |
||||
# (ubuntu:26.04 ships an 'ubuntu' user at 1000:1000) and make sure every file |
||||
# produced from here on is owned by the requested uid |
||||
RUN set -eu; \ |
||||
if getent passwd "${USER_UID}" >/dev/null; then \ |
||||
userdel -r "$(getent passwd "${USER_UID}" | cut -d: -f1)" || true; fi; \ |
||||
if getent group "${USER_GID}" >/dev/null; then \ |
||||
groupdel "$(getent group "${USER_GID}" | cut -d: -f1)" || true; fi; \ |
||||
groupadd -g "${USER_GID}" "${USERNAME}" \ |
||||
&& useradd -m -s /bin/bash -u "${USER_UID}" -g "${USER_GID}" "${USERNAME}" \ |
||||
&& echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/${USERNAME}" \ |
||||
&& chmod 440 "/etc/sudoers.d/${USERNAME}" |
||||
|
||||
# run the toolset as root, provisioning the user's environment; |
||||
# SHELL_INITS_NVIM_BOOTSTRAP=1 syncs lazyvim plugins headlessly inside the |
||||
# build so no network is needed when the container runs |
||||
RUN curl -fsSL "${SHELL_INITS_BASE_URL}/sets/zsh-tmux-neovim.sh" \ |
||||
| env SHELL_INITS_BASE_URL="${SHELL_INITS_BASE_URL}" \ |
||||
SHELL_INITS_USER="${USERNAME}" \ |
||||
SHELL_INITS_NVIM_BOOTSTRAP=1 \ |
||||
bash |
||||
|
||||
# slim down |
||||
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* |
||||
|
||||
COPY dockerfiles/entrypoint.sh /usr/local/bin/shell-inits-entrypoint |
||||
RUN chmod 0755 /usr/local/bin/shell-inits-entrypoint |
||||
|
||||
USER ${USERNAME} |
||||
WORKDIR /home/${USERNAME} |
||||
ENV SHELL=/usr/bin/zsh |
||||
# UTF-8 locale for the tmux server and everything else in the container |
||||
ENV LANG=C.UTF-8 |
||||
ENTRYPOINT ["/usr/local/bin/shell-inits-entrypoint"] |
||||
@ -0,0 +1,87 @@
|
||||
# lib/asset.sh - install prebuilt binaries from GitHub releases |
||||
# Asset pattern placeholders: {tag}=full tag (v1.2.3), {ver}=tag without 'v', |
||||
# {arch}=x86_64 | arm64 (aarch64 is mapped to arm64, the common release naming). |
||||
|
||||
si_arch() { |
||||
case $(uname -m) in |
||||
x86_64) printf x86_64 ;; |
||||
aarch64 | arm64) printf arm64 ;; |
||||
*) si_die "unsupported architecture: $(uname -m)" ;; |
||||
esac |
||||
} |
||||
|
||||
si_gh_latest_tag() { |
||||
local repo=$1 url tag |
||||
url=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \ |
||||
"https://github.com/${repo}/releases/latest") \ |
||||
|| si_die "failed to resolve latest release of ${repo}" |
||||
tag=${url##*/} |
||||
[[ -n $tag ]] || si_die "could not parse latest tag for ${repo}" |
||||
printf '%s' "$tag" |
||||
} |
||||
|
||||
# si_install_bin <file> - install an executable to /usr/local/bin when |
||||
# possible, otherwise to the target user's ~/.local/bin. |
||||
si_install_bin() { |
||||
local src=$1 name dest dir |
||||
name=$(basename "$src") |
||||
if si_is_root || si_have_cmd sudo; then |
||||
dest="/usr/local/bin/${name}" |
||||
si_as_root install -m 0755 "$src" "$dest" |
||||
else |
||||
dir="$(si_target_home)/.local/bin" |
||||
mkdir -p "$dir" |
||||
install -m 0755 "$src" "$dir/$name" |
||||
si_chown_target "$dir" |
||||
dest="$dir/$name" |
||||
fi |
||||
si_log "installed binary: $dest" |
||||
} |
||||
|
||||
# si_asset_github <owner/repo> <version|latest> <asset-pattern> [checksums-file] |
||||
# Downloads, verifies (when a checksums file is given), extracts and installs |
||||
# the single executable contained in the asset. |
||||
si_asset_github() { |
||||
local repo=$1 version=$2 pattern=$3 checksums=${4:-} |
||||
local arch tag base asset tmp want candidate cks |
||||
|
||||
arch=$(si_arch) |
||||
if [[ $version == latest ]]; then |
||||
tag=$(si_gh_latest_tag "$repo") |
||||
else |
||||
tag=$version |
||||
fi |
||||
base="https://github.com/${repo}/releases/download/${tag}" |
||||
asset=$pattern |
||||
asset=${asset//\{tag\}/$tag} |
||||
asset=${asset//\{ver\}/${tag#v}} |
||||
asset=${asset//\{arch\}/$arch} |
||||
|
||||
tmp=$(mktemp -d) |
||||
si_log "downloading ${repo} ${tag} (${asset})" |
||||
si_fetch_raw "${base}/${asset}" >"${tmp}/${asset}" || si_die "download failed: ${asset}" |
||||
|
||||
if [[ -n $checksums ]]; then |
||||
cks=$(basename "$checksums") |
||||
si_fetch_raw "${base}/${checksums}" >"${tmp}/${cks}" || si_die "checksums download failed" |
||||
(cd "$tmp" && grep " ${asset}\$" "$cks" | sha256sum -c - >/dev/null) \ |
||||
|| si_die "checksum mismatch for ${asset}" |
||||
si_log "checksum verified: ${asset}" |
||||
fi |
||||
|
||||
case $asset in |
||||
*.tar.gz | *.tgz) tar -xzf "${tmp}/${asset}" -C "$tmp" ;; |
||||
*.zip) unzip -qo "${tmp}/${asset}" -d "$tmp" ;; |
||||
*.gz) gzip -dc "${tmp}/${asset}" >"${tmp}/${repo##*/}" \ |
||||
&& chmod +x "${tmp}/${repo##*/}" ;; |
||||
*) mv "${tmp}/${asset}" "${tmp}/binary" ;; |
||||
esac |
||||
|
||||
want=${repo##*/} |
||||
candidate=$(find "$tmp" -maxdepth 3 -type f -name "$want" -perm -u+x | head -n1) |
||||
[[ -n $candidate ]] || candidate=$(find "$tmp" -maxdepth 3 -type f -perm -u+x | head -n1) |
||||
[[ -n $candidate ]] || si_die "no executable found inside ${asset}" |
||||
si_install_bin "$candidate" |
||||
rm -rf "$tmp" |
||||
printf '%s\n' "${tag#v}" |
||||
} |
||||
@ -0,0 +1,114 @@
|
||||
# lib/common.sh - shared helpers for all shell-inits scripts |
||||
# Sourced after the standard entry preamble (which exports SI_BASE_URL/SI_MODE/SI_ROOT). |
||||
set -euo pipefail |
||||
|
||||
# --- logging (logs go to stderr; stdout is reserved for return values) ------- |
||||
|
||||
si_log() { printf '[shell-inits] %s\n' "$*" >&2; } |
||||
si_warn() { printf '[shell-inits][warn] %s\n' "$*" >&2; } |
||||
si_die() { printf '[shell-inits][error] %s\n' "$*" >&2; exit 1; } |
||||
si_banner() { printf '\n==> %s\n' "$*" >&2; } |
||||
|
||||
# --- small predicates ------------------------------------------------------ |
||||
|
||||
si_have_cmd() { command -v "$1" >/dev/null 2>&1; } |
||||
si_is_root() { [[ $(id -u) -eq 0 ]]; } |
||||
|
||||
# --- privileges ------------------------------------------------------------ |
||||
|
||||
# Run a command with root privileges (packages etc). |
||||
si_as_root() { |
||||
if si_is_root; then |
||||
"$@" |
||||
elif si_have_cmd sudo; then |
||||
sudo "$@" |
||||
elif si_have_cmd doas; then |
||||
doas "$@" |
||||
else |
||||
si_die "root privileges required and no sudo/doas available" |
||||
fi |
||||
} |
||||
|
||||
# The user whose environment we configure: |
||||
# 1. SHELL_INITS_USER (explicit, e.g. docker builds running as root for a user) |
||||
# 2. SUDO_USER (vm user running the script through sudo) |
||||
# 3. current user (root login / running as the user directly) |
||||
si_target_user() { |
||||
if [[ -n ${SHELL_INITS_USER:-} ]]; then |
||||
printf '%s' "$SHELL_INITS_USER" |
||||
elif [[ -n ${SUDO_USER:-} ]]; then |
||||
printf '%s' "$SUDO_USER" |
||||
else |
||||
id -un |
||||
fi |
||||
} |
||||
|
||||
si_target_home() { |
||||
local user home |
||||
user=$(si_target_user) |
||||
home=$(getent passwd "$user" 2>/dev/null | cut -d: -f6 || true) |
||||
[[ -n $home ]] || home=$HOME |
||||
printf '%s' "$home" |
||||
} |
||||
|
||||
# Run a shell command as the target user (git config, plugin clones, nvim...). |
||||
si_as_user() { |
||||
local cmd=$1 target |
||||
target=$(si_target_user) |
||||
if [[ $target == $(id -un) ]]; then |
||||
bash -c "$cmd" |
||||
elif si_is_root; then |
||||
su -s /bin/bash "$target" -c "$cmd" |
||||
else |
||||
si_die "cannot run as user '$target' without root privileges" |
||||
fi |
||||
} |
||||
|
||||
# Fix ownership of files we created as root for another user. |
||||
si_chown_target() { |
||||
local target group |
||||
target=$(si_target_user) |
||||
[[ $target == $(id -un) ]] && return 0 |
||||
si_is_root || return 0 |
||||
group=$(id -gn "$target" 2>/dev/null || true) |
||||
[[ -n $group ]] || group=$target |
||||
chown -R "$target:$group" "$@" |
||||
} |
||||
|
||||
# Create a directory as the target user (avoids root-owned ~/.local/... when |
||||
# provisioning another user's home). |
||||
si_user_mkdir() { |
||||
local dir=$1 |
||||
if [[ $(si_target_user) == $(id -un) ]]; then |
||||
mkdir -p "$dir" |
||||
else |
||||
si_as_user "mkdir -p '$dir'" |
||||
fi |
||||
} |
||||
|
||||
# --- component dependencies ------------------------------------------------- |
||||
|
||||
# Keep a one-time backup of a pre-existing config path. |
||||
si_backup_existing() { |
||||
local path=$1 bak |
||||
[[ -e $path ]] || return 0 |
||||
bak="${path}.pre-shell-inits" |
||||
[[ -e $bak ]] && bak="${path}.pre-shell-inits.$(date +%s)" |
||||
cp -a "$path" "$bak" |
||||
si_warn "backed up existing $path -> $bak" |
||||
} |
||||
|
||||
# Ensure another core initializer has run (checks its state marker). |
||||
si_ensure_core() { |
||||
local component=$1 script |
||||
if si_state_has "$component"; then |
||||
return 0 |
||||
fi |
||||
si_log "dependency core/${component}.sh not satisfied yet, running it" |
||||
if [[ ${SI_MODE:-local} == local ]]; then |
||||
script="$SI_ROOT/core/${component}.sh" |
||||
else |
||||
script=$(si_fetch_file "core/${component}.sh") |
||||
fi |
||||
bash "$script" || si_die "dependency component '${component}' failed" |
||||
} |
||||
@ -0,0 +1,20 @@
|
||||
# lib/distro.sh - detect the target distro, reject unsupported ones |
||||
# Sets: SI_DISTRO_SLUG (e.g. ubuntu-26.04), SI_PKG_MANAGER (apt|apk) |
||||
|
||||
si_distro_detect() { |
||||
[[ -r /etc/os-release ]] || si_die "cannot read /etc/os-release" |
||||
# shellcheck disable=SC1091 |
||||
. /etc/os-release |
||||
local slug="${ID}-${VERSION_ID}" |
||||
unset ID VERSION_ID NAME PRETTY_NAME VERSION_ID VERSION 2>/dev/null || true |
||||
|
||||
case $slug in |
||||
ubuntu-26.04) SI_DISTRO_SLUG=$slug; SI_PKG_MANAGER=apt ;; |
||||
*) |
||||
si_die "unsupported distro '${slug}'. Supported: ubuntu-26.04. \ |
||||
To support another distro add lib/pkgmaps/${slug}.sh and extend lib/distro.sh." |
||||
;; |
||||
esac |
||||
export SI_DISTRO_SLUG SI_PKG_MANAGER |
||||
si_log "detected distro: $SI_DISTRO_SLUG (pkg manager: $SI_PKG_MANAGER)" |
||||
} |
||||
@ -0,0 +1,61 @@
|
||||
# lib/fetch.sh - fetch files relative to the base URL (or read from a checkout) |
||||
# Requires SI_MODE/SI_ROOT/SI_BASE_URL from the entry preamble. |
||||
|
||||
# Print a URL to stdout (curl preferred, wget fallback). |
||||
si_fetch_raw() { |
||||
local url=$1 |
||||
if si_have_cmd curl; then |
||||
curl -fsSL "$url" |
||||
elif si_have_cmd wget; then |
||||
wget -qO- "$url" |
||||
else |
||||
si_die "neither curl nor wget available" |
||||
fi |
||||
} |
||||
|
||||
# si_fetch_file <relpath> -> prints a local path to the file (dies on failure) |
||||
si_fetch_file() { |
||||
local rel=$1 out |
||||
if [[ ${SI_MODE:-local} == local ]]; then |
||||
out="$SI_ROOT/$rel" |
||||
[[ -f $out ]] || si_die "file missing from checkout: $out" |
||||
else |
||||
out=$(mktemp) |
||||
si_fetch_raw "${SI_BASE_URL%/}/$rel" >"$out" || si_die "failed to fetch $rel" |
||||
fi |
||||
printf '%s' "$out" |
||||
} |
||||
|
||||
# si_try_fetch_file <relpath> -> prints path or nothing if unavailable |
||||
si_try_fetch_file() { |
||||
local rel=$1 |
||||
if [[ ${SI_MODE:-local} == local ]]; then |
||||
[[ -f "$SI_ROOT/$rel" ]] && printf '%s' "$SI_ROOT/$rel" |
||||
else |
||||
local out |
||||
out=$(mktemp) |
||||
if si_fetch_raw "${SI_BASE_URL%/}/$rel" >"$out" 2>/dev/null; then |
||||
printf '%s' "$out" |
||||
else |
||||
rm -f "$out" |
||||
fi |
||||
fi |
||||
} |
||||
|
||||
# si_fetch <relpath> -> file contents on stdout |
||||
si_fetch() { |
||||
local path |
||||
path=$(si_fetch_file "$1") |
||||
cat "$path" |
||||
} |
||||
|
||||
# si_deploy_file <relpath> <absolute-dest-path> |
||||
si_deploy_file() { |
||||
local src dest |
||||
src=$(si_fetch_file "$1") |
||||
dest=$2 |
||||
si_user_mkdir "$(dirname "$dest")" |
||||
cp "$src" "$dest" |
||||
si_chown_target "$dest" |
||||
si_log "deployed $1 -> $dest" |
||||
} |
||||
@ -0,0 +1,49 @@
|
||||
# lib/pkg.sh - package installation abstraction (apt/apk dispatch via pkgmaps) |
||||
# Requires SI_DISTRO_SLUG. The pkgmap defines: declare -A PKG (abstract -> real). |
||||
|
||||
si_pkgmap_load() { |
||||
local map |
||||
map=$(si_fetch_file "lib/pkgmaps/${SI_DISTRO_SLUG}.sh") |
||||
# shellcheck disable=SC1090 |
||||
source "$map" || si_die "failed to load pkgmap for ${SI_DISTRO_SLUG}" |
||||
si_log "loaded pkgmap for ${SI_DISTRO_SLUG}" |
||||
} |
||||
|
||||
# Refresh the package index once per process. |
||||
si_pkg_update() { |
||||
[[ -n ${SI_PKG_UPDATED:-} ]] && return 0 |
||||
case ${SI_PKG_MANAGER:?pkg manager not detected} in |
||||
apt) si_as_root env DEBIAN_FRONTEND=noninteractive apt-get update -qq ;; |
||||
apk) si_as_root apk update -q ;; |
||||
*) si_die "unknown package manager: $SI_PKG_MANAGER" ;; |
||||
esac |
||||
SI_PKG_UPDATED=1 |
||||
} |
||||
|
||||
# si_pkg_install <abstract-name>... (names resolved through the pkgmap) |
||||
si_pkg_install() { |
||||
(( $# > 0 )) || return 0 |
||||
si_pkg_update |
||||
local -a real=() |
||||
local abstract mapped |
||||
for abstract in "$@"; do |
||||
mapped=${PKG[$abstract]:-} |
||||
if [[ -z $mapped ]]; then |
||||
si_warn "no pkgmap entry for '$abstract', using the name as-is" |
||||
mapped=$abstract |
||||
fi |
||||
real+=("$mapped") |
||||
done |
||||
si_log "installing packages: ${real[*]}" |
||||
case $SI_PKG_MANAGER in |
||||
apt) |
||||
si_as_root env DEBIAN_FRONTEND=noninteractive \ |
||||
apt-get install -y -qq --no-install-recommends "${real[@]}" |
||||
;; |
||||
apk) |
||||
si_as_root apk add -q "${real[@]}" |
||||
;; |
||||
*) |
||||
si_die "unknown package manager: $SI_PKG_MANAGER" ;; |
||||
esac |
||||
} |
||||
@ -0,0 +1,20 @@
|
||||
# Package name map for ubuntu-26.04 (abstract name -> apt package) |
||||
# Used by lib/pkg.sh. Keep abstract names distro-neutral. |
||||
|
||||
declare -gA PKG=( |
||||
[ca-certificates]=ca-certificates |
||||
[curl]=curl |
||||
[wget]=wget |
||||
[git]=git |
||||
[unzip]=unzip |
||||
[tar]=tar |
||||
|
||||
[tmux]=tmux |
||||
[zsh]=zsh |
||||
[fzf]=fzf |
||||
[neovim]=neovim |
||||
[ripgrep]=ripgrep |
||||
[fd-find]=fd-find |
||||
[gcc]=gcc |
||||
[libc6-dev]=libc6-dev |
||||
) |
||||
@ -0,0 +1,65 @@
|
||||
# lib/run-set.sh - shared runner used by sets/<name>.sh wrappers |
||||
# The set wrapper defines SET_NAME and COMPONENTS, then calls si_run_set. |
||||
|
||||
si_run_core() { |
||||
local component=$1 script |
||||
if [[ ${SI_MODE:-local} == local ]]; then |
||||
script="$SI_ROOT/core/${component}.sh" |
||||
[[ -f $script ]] || si_die "unknown component '${component}' (no ${script})" |
||||
else |
||||
script=$(si_fetch_file "core/${component}.sh") |
||||
fi |
||||
si_banner "component: ${component}" |
||||
SI_COMPONENT=$component bash "$script" || si_die "component '${component}' failed" |
||||
} |
||||
|
||||
si_run_set() { |
||||
local component skip |
||||
local -a skips=() |
||||
if [[ -n ${SHELL_INITS_SKIP:-} ]]; then |
||||
IFS=',' read -r -a skips <<<"$SHELL_INITS_SKIP" |
||||
fi |
||||
for component in "${COMPONENTS[@]}"; do |
||||
for skip in "${skips[@]:-}"; do |
||||
[[ $component == "$skip" ]] && continue 2 |
||||
done |
||||
si_run_core "$component" |
||||
done |
||||
} |
||||
|
||||
# --- common post hooks ------------------------------------------------------ |
||||
|
||||
# Make zsh the login shell of the target user (best effort). |
||||
si_set_default_shell_zsh() { |
||||
local target shell zsh_bin |
||||
target=$(si_target_user) |
||||
shell=$(getent passwd "$target" | cut -d: -f7) |
||||
if [[ $shell == *zsh ]]; then |
||||
si_log "login shell of '${target}' is already zsh" |
||||
return 0 |
||||
fi |
||||
zsh_bin=$(command -v zsh || true) |
||||
[[ -n $zsh_bin ]] || { si_warn "zsh not found, cannot set default shell"; return 0; } |
||||
if si_is_root; then |
||||
usermod -s "$zsh_bin" "$target" 2>/dev/null \ |
||||
|| chsh -s "$zsh_bin" "$target" 2>/dev/null \ |
||||
|| si_warn "failed to change login shell for '${target}'" |
||||
elif si_have_cmd sudo; then |
||||
sudo chsh -s "$zsh_bin" "$target" 2>/dev/null \ |
||||
|| sudo usermod -s "$zsh_bin" "$target" 2>/dev/null \ |
||||
|| si_warn "failed to change login shell for '${target}'" |
||||
else |
||||
si_warn "no root privileges; run 'chsh -s ${zsh_bin}' manually" |
||||
fi |
||||
} |
||||
|
||||
si_print_summary() { |
||||
local tool |
||||
printf '\n==> %s complete\n' "${SET_NAME:-shell-inits}" |
||||
for tool in zsh tmux fzf nvim lazygit; do |
||||
if si_have_cmd "$tool"; then |
||||
printf ' %-8s %s\n' "$tool" "$(command -v "$tool")" |
||||
fi |
||||
done |
||||
printf '==> next: start a new login shell (zsh), then run: tmux new -As main\n\n' |
||||
} |
||||
@ -0,0 +1,35 @@
|
||||
# lib/state.sh - idempotency markers (which components are already installed) |
||||
# State lives in the target user's state dir; first line = version/rev marker. |
||||
|
||||
si_state_root() { |
||||
printf '%s/shell-inits' "${XDG_STATE_HOME:-$(si_target_home)/.local/state}" |
||||
} |
||||
|
||||
si_state_file() { |
||||
printf '%s/%s.ver' "$(si_state_root)" "$1" |
||||
} |
||||
|
||||
# si_state_has <component> [revision] |
||||
# True when the marker exists and matches the revision (any revision if omitted). |
||||
# SHELL_INITS_FORCE=1 or SI_FORCE=1 always returns false (reinstall). |
||||
si_state_has() { |
||||
local component=$1 revision=${2:-} file |
||||
[[ -n ${SI_FORCE:-${SHELL_INITS_FORCE:-}} ]] && return 1 |
||||
file=$(si_state_file "$component") |
||||
[[ -f $file ]] || return 1 |
||||
if [[ -n $revision ]]; then |
||||
[[ $(head -n1 "$file") == "$revision" ]] || return 1 |
||||
fi |
||||
return 0 |
||||
} |
||||
|
||||
# si_state_mark <component> <revision> |
||||
si_state_mark() { |
||||
local component=$1 revision=$2 file |
||||
file=$(si_state_file "$component") |
||||
si_user_mkdir "$(dirname "$file")" |
||||
printf '%s\ninstalled=%s distro=%s\n' \ |
||||
"$revision" "$(date -u +%FT%TZ)" "${SI_DISTRO_SLUG:-unknown}" >"$file" |
||||
si_chown_target "$file" |
||||
si_log "marked ${component} as initialized (rev ${revision})" |
||||
} |
||||
@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash |
||||
# sets/zsh-tmux-neovim.sh - zsh + fzf + tmux + lazyvim + lazygit |
||||
# Single-command entry point, curl-able: |
||||
# curl -fsSL <base>/sets/zsh-tmux-neovim.sh | bash -s -- --base-url <base> |
||||
# Env: SHELL_INITS_SKIP=tmux,neovim exclude components |
||||
# SHELL_INITS_FORCE=1 reinstall everything |
||||
# SHELL_INITS_NVIM_BOOTSTRAP=1 sync nvim plugins headlessly right away |
||||
|
||||
set -euo pipefail |
||||
|
||||
# --- shell-inits preamble v1 (identical across core/ and sets/ scripts) ----- |
||||
DEFAULT_BASE_URL="" # per-deployment fallback, e.g. https://git.example.com/shell-inits/raw/main |
||||
SI_BASE_URL=${SHELL_INITS_BASE_URL:-} |
||||
_si_self=${BASH_SOURCE[0]:-} |
||||
while [[ $# -gt 0 ]]; do |
||||
case $1 in |
||||
--base-url) SI_BASE_URL=${2:?--base-url needs a value}; shift 2 ;; |
||||
*) shift ;; |
||||
esac |
||||
done |
||||
if [[ -z $SI_BASE_URL && -n $_si_self && -f $_si_self ]]; then |
||||
SI_MODE=local |
||||
SI_ROOT=$(cd -- "$(dirname -- "$_si_self")/.." && pwd) |
||||
elif [[ -z $SI_BASE_URL && -n $DEFAULT_BASE_URL ]]; then |
||||
SI_MODE=remote |
||||
SI_BASE_URL=$DEFAULT_BASE_URL |
||||
else |
||||
SI_MODE=remote |
||||
fi |
||||
export SI_BASE_URL SI_MODE SI_ROOT |
||||
if [[ -z $SI_BASE_URL && $SI_MODE != local ]]; then |
||||
echo "shell-inits: base URL required (--base-url URL, SHELL_INITS_BASE_URL env, or run from a checkout)" >&2 |
||||
exit 1 |
||||
fi |
||||
if [[ $SI_MODE == local ]]; then |
||||
source "$SI_ROOT/lib/common.sh" |
||||
else |
||||
command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 \ |
||||
|| { echo "shell-inits: curl or wget required" >&2; exit 1; } |
||||
_si_tmp=$(mktemp -d) |
||||
for _si_lib in common distro fetch pkg state asset run-set; do |
||||
if command -v curl >/dev/null 2>&1; then |
||||
curl -fsSL "${SI_BASE_URL%/}/lib/${_si_lib}.sh" -o "${_si_tmp}/${_si_lib}.sh" |
||||
else |
||||
wget -qO "${_si_tmp}/${_si_lib}.sh" "${SI_BASE_URL%/}/lib/${_si_lib}.sh" |
||||
fi |
||||
source "${_si_tmp}/${_si_lib}.sh" |
||||
done |
||||
unset _si_lib _si_tmp |
||||
fi |
||||
# --- end preamble ------------------------------------------------------------ |
||||
|
||||
SET_NAME="zsh-tmux-neovim" |
||||
COMPONENTS=(fzf zsh tmux lazygit neovim) |
||||
|
||||
si_banner "set: ${SET_NAME}" |
||||
si_distro_detect |
||||
si_pkgmap_load |
||||
|
||||
si_run_set |
||||
|
||||
si_set_default_shell_zsh |
||||
si_print_summary |
||||
Loading…
Reference in new issue