Modules
Jotain’s Elisp configuration is split acrosslisp/init-*.el, one file per concern. Each file is self-contained: a package that only exists to enhance a built-in (e.g. dirvish → dired, magit → vc, corfu → completion-preview) lives in the same file as the built-in it enhances. There is no “builtins.el” / “third-party.el” split.
Conventions
- Every module file is named
init-<concern>.elunderlisp/. - Every file starts with a
lexical-binding: tcookie. - Every file ends with
(provide 'init-<concern>). - Modules are loaded in order from
init.el. use-package-always-ensureist(set inearly-init.el), so everyuse-packageblock defaults to “install if missing”. Built-ins must opt out with:ensure nil. Packages provided by Nix use:ensure nil— Nix puts them onload-path, souse-packagefinds them without touching the network.- User options (
defcustomvariables) are set viasetopt, notsetq, so the:setcallback and:typevalidation run.
Load order
init.el loads modules in this order:
Core modules
init-core
GC, encoding, and the sane-defaults baseline. Restores gc-cons-threshold to 16 MiB after the early-init bump; pauses GC entirely while the minibuffer is open; forces UTF-8 everywhere; enables save-place-mode, recentf, savehist, repeat-mode, uniquify, ibuffer as the default buffer list, global-auto-revert-mode. Defines jotain-var-dir and the jotain-var-file helper, and themes the built-in state-file variables (recentf-save-file, savehist-file, save-place-file, bookmark-default-file, auto-save-list-file-prefix) to live under var/. Other modules use the same helper to keep their own state (keyfreq, logview, forge, transient) in the same place. Also contains the macOS modifier-key fix (Cmd→Meta, Option→Super, right Option untouched for special characters). Sets read-extended-command-predicate to filter M-x candidates by major mode; untagged commands are still shown, and typing a full command name always runs it regardless of the filter.
init-keys
Global bindings only — per-package bindings live in the relevant use-package block. Unbinds C-z / C-x C-z (no accidental suspend on GUI), binds M-o to other-window, enables windmove-default-keybindings (Shift + arrows), defines jotain-toggle-window-split on C-x j for rotating a two-window layout, and ships a window-resize repeat-map so C-x ^ ^ ^ keeps enlarging without re-pressing C-x. Also registers which-key-add-key-based-replacements for the cross-cutting C-c / C-x prefixes (C-c n org-roam, C-c r eglot-refactor, C-c o combobulate, C-x P project, etc.) so the prefix menus surface short noun-phrase labels instead of bare command names. See Keybindings for the full namespace map and Ergonomics for the repeat-maps rationale.
init-ui
Appearance. Uses modus-operandi-tinted and modus-vivendi-tinted (user-customisable via jotain-theme-light / jotain-theme-dark), with auto-dark-mode flipping between them based on the system appearance. Installs doom-modeline, picks the first available font from jotain-font-preferences (JetBrains Mono Nerd Font → Iosevka Nerd Font → DejaVu Sans Mono), enables display-line-numbers, pixel-scroll-precision-mode, hl-line-mode, show-paren-mode, built-in which-key, nerd-icons integrations for dired/ibuffer/corfu/marginalia, hl-todo, breadcrumb, pulsar, rainbow-delimiters, indent-bars.
init-help
Built-in help tweaks (help-window-select) plus helpful for richer describe-* buffers. See Finding Information in Emacs for day-to-day usage.
init-docs
Makes the bundled Jotain Info manual (jotain.info, generated by nix build .#info) discoverable from C-h i. For NixOS / nix-darwin / Home Manager users this is already wired up at the Nix layer: the Emacs wrapper pre-sets INFOPATH, so no Elisp is needed — this module is the source-checkout fallback. After just info, result-info/share/info is added to Info-directory-list; C-h i d m Jotain RET opens the manual. JOTAIN_INFO_DIR overrides the search.
init-editing
Baseline editing primitives: electric-pair-mode, delete-selection-mode, whitespace-mode, undo settings, region tools.
init-completion
The vertico stack on the minibuffer side, corfu + cape on the in-buffer side, plus the built-in minibuffer tweaks they rely on. All configured in one file because touching one nearly always means touching the others. C-s is deliberately left as isearch-forward; use M-s l for consult-line when you want the consult UI.
init-navigation
dired and dirvish in the same file, along with project, winner-mode, and directional navigation.
init-vc
vc pinned to Git + Jujutsu (other backends are a slow startup tax), vc-jj for jj support through built-in vc/project.el (jj repos are typically colocated with git), magit with refined hunks and worktrees in magit-status, majutsu as a magit-style jj porcelain (C-c j status/log, C-c M-j dispatch), magit-todos, forge (configured with the built-in sqlite backend, emacsql-sqlite-builtin), diff-hl (including diff-hl-flydiff-mode for pre-save indicators), smerge-mode helpers, and ediff with sane window setup. The N/M modeline counter is backend-aware: a .jj directory selects jj probes (jj diff --git, an author_date revset), otherwise git. For correct jj diff/conflict rendering set ui.diff-formatter = ":git" and ui.conflict-marker-style = "git" via jj config edit --user.
init-prog
The shared substrate for programming modes: prog-mode hooks, treesit setup, eglot, flymake, eldoc, apheleia for format-on-save, compile. Per-language eglot-ensure hooks live here so all LSP wiring is in one place, and beyond that curated list eglot auto-starts for any project file whose language server is on the buffer’s (envrc-applied) PATH — so enabling a language in a project’s devenv lights up its LSP with no per-language config. Per-language mode settings live in init-lang-*.el. When the rass binary (rassumfrassum, shipped in the devenv shell) is on PATH, tsx-ts-mode / typescript-ts-mode / typescript-mode buffers are routed through it to run typescript-language-server alongside whichever of eslint-lsp and tailwindcss-language-server are also on PATH, and Python buffers run the bundled rass python preset (basedpyright + ruff) when both binaries are present; any missing prerequisite makes eglot fall back to its built-in single-server lookup, so projects using pylsp / pyright / unguarded tsserver keep working unchanged. Also provides jotain-sonarlint for on-demand SonarLint analysis via the sonarlint-ls language server — SonarCloud connected mode is configured per-project via eglot-workspace-configuration in .dir-locals.el. Debugging goes through dape (Debug Adapter Protocol, C-x C-a prefix); the adapter binaries (e.g. dlv for Go, debugpy for Python) come from the project/host PATH, same as the LSP servers.
init-snippets
Template/snippet expansion via tempel, chosen over the built-in skeleton/abbrev and the heavier yasnippet because it shares an author with the corfu/cape/vertico stack and plugs straight into completion-at-point-functions — snippet names surface in the corfu popup (added buffer-locally ahead of the cape capfs on prog-mode/text-mode), and M-+ (tempel-complete) / M-* (tempel-insert) expand them, with TAB / S-TAB moving between fields. The curated templates themselves live in templates/jotain.eld (keyed by major mode — basic for/while/if/function constructs for Emacs Lisp, Python, Rust, Go, C/C++, and TS/TSX/JS), so adding a snippet never touches Elisp. eglot-tempel is configured here too, so Tempel also expands the snippets language servers return (e.g. function-argument placeholders); it is armed the moment eglot loads, before the first connection.
init-project
Two complementary project command systems: projection (.dir-locals.el-backed commands exposed via C-x P, auto-detected from Makefiles/justfiles/Cargo.toml/etc) and compile-multi (per-major-mode named compile commands like “go test”, “pytest file”, “nix flake check”). They overlap but neither fully covers the other.
init-devenv
Wiring for the in-repo devenv.sh integration library lisp/devenv.el — the one file under lisp/ that is not an init-* module: it is a standalone, reusable package (own devenv- namespace, no jotain- dependencies) that this module merely binds into the configuration. C-c v opens a transient with task and script runners (completing-read over devenv tasks list --json / devenv eval scripts), devenv test/build/update/gc through compilation-mode with Nix-trace error matching, a tabulated process dashboard over the devenv process manager (devenv up/down, per-process start/stop/restart/logs), environment introspection (devenv eval/info/search), and devenv-reload (invalidates caches, refreshes the environment through envrc, offers eglot-reconnect). devenv.nix buffers are routed to the bundled devenv lsp server while nil keeps serving other Nix files. devenv-mcp-setup registers the project’s devenv mcp stdio server with mcp.el for gptel tool use. Environment loading stays with envrc (see init-prog); the library’s optional native loader (devenv-env-global-mode) exists for devenv projects without direnv and skips envrc-managed projects. See devenv integration for the user guide.
init-ai
AI assistants. claude-code-ide on C-c C-' for agentic multi-file edits via the Claude Code CLI. eca on C-c C-e is the Editor Code Assistant client (chat, inline completion, rewrite, MCP) talking to a Nix-provided server. gptel on C-c RET (send) / C-c M-RET (menu) with four backends configured: OpenRouter (default, OpenAI-compatible aggregator, anthropic/claude-sonnet-4.6), direct Anthropic, Google Gemini, and a local Ollama instance (no key needed). mcp is loaded after gptel for Model Context Protocol tool use. API keys resolve from the environment (OPENROUTER_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY) first, then fall back to auth-source — auth-source-1password (see init-systems) makes that transparent. The eca server’s OpenRouter provider lives in config/eca/config.json, installed by the Home Manager module when services.jotain.openrouter.enable = true.
init-shell
eshell, comint, and ielm together — shells and REPLs have their own input handling, history, prompts, and rendering, so grouping them lets the rules be coordinated in one place. The true PTY terminal emulator lives in init-terminal.
init-terminal
Both directions of terminal support. Inside Emacs: ghostel, a terminal emulator powered by libghostty-vt (the VT engine behind Ghostty) through a native dynamic module — real PTY for tmux/ncurses programs, plus automatic shell integration (OSC 7 directory tracking, OSC 133 prompt navigation) and OSC 52 clipboard. The Nix package builds the module from source and ships it in the package directory; in a source checkout the module is downloaded into the writable elpa/ dir on first M-x ghostel. Emacs inside a terminal: global-kkp-mode (Kitty Keyboard Protocol), clipetty (OSC 52 clipboard through SSH + tmux), xterm-mouse-mode, and tty-tip-mode on Emacs 31+ — all no-ops in GUI frames. Ghostel’s buffers advertise TERM=xterm-ghostty, the same dialect the outer Ghostty terminal uses; the xterm-ghostty → xterm-256color alias that supports both lives in early-init.el because terminal initialisation runs before init.el.
init-systems
Sysadmin tools: SOPS-encrypted file editing, logview for log files, and auth-source-1password as an auth-source backend so magit/forge, gptel, smtpmail, etc. all resolve credentials against the 1Password vault transparently.
init-tracking
Passive usage instrumentation. keyfreq counts command frequency (M-x keyfreq-show), wakatime-mode reports heartbeats to a Wakatime/Wakapi server (guarded by :if on wakatime-cli + WAKATIME_API_KEY, so it is a no-op if either is missing), and activity-watch-mode is installed but off by default — enable with M-x global-activity-watch-mode.
init-writing
Prose editing: jinx for spell-checking, markdown-mode, denote for notes, pdf-tools. Org is big enough to earn its own file.
init-org
org-mode, org-modern, capture templates, agenda, and friends. Binds C-c a → org-agenda, C-c c → org-capture, C-c l → org-store-link.
Language modules
Eachinit-lang-*.el file pins :mode regexes and provides font-lock for a group of related languages. Formatter configuration is centralised through apheleia in init-prog.el; LSP server hooks live in init-prog.el too.
init-lang-nix—nix-ts-mode. Formatting via apheleia (which ships anixfmtentry).init-lang-rust—rust-ts-mode(built-in) plus Cargo command wrappers.init-lang-python—python-ts-mode(built-in), the interpreter set topython3. The LSP server (pyright/basedpyright/ruff-lsp) comes from the project environment, not from this config.init-lang-go—go-ts-mode/go-mod-ts-mode/go-work-ts-mode(built-in), with a buffer-local gopls workspace config that turns on inlay hints, staticcheck, and theunusedparams/shadow/nilness/unusedwriteanalyses (eglot wired ininit-prog). Amajor-mode-remap-alistentry routes the classicgo-modetogo-ts-mode, so a stale autoload or missing grammar can never drop a.gofile into an unavailable classic mode. Addsgo-tag(struct-tag editing) andgotest(point-aware test/benchmark/coverage runs) under the buffer-localC-c C-tprefix. Formatting via apheleia →goimports; debugging via dape →dlv. All Go tooling (go,gopls,goimports,dlv,gomodifytags) comes from the project/host PATH.init-lang-web—typescript-ts-mode,tsx-ts-mode, CSS, HTML, JSON, and related tree-sitter modes.init-lang-devops— Dockerfile, docker-compose, Terraform (*.tf), gitlab-ci, justfile, Ansible, Bazel/Starlark (BUILD,WORKSPACE,MODULE.bazel,*.bzl,.bazelrc, …). Ajotain-docker-backenddefcustom ('podmanby default, set to'dockerfor classic Docker) switches the runtime command used bydockerfile-mode. Bazel buffers format on save via buildifier (centralised through apheleia ininit-prog).init-lang-data— YAML, CSV (withcsv-align-mode), SQL (sql-indent), Jinja2, gnuplot.init-lang-systems— C/C++ (cc-mode), CMake, Meson, Haskell, Zig (zig-ts-mode, eglot wireszls).
Adding a new module
- Create
lisp/init-<concern>.elwith the usual Elisp headers and alexical-binding: tcookie. - End the file with
(provide 'init-<concern>). - Add a
(require 'init-<concern>)line ininit.elat the appropriate point in the load order. - Run
just checkto confirm the parser is happy, thenjust compileto catch warnings.