Skip to main content

Modules

Jotain’s Elisp configuration is split across lisp/init-*.el, one file per concern. Each file is self-contained: a package that only exists to enhance a built-in (e.g. dirvishdired, magitvc, corfucompletion-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>.el under lisp/.
  • Every file starts with a lexical-binding: t cookie.
  • Every file ends with (provide 'init-<concern>).
  • Modules are loaded in order from init.el.
  • use-package-always-ensure is t (set in early-init.el), so every use-package block defaults to “install if missing”. Built-ins must opt out with :ensure nil. Packages provided by Nix use :ensure nil — Nix puts them on load-path, so use-package finds them without touching the network.
  • User options (defcustom variables) are set via setopt, not setq, so the :set callback and :type validation run.

Load order

init.el loads modules in this order:
init-core         GC, encoding, var/ paths, sane file-handling defaults
init-keys         Global keymap and window-split helpers
init-ui           Theme, modeline, fonts, frame tweaks, icons
init-tabs         Workspace tabs via tab-bar-mode
init-help         helpful + built-in help tweaks
init-docs         Register the bundled jotain.info with C-h i
init-editing      elec-pair, delsel, whitespace, region tools
init-completion   vertico, marginalia, orderless, consult, corfu, cape
init-navigation   dired, dirvish, project, windmove, winner
init-vc           vc, vc-jj, magit, majutsu, magit-todos, forge, diff-hl, smerge, ediff
init-prog         prog-mode, treesit, eglot, dape, flymake, eldoc, apheleia, compile
init-snippets     tempel snippets + eglot-tempel LSP expansion
init-project      project + projection + compile-multi
init-devenv       devenv.sh: tasks, processes, env, LSP, MCP
init-ai           claude-code-ide, gptel, mcp
init-shell        eshell, comint, ielm
init-terminal     ghostel terminal + tty integration (kkp, clipetty)
init-systems      sops, logview, auth-source-1password
init-tracking     keyfreq, wakatime, activity-watch
init-writing      jinx, markdown-mode, denote, pdf-tools
init-org          org, org-modern, capture templates

init-lang-nix
init-lang-rust
init-lang-python
init-lang-go
init-lang-web         TS/TSX/CSS/HTML/JSON
init-lang-devops      Dockerfile, terraform, just, ansible, bazel
init-lang-data        yaml, csv, sql, jinja2, gnuplot
init-lang-systems     C/C++, CMake, Meson, Haskell, Zig

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-sourceauth-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 aorg-agenda, C-c corg-capture, C-c lorg-store-link.

Language modules

Each init-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-nixnix-ts-mode. Formatting via apheleia (which ships a nixfmt entry).
  • init-lang-rustrust-ts-mode (built-in) plus Cargo command wrappers.
  • init-lang-pythonpython-ts-mode (built-in), the interpreter set to python3. The LSP server (pyright/basedpyright/ruff-lsp) comes from the project environment, not from this config.
  • init-lang-gogo-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 the unusedparams/shadow/nilness/unusedwrite analyses (eglot wired in init-prog). A major-mode-remap-alist entry routes the classic go-mode to go-ts-mode, so a stale autoload or missing grammar can never drop a .go file into an unavailable classic mode. Adds go-tag (struct-tag editing) and gotest (point-aware test/benchmark/coverage runs) under the buffer-local C-c C-t prefix. Formatting via apheleia → goimports; debugging via dape → dlv. All Go tooling (go, gopls, goimports, dlv, gomodifytags) comes from the project/host PATH.
  • init-lang-webtypescript-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, …). A jotain-docker-backend defcustom ('podman by default, set to 'docker for classic Docker) switches the runtime command used by dockerfile-mode. Bazel buffers format on save via buildifier (centralised through apheleia in init-prog).
  • init-lang-data — YAML, CSV (with csv-align-mode), SQL (sql-indent), Jinja2, gnuplot.
  • init-lang-systems — C/C++ (cc-mode), CMake, Meson, Haskell, Zig (zig-ts-mode, eglot wires zls).

Adding a new module

  1. Create lisp/init-<concern>.el with the usual Elisp headers and a lexical-binding: t cookie.
  2. End the file with (provide 'init-<concern>).
  3. Add a (require 'init-<concern>) line in init.el at the appropriate point in the load order.
  4. Run just check to confirm the parser is happy, then just compile to catch warnings.
Last modified on July 9, 2026