GitHub

A reproducible, batteries-included Emacs distribution. Pick what you want via a small DSL; pin everything via lockfile; switch modal systems on the fly.

CI License: GPL-3.0 Emacs

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 defaultbin/zetta install byte-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 install before 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:

  1. Create ~/.zetta.el (your config) and ~/.private.el (API keys)
  2. 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 an elpaca.pre-*.bak snapshot, purge eln-cache/ the same way.
  3. Install and byte-compile all packages from source via Elpaca
  4. 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

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 freeze output 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-run bin/zetta freeze afterwards 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-MISSING warning 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 :ref in elpaca-order in source/bootstrap/bootstrap-elpaca.el, because the installer clones elpaca before any lockfile is read. When bumping it, re-check the zetta--elpaca-queue-return-struct advice in the same file: it patches the duplicate-declaration branch that returns the warn string instead of the elpaca struct (which kills daemon startup with wrong-type-argument listp, measured 2026-07-23). The queue function was renamed elpaca--queueelpaca--enqueue after 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-daemon start 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 gradients
  • repeatable — which-key-integrated repeatable prefix commands
  • spot / spot4e — Spotify control from Emacs
  • magneto — buffer/window magnetism
  • touchtype — typing-speed practice
  • space-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.

Read the original on github.com ↗