A reproducible, batteries-included Emacs distribution. Pick what you want via a small DSL; pin everything via lockfile; switch modal systems on the fly.
Status: This is my personal Emacs config, published in the open. Use at your own risk; expect breaking changes whenever I want them.
Highlights
- Triple-modal editing — Switch between Evil, Meow, and vanilla Emacs with
s-z e/s-z m/s-z E. No restart. - brushup — Parametric, theme-aware color gradient system. Faces auto-update when you change themes. (chiply/brushup)
- repeatable — Repeatable prefix commands integrated with which-key. (chiply/repeatable)
- Module DSL — Enable/disable categories or individual packages via the
zetta-modules!macro in~/.zetta.el. - Reproducible — Elpaca lockfile pins every package to an exact commit.
- Compiled by default —
bin/zetta installbyte-compiles modules and native-compiles packages ahead of first launch.
How does this compare to…
- Doom Emacs — Doom is Evil-only with a framework around use-package. Zetta supports Evil, Meow, and vanilla side-by-side, and the module loader is plain Elisp around use-package — no DSL to learn beyond
zetta-modules!. - Vanilla /
init.el— Zetta gives you ~320 packages already wired up; turn off any module category you don't want. - Crafted Emacs / Prelude — Similar modular philosophy. Zetta is more opinionated, ships a custom theme engine (
brushup), and uses Elpaca with a real lockfile.
Requirements
- Emacs 29+ (30+ recommended). CI tests 29.4, 30.2, and the Emacs 31 pretest (release branch); master snapshots are untested. The pretest entry becomes a pinned 31.1 once it ships.
- Git, ripgrep (for consult-ripgrep)
- Optional: Node.js (LSP servers), Python 3 (python-ts-mode, pytest),
fd(fast file search)
Installation
git clone https://github.com/chiply/.zetta.d ~/.zetta.d && cd ~/.zetta.d && bin/zetta install
Important: Run
bin/zetta installbefore launching Emacs for the first time. It byte-compiles and native-compiles everything up front so the UI renders correctly on first launch. Without it, Emacs will install ~320 packages on startup, leading to a broken modeline, missing tab bar, and other glitches while it catches up.
The install command will:
- Create
~/.zetta.el(your config) and~/.private.el(API keys) - Purge stale compiled artifacts (
eln-cache/, module/bootstrap.elc) — Emacs prefers a matching native-compiled cache entry over the source on disk, so a stale entry can silently shadow fixed code and crash the installer. Reinstalling over an existing checkout is safe because of this step; if you ever roll back package state by restoring anelpaca.pre-*.baksnapshot, purgeeln-cache/the same way. - Install and byte-compile all packages from source via Elpaca
- Native-compile everything
Everything builds locally
bin/zetta install builds every package from source on your machine,
against whatever Emacs you bring. Nothing prebuilt is distributed:
byte-compiled artifacts are only guaranteed correct for the exact
Emacs build that produced them, and cross-compiling on CI for other
platforms and Emacs builds produces subtle incompatibilities (we
tried). A full first build takes on the order of an hour; after that,
installs are incremental.
Building locally also means running a moving target like the Emacs 31 pretest is harmless: your bytecode is always compiled by exactly the Emacs that runs it.
If you switch to a different Emacs build over an existing install,
rebuild against it: rm -rf elpaca && bin/zetta install.
bin/zetta doctor includes a bytecode census that flags any .elc
compiled by a different Emacs than the one it is run with.
With chemacs2
If you use chemacs2, add to ~/.emacs-profiles.el:
("zetta" . ((user-emacs-directory . "~/.zetta.d")))
Then launch with emacs --with-profile zetta.
Quick Start
Edit ~/.zetta.el to customize:
(setq zetta-theme 'modus-operandi) ; default (setq zetta-font "Terminus (TTF)") ; default (zetta-modules! :core :completion :ui (-nyan-mode -parrot) ; load all of :ui except these :editor :lang (python yaml typescript-ts-mode) ; load only these :tools (magit lsp docker flycheck) :org :term)
API keys and credentials go in ~/.private.el (see .private.sample.el). For 1Password-backed secrets, see secrets.md.
Documentation
docs/modules.md— every module file and what it configuresdocs/keybindings.md— full keybinding referenceCOMPILATION.md— byte-compile, native-compile, compile-angel architecturesecrets.md— 1Password CLI secrets managementslack.md— emacs-slack token + cookie setupCHANGELOG.md— release notes
CLI
bin/zetta <command>
install Install packages and native-compile
sync Re-evaluate config, install new packages
freeze [--commit] Write lockfile (optionally commit it)
update Pull all packages (backs up lockfile first)
doctor Diagnose environment and configuration
test Start test daemon and verify startup
Modules
| Category | Description |
|---|---|
:core |
Emacs defaults, buffers, projects, persistence, keybindings |
:completion |
Vertico, consult, orderless, corfu, embark, marginalia |
:ui |
Themes, modeline, icons, treemacs, window management, visual aids |
:editor |
Evil + extensions, smartparens, snippets, undo-tree, avy, ace-window |
:lang |
Python, TypeScript, YAML, Terraform, SQL, web-mode, tree-sitter |
:tools |
Magit, LSP, Docker, flycheck, dired, compile, git utilities |
:app |
Elfeed, bookmarks, Spotify, Mastodon, ERC, EWW, word lookup |
:org |
Org-mode, org-ref, citar, pdf-tools, org-capture, org-remark |
:term |
Vterm, shell, foreman |
See docs/modules.md for per-file descriptions.
Keybindings
The leader key is , (comma) in non-insert states, C-, in insert state.
| Key | Menu | Purpose |
|---|---|---|
, g |
Version control | Magit, git-link, git-timemachine, blamer |
, p |
Project | Project-scoped operations |
, w |
Window | Window management, ace-window |
, l |
Lookup | Search, devdocs, consult |
, o |
Org | Org-mode commands, capture, agenda |
, r |
Run | Compile, run, shell commands |
, t |
Theme | Theme switching |
, h |
Help | Help, documentation |
, d |
Smerge | Merge conflict resolution |
, i |
iedit | Multi-occurrence editing |
See docs/keybindings.md for the full reference.
Updating
bin/zetta update # pull latest packages, back up lockfile bin/zetta freeze # rewrite lockfile from current state
To run on bleeding-edge instead of pinned: (setq zetta-use-lockfile nil) in ~/.zetta.el.
The lockfile is distro-managed
elpaca-lock.el pins every package to an exact commit so that a fresh
install builds the same verified set CI tests (the weekly cold-install
rehearsal builds from nothing against exactly these pins). Treat it like
Doom's pins or a flake.lock:
- Don't hand-edit it, and don't commit your own
freezeoutput unless you maintain a fork. Personal version preferences belong in~/.zetta.el(zetta-use-lockfile nil), not in the lock. - On a pull conflict, take upstream's copy (
git checkout --theirs elpaca-lock.el) and re-runbin/zetta freezeafterwards if you maintain your own pins. - Adding a package? Freeze in the same PR. A package without a lock
entry floats at whatever upstream serves that day — the exact failure
mode that broke fresh installs for ten weeks in mid-2026. CI prints a
LOCK-MISSINGwarning for unpinned packages, and an optional pre-commit hook nags locally:git config core.hooksPath .githooks - Bumping elpaca itself? Elpaca's own pin is not in the lockfile — it's
the
:refinelpaca-orderinsource/bootstrap/bootstrap-elpaca.el, because the installer clones elpaca before any lockfile is read. When bumping it, re-check thezetta--elpaca-queue-return-structadvice in the same file: it patches the duplicate-declaration branch that returns thewarnstring instead of the elpaca struct (which kills daemon startup withwrong-type-argument listp, measured 2026-07-23). The queue function was renamedelpaca--queue→elpaca--enqueueafter the current pin, so the advice targets both names and survives the rename — but drop the advice entirely once the pinned ref carries an upstream fix that returns the struct, and verify a cold--fg-daemonstart either way.
Uninstalling
rm -rf ~/.zetta.d ~/.zetta.el ~/.private.el
If you used chemacs2, also remove the ("zetta" ...) entry from ~/.emacs-profiles.el.
Structure
.zetta.d/
├── bin/zetta # CLI wrapper
├── init.el # Entry point
├── early-init.el # Startup optimization
├── elpaca-lock.el # Package version lockfile
├── templates/ # User config templates
├── source/
│ ├── bootstrap/ # Core initialization
│ ├── init-data/ # Default module file list
│ └── zettapkg/ # Bundled custom packages
├── modules/ # Package configurations (9 categories)
└── docs/ # Documentation
Bundled custom packages
Several packages are written specifically for Zetta and live as separate public repos:
brushup— theme-aware parametric face gradientsrepeatable— which-key-integrated repeatable prefix commandsspot/spot4e— Spotify control from Emacsmagneto— buffer/window magnetismtouchtype— typing-speed practicespace-tree— workspace tree navigation
Acknowledgements
- Elpaca — the package manager Zetta is built on
- Doom Emacs — inspiration for the modular layout and use-package conventions
- general.el — keybinding system
- Meow and Evil — both modal editing systems Zetta wires up
Contributing / Issues
Issues and PRs are welcome at chiply/.zetta.d/issues. Bear in mind the status above — this is primarily a personal config, and I make decisions accordingly.
License
GPL-3.0 — see LICENSE.