Browse Source

Added core initializers: zsh, tmux, fzf, neovim, lazygit + basic set

master
chodak166 4 weeks ago
parent
commit
def0eb4104
  1. 3
      .gitignore
  2. 418
      PLAN.md
  3. 110
      README.md
  4. 25
      config/lazygit/config.yml
  5. 26
      config/nvim/bootstrap.lua
  6. 3
      config/nvim/init.lua
  7. 1
      config/nvim/lua/config/autocmds.lua
  8. 53
      config/nvim/lua/config/clipboard.lua
  9. 2
      config/nvim/lua/config/keymaps.lua
  10. 43
      config/nvim/lua/config/lazy.lua
  11. 32
      config/nvim/lua/config/options.lua
  12. 47
      config/nvim/lua/config/treesitter_parsers.lua
  13. 28
      config/nvim/lua/plugins/colorscheme.lua
  14. 34
      config/nvim/lua/plugins/indent-blankline.lua
  15. 51
      config/nvim/lua/plugins/snacks.lua
  16. 8
      config/nvim/lua/plugins/terminal.lua
  17. 8
      config/nvim/lua/plugins/treesitter.lua
  18. 24
      config/tmux/osc52-copy.sh
  19. 77
      config/tmux/tmux.conf
  20. 79
      config/zsh/prompt.zsh
  21. 19
      config/zsh/zshenv
  22. 67
      config/zsh/zshrc
  23. 63
      core/fzf.sh
  24. 72
      core/lazygit.sh
  25. 148
      core/neovim.sh
  26. 71
      core/tmux.sh
  27. 96
      core/zsh.sh
  28. 121
      dockerfiles/_template.build.sh
  29. 54
      dockerfiles/_template.dockerfile
  30. 8
      dockerfiles/entrypoint.sh
  31. 121
      dockerfiles/zsh-tmux-neovim.build.sh
  32. 54
      dockerfiles/zsh-tmux-neovim.dockerfile
  33. 87
      lib/asset.sh
  34. 114
      lib/common.sh
  35. 20
      lib/distro.sh
  36. 61
      lib/fetch.sh
  37. 49
      lib/pkg.sh
  38. 20
      lib/pkgmaps/ubuntu-26.04.sh
  39. 65
      lib/run-set.sh
  40. 35
      lib/state.sh
  41. 63
      sets/zsh-tmux-neovim.sh
  42. 5
      versions.env

3
.gitignore vendored

@ -0,0 +1,3 @@
dist/
*.pyc
__pycache__/

418
PLAN.md

@ -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.

110
README.md

@ -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.

25
config/lazygit/config.yml

@ -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

26
config/nvim/bootstrap.lua

@ -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)))

3
config/nvim/init.lua

@ -0,0 +1,3 @@
-- shell-inits: early OSC52 clipboard setup must run before lazy.nvim
require("config.clipboard")
require("config.lazy")

1
config/nvim/lua/config/autocmds.lua

@ -0,0 +1 @@
-- shell-inits: autocmds on top of LazyVim defaults

53
config/nvim/lua/config/clipboard.lua

@ -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"

2
config/nvim/lua/config/keymaps.lua

@ -0,0 +1,2 @@
-- shell-inits: keymaps on top of LazyVim defaults
-- (terminal keymaps are registered by lua/plugins/terminal.lua)

43
config/nvim/lua/config/lazy.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",
},
},
},
})

32
config/nvim/lua/config/options.lua

@ -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 = "<",
}

47
config/nvim/lua/config/treesitter_parsers.lua

@ -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",
}

28
config/nvim/lua/plugins/colorscheme.lua

@ -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",
},
},
}

34
config/nvim/lua/plugins/indent-blankline.lua

@ -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 },
},
}

51
config/nvim/lua/plugins/snacks.lua

@ -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,
},
},
},
},
}

8
config/nvim/lua/plugins/terminal.lua

@ -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 = {},
}

8
config/nvim/lua/plugins/treesitter.lua

@ -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"),
},
}

24
config/tmux/osc52-copy.sh

@ -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

77
config/tmux/tmux.conf

@ -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"

79
config/zsh/prompt.zsh

@ -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)'

19
config/zsh/zshenv

@ -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

67
config/zsh/zshrc

@ -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"

63
core/fzf.sh

@ -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)"

72
core/lazygit.sh

@ -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"

148
core/neovim.sh

@ -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}"

71
core/tmux.sh

@ -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"

96
core/zsh.sh

@ -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}"

121
dockerfiles/_template.build.sh

@ -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"

54
dockerfiles/_template.dockerfile

@ -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"]

8
dockerfiles/entrypoint.sh

@ -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

121
dockerfiles/zsh-tmux-neovim.build.sh

@ -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"

54
dockerfiles/zsh-tmux-neovim.dockerfile

@ -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"]

87
lib/asset.sh

@ -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}"
}

114
lib/common.sh

@ -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"
}

20
lib/distro.sh

@ -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)"
}

61
lib/fetch.sh

@ -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"
}

49
lib/pkg.sh

@ -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
}

20
lib/pkgmaps/ubuntu-26.04.sh

@ -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
)

65
lib/run-set.sh

@ -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'
}

35
lib/state.sh

@ -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})"
}

63
sets/zsh-tmux-neovim.sh

@ -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

5
versions.env

@ -0,0 +1,5 @@
# Versions of tools installed from GitHub release binaries.
# "latest" resolves through the releases/latest redirect; pin a concrete
# version (e.g. "v0.55.1") for reproducible installs.
LAZYGIT_VERSION=latest
TREE_SITTER_VERSION=latest
Loading…
Cancel
Save