Initial commit for v1.0 of Simply Emacs.

This commit is contained in:
2026-07-24 09:47:24 +02:00
commit 7774e58c79
13 changed files with 1244 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
# Auto-generated by Custom (M-x customize) - not hand-edited, don't track
custom.el
# Local state / caches
backups/
auto-save/
auto-save-list/
eln-cache/
elpa/
transient/
tree-sitter/
.cache/
# Byte-compiled / elc
*.elc
places
projects
recentf
history
todo
+70
View File
@@ -0,0 +1,70 @@
# SimplyEmacs
> Vanilla Emacs at heart, with Evil scoped strictly to buffer editing and basic motion.
## Philosophy
SimplyEmacs is my personal, versioned Emacs setup — built up piece by piece and
treated like a project with tags and releases.
Simple idea: stick as close to vanilla Emacs as possible, but keep movement comfortable.
Evil handles normal modal editing inside regular text buffers, while in modes like
Dired or Magit it's stripped down to a paper-thin navigation layer (hjkl for moving
around) so all the native keybinds and features keep working as intended.
It gives you comfortable motion without burying how Emacs actually works.
## Layout
```
.
├── init.el # bootstrap, general UI, built-in settings, module loader
├── lisp/
│ ├── init-evil.el # evil-mode, ex commands, buffer-editing keybinds
│ ├── init-dired.el # evil-style navigation for dired
│ ├── init-completion.el # vertico, orderless, marginalia, corfu, consult, which-key
│ ├── init-project.el # project.el scoped to ~/Projects, with a sticky project pin
│ ├── init-build.el # simple build system around build.sh scripts
│ ├── init-dev.el # per-language tooling (Odin, Lua, TOML) + eglot
│ ├── init-git.el # version control with Magit
│ ├── init-theme.el # gruvbox-dark-hard + a few face tweaks
│ └── init-cheatsheet.el # in-Emacs keybinding reference (toggle with C-c /)
└── custom.el # auto-generated by M-x customize, gitignored
```
`init.el` is meant to stay readable as "what does bare Emacs look and feel
like" — general UI, backups/autosave, scrolling, removing confirmations, etc.
Everything else is one file per concern under `lisp/`, `require`d at the
bottom of `init.el`. Custom's auto-generated output is redirected to its
own `custom.el` (gitignored) so a stray `M-x customize` never shows up
as noise in a diff.
## Included
- **Completion** — vertico + vertico-posframe, orderless, marginalia, corfu, consult
- **Discovery** — which-key
- **Editing** — evil (buffer editing only)
- **Files** — dired, tuned with evil-style `hjkl` navigation
- **Dev tooling** — tree-sitter + odin-ts-mode + eglot (OLS) for Odin, plus lua-mode, toml-mode, and markdown-mode
- **Theme** — gruvbox-dark-hard, darkened background/fringe/line-numbers to `#111111`
## Install
```sh
git clone <this-repo> ~/.config/emacs
emacs
```
First launch will refresh package archives and install everything declared
via `use-package`.
## Adding a language
Add a new `;;;; <Language>` section to `lisp/init-dev.el` following the Odin
example (tree-sitter grammar source, mode package, eglot hook).
## Versioning
Tagged releases (`v1.0`, `v1.1`, ...) mark points where the config is in a
known-good, "I'd hand this to past-me" state.
Day-to-day tweaks happen over time and, once they've proven themselves,
eventually land in a version.
+44
View File
@@ -0,0 +1,44 @@
;;; early-init.el --- Pre-init: GC ceiling and frame chrome -*- lexical-binding: t; -*-
;;; Commentary:
;; Runs before package.el initialises and before the first frame is drawn, so
;; it is the only place two things can take effect:
;; 1. Raising the GC ceiling, so the whole startup (package load + init.el)
;; runs without garbage-collection pauses.
;; 2. Removing the menu/tool/scroll bars via `default-frame-alist', so they
;; are never drawn — toggling the minor modes in init.el instead draws
;; them and then removes them, a visible flicker on launch.
;;
;; Nothing here may depend on a package: none are loaded yet.
;;
;; IMPORTANT: the GC bump below is only half the pair. Restore a working value
;; once startup is done by adding this to init.el:
;;
;; (add-hook 'emacs-startup-hook
;; (lambda () (setq gc-cons-threshold (* 16 1024 1024))))
;;
;; Without it, GC never runs again and Emacs slowly eats memory.
;;; Code:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Garbage Collection
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Hold GC off for the whole startup; init.el lowers it back on
;; `emacs-startup-hook' (see the Commentary above).
(setq gc-cons-threshold most-positive-fixnum)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Frame Chrome (no flicker)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Disable the bars before the first frame exists.
(push '(menu-bar-lines . 0) default-frame-alist)
(push '(tool-bar-lines . 0) default-frame-alist)
(push '(vertical-scroll-bars) default-frame-alist)
;; Don't resize the frame when the UI/fonts change during startup.
(setq frame-inhibit-implied-resize t)
;;; early-init.el ends here
Executable
+217
View File
@@ -0,0 +1,217 @@
;;; init.el --- SimplyEmacs: bootstrap, UI, built-in settings -*- lexical-binding: t; -*-
;;; Commentary:
;; The "bare Emacs" layer: package bootstrap, general UI, fonts, built-in
;; behaviour, and the module loader. Anything beyond vanilla UI/built-in
;; settings lives in its own lisp/ file and is required near the bottom, so
;; this file stays readable as "what does bare Emacs look and feel like".
;;; Code:
;; Make Emacs find programs installed in ~/.local/bin
(add-to-list 'exec-path (expand-file-name "~/.local/bin"))
(setenv "PATH"
(concat (expand-file-name "~/.local/bin")
path-separator
(getenv "PATH")))
;; GC restore - see early-init.el
(add-hook 'emacs-startup-hook
(lambda () (setq gc-cons-threshold (* 16 1024 1024))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Package Setup
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(require 'package)
(setq package-archives
'(("gnu" . "https://elpa.gnu.org/packages/")
("nongnu" . "https://elpa.nongnu.org/nongnu/")
("melpa" . "https://melpa.org/packages/")))
(package-initialize)
(unless package-archive-contents
(package-refresh-contents))
(unless (package-installed-p 'use-package)
(package-install 'use-package))
(require 'use-package)
(setq use-package-always-ensure t)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Module Loader
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Anything beyond vanilla UI/built-in behaviour lives in its own lisp/ file
;; and is required at the bottom, so this file stays the "bare Emacs" layer.
(add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; General UI
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(global-so-long-mode 1)
(setq frame-resize-pixelwise t)
(setq inhibit-startup-screen t)
(setq initial-scratch-message nil)
(setq-default header-line-format mode-line-format)
(setq-default mode-line-format nil)
(blink-cursor-mode 0)
(setq ring-bell-function #'ignore)
(global-hl-line-mode 1)
(global-display-line-numbers-mode 1)
(column-number-mode 1)
(size-indication-mode 1)
(setq-default indent-tabs-mode nil)
(setq-default tab-width 4)
(fset 'yes-or-no-p #'y-or-n-p)
(repeat-mode 1)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Fonts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Main font
(set-face-attribute 'default nil
:family "JetBrainsMono Nerd Font"
:height 120
:weight 'regular)
;; Variable pitch font (used by Org, Markdown, etc. if enabled)
(set-face-attribute 'variable-pitch nil
:family "Noto Sans"
:height 130
:weight 'regular)
;; Fixed pitch should match the default font
(set-face-attribute 'fixed-pitch nil
:family "JetBrainsMono Nerd Font"
:height 120
:weight 'regular)
;; Slightly larger mode line
(set-face-attribute 'mode-line nil
:height 130)
(set-face-attribute 'mode-line-inactive nil
:height 130)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Keep Projects Clean (built-in)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(setq backup-directory-alist
`(("." . ,(expand-file-name "backups/" user-emacs-directory))))
(setq auto-save-file-name-transforms
`((".*" ,(expand-file-name "auto-save/" user-emacs-directory) t)))
(setq create-lockfiles nil)
(make-directory (expand-file-name "backups/" user-emacs-directory) t)
(make-directory (expand-file-name "auto-save/" user-emacs-directory) t)
(save-place-mode 1)
(savehist-mode 1)
(recentf-mode 1)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Convenience (built-in)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(setq scroll-conservatively 101)
(setq scroll-margin 15)
(electric-pair-mode 1)
(show-paren-mode 1)
;; Command Palette
(global-set-key (kbd "C-S-p") #'execute-extended-command)
(defun my/keyboard-quit ()
"Escape behaves like C-g."
(interactive)
(keyboard-quit))
(global-set-key (kbd "<escape>") #'my/keyboard-quit)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Don't ask so many questions (built-in)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Quit immediately
(setq confirm-kill-emacs nil)
;; Don't ask before killing buffers with running processes
(setq confirm-kill-processes nil)
;; Don't ask about active processes when exiting
(setq kill-buffer-query-functions nil)
;; Follow symlinks without asking
(setq vc-follow-symlinks t)
;; Revert changed files automatically
(global-auto-revert-mode 1)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Modules
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; vim motions used sparingly
(require 'init-evil)
;; file browser
(require 'init-dired)
;; code completion
(require 'init-completion)
;; project management
(require 'init-project)
;; project building
(require 'init-build)
;; development languages and lsp
(require 'init-dev)
;; version control git
(require 'init-git)
;; theme
(require 'init-theme)
;; cheatsheet
(require 'init-cheatsheet)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Custom
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Custom's auto-generated output goes to its own custom.el so a stray
;; `M-x customize' never leaves noise in this hand-written init.
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(when (file-exists-p custom-file)
(load custom-file))
(custom-set-variables
;; custom-set-variables was added by Custom.
;; If you edit it by hand, you could mess it up, so be careful.
;; Your init file should contain only one such instance.
;; If there is more than one, they won't work right.
'(package-vc-selected-packages
'((odin-ts-mode :url "https://github.com/Sampie159/odin-ts-mode"))))
(custom-set-faces
;; custom-set-faces was added by Custom.
;; If you edit it by hand, you could mess it up, so be careful.
;; Your init file should contain only one such instance.
;; If there is more than one, they won't work right.
)
;;; init.el ends here
+71
View File
@@ -0,0 +1,71 @@
;;; init-build.el --- Project build command -*- lexical-binding: t; -*-
;;; Commentary:
;; Build via built-in `compile', running ./build.sh from the project root.
;; compile saves buffers, reuses one output buffer, and turns file:line:col
;; into clickable jumps — a thin wrapper, not a terminal-send hack.
;;; Code:
;; Always show *compilation* as a fixed right-hand panel, so the toggle below
;; hits a predictable window.
(add-to-list 'display-buffer-alist
'("\\*compilation\\*"
(display-buffer-reuse-window display-buffer-in-side-window)
(side . right)
(window-width . 0.35)))
;; If t is too aggressive (e.g. auto-scrolls while trying to read mid-output
;; that hasn't errored yet), swap to:
;; (setq compilation-scroll-output 'first-error)
(setq compilation-scroll-output t)
(defun my/project-build (&optional args)
"Run ./build.sh from the project root, appending ARGS (a string) if given.
Errors if build.sh isn't readable there. Saves all buffers first."
(let* ((root (if (project-current)
(project-root (project-current))
default-directory))
(script (expand-file-name "build.sh" root))
(default-directory root))
(unless (file-readable-p script)
(user-error "No build.sh found in %s" root))
(save-some-buffers t)
(compile (string-trim (concat "./build.sh " (or args ""))))))
(defun my/build ()
"Build the current project via `./build.sh' with no arguments."
(interactive)
(my/project-build nil))
(defun my/build-with-options ()
"Build the current project, prompting for arguments to append."
(interactive)
(my/project-build (read-string "build.sh args: ")))
(defun my/toggle-build-panel ()
"Show/hide the *compilation* window from the last run (doesn't rebuild)."
(interactive)
(let ((buf (get-buffer "*compilation*")))
(cond
((null buf)
(user-error "No build output yet — run `my/build' first"))
((get-buffer-window buf)
(delete-window (get-buffer-window buf)))
(t
(display-buffer buf)))))
;; Global (not evil-scoped) so it works from any state or buffer.
;; C-c C-c build C-c C-o build w/ options C-j toggle panel
(global-set-key (kbd "C-c C-c") #'my/build)
(global-set-key (kbd "C-c C-o") #'my/build-with-options)
(global-set-key (kbd "C-j") #'my/toggle-build-panel)
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-c" "code"
"C-c c" "build"
"C-c o" "build (with options)"))
(provide 'init-build)
;;; init-build.el ends here
+248
View File
@@ -0,0 +1,248 @@
;;; init-cheatsheet.el --- In-Emacs keybinding reference -*- lexical-binding: t; -*-
;;; Commentary:
;; A static reference buffer listing this config's custom keybindings,
;; opened as a non-blocking side window — same window-management model as
;; the build panel in `init-build.el' — so it can be toggled in and out of
;; view without interrupting whatever else is on screen.
;;
;; Content is real Markdown, viewed read-only via `markdown-view-mode'
;; (from the `markdown-mode' package already pulled in by `init-dev.el')
;; so headers/tables/code spans get proper fontification for free.
;;; Code:
(require 'markdown-mode) ; markdown-view-mode; make the dep explicit
(add-to-list 'display-buffer-alist
'("\\*Cheatsheet\\*"
(display-buffer-reuse-window display-buffer-in-side-window)
(side . left)
(window-width . 0.4)))
(defconst my/cheatsheet-text
"# SimplyEmacs Cheat Sheet
Custom keybindings layered on top of vanilla Emacs. Toggle this buffer
any time with `C-c /`.
Many keys use a project/fallback pattern (`my/project-or`): one action
inside a known project, a `consult`-based fallback everywhere else.
## Navigation & Search *(Evil normal/motion state)*
| Key | In a project | Fallback |
|------------|----------------------------|-------------------|
| `C-e` | `project-dired` | `dired-jump` |
| `C-m` | `project-switch-to-buffer` | `consult-buffer` |
| `C-f` | `consult-ripgrep` | `consult-ripgrep` |
| `C-p` | `project-find-file` | `consult-fd` |
| `C-s` | `consult-line` | |
| `<return>` | `evil-ret` | |
`C-f` runs `consult-ripgrep` everywhere — it already scopes to the
project root when there is one, so it needs no project/fallback split.
## Projects
| Key | Action |
|-----------|-------------------------------------------------------------------------------|
| `C-c C-p` | Switch project — saves & closes current project's buffers first, then prompts |
Projects are rescanned from `~/Projects` on every startup (direct
children only, no symlinks, most-recently-modified first). Jumping into
a dependency's source keeps the *current project* pinned to your last
real project instead of hijacking to it.
## Build
| Key | Action |
|-----------|-----------------------------------------------------------|
| `C-c C-c` | Run `./build.sh` from project root (saves buffers first) |
| `C-c C-o` | Same, but prompts for extra args to append |
| `C-j` | Toggle the `*compilation*` output panel (right side, 35%) |
Requires `build.sh` at the project root; output is a real `compile`
buffer, so `M-g n` / `M-g p` jump between errors.
## Dev / Eglot (LSP)
Wired up for Odin (`.odin`, tree-sitter + `odin-ts-mode` + OLS).
| Key | Action |
|------------|----------------------------------|
| `C-c r` | `eglot-rename` |
| `C-c a` | `eglot-code-actions` |
| `K` norm | Full hover doc in a focused pane |
| `q` or `K` | Close the hover pane |
Add a new language: new `;;;; <Language>` section in `init-dev.el`.
## Git (Magit)
`C-c C-g` opens `magit-status` (replaces the lazygit TUI). Magit keeps
its own native keys — nothing reimplemented — evil's normal state just
shadows most of them by default, so they're restored below plus a couple
of hjkl navigation aliases.
### Workflow
- Expand only the file you're working on with `TAB`.
- Move between hunks with `j` / `k`.
- Press `s` on a hunk to stage only that hunk.
- Continue staging related hunks until they form a single logical change.
- Press `c c` to create a commit from the staged changes.
- Repeat for the remaining hunks, creating small, focused commits instead of one large commit.
> **Tip:** If a single hunk contains unrelated changes, press `e` to edit the patch before staging, allowing you to split the changes into separate commits.
| Key | Action |
|-----------|----------------------|
| `j` / `n` | next section |
| `s` | stage |
| `k` / `p` | previous section |
| `u` | unstage |
| `h` | up to parent section |
| `U` | unstage all |
| `RET` | visit thing at point |
| `x` | discard |
| `g r` | refresh |
| `q` | quit / bury buffer |
**Popups** *(transient menus)*:
| Key | Action |
|-----|----------------|
| `c` | commit |
| `m` | merge |
| `d` | diff |
| `r` | rebase |
| `b` | branch |
| `t` | tag |
| `l` | log |
| `z` | stash |
| `f` | fetch |
| `e` | ediff |
| `F` | pull |
| `$` | process buffer |
| `P` | push |
Covers the daily-driver surface, not magit's entire command set —
anything not listed falls through to evil's default on that key
(harmless, just `<escape>`).
## Dired
Evil `hjkl` grafted onto stock dired commands, remapped only so evil
doesn't swallow dired's own letters.
| Key | Action |
|-------------|---------------------------|
| `j` / `k` | next / previous line |
| `d` | flag for deletion |
| `h` | up a directory |
| `x` | execute flagged deletions |
| `l` / `RET` | open file or directory |
| `D` | delete now, no flag step |
| `q` | quit window |
| `R` | rename / move |
| `SPC` / `m` | mark |
| `C` | copy |
| `u` | unmark |
| `y` | copy filename as kill |
| `U` | unmark all |
| `!` | shell command on file(s) |
| `t` | toggle marks |
| `+` / `f` | create directory |
| `g r` | refresh listing |
| `a` | create empty file |
| `(` | toggle file details |
## Evil *(buffer editing only — dired/magit/etc. keep native binds)*
**Ex commands:**
| Command | Action |
|---------|---------------------|
| `:w` | save buffer |
| `:q` | kill current buffer |
| `:wq` | save and kill Emacs |
**Insert state:**
| Key | Action |
|-------|----------------------------------------------------|
| `TAB` | insert a literal tab (overrides major-mode indent) |
## In-buffer Completion (Corfu)
Auto-popup after 2 characters, 0.15s delay.
| Key | Action |
|-----------------|--------------------|
| `C-n` / `TAB` | next candidate |
| `C-p` / `S-TAB` | previous candidate |
| `RET` | insert selection |
| `<escape>` | quit completion |
## Minibuffer Completion
Vertico *(centered posframe)* + orderless matching + marginalia +
consult. Type fragments in any order to filter.
## Misc / Global
| Key | Action |
|----------------|-----------------------------------|
| `C-S-p` | `execute-extended-command` (M-x) |
| `<escape>` | acts like `C-g` everywhere |
| *(any prefix)* | which-key shows available options |
## Look & Feel
Theme: **gruvbox-dark-hard**, background/fringe/line-numbers darkened
to `#111111`. Font: JetBrainsMono Nerd Font @ 12pt. Line numbers and
current-line highlight on globally; scroll margin 15 lines.
"
"Reference text shown by `my/toggle-cheatsheet'.")
(define-derived-mode my/cheatsheet-mode markdown-view-mode "Cheatsheet"
"Read-only `markdown-view-mode' variant for the keybinding cheat sheet.")
(with-eval-after-load 'evil
(evil-define-key '(normal motion) my/cheatsheet-mode-map
(kbd "q") #'quit-window))
(defun my/cheatsheet-buffer ()
"Return the cheatsheet buffer, creating and populating it on first use."
(let ((buf (get-buffer-create "*Cheatsheet*")))
(with-current-buffer buf
(unless (derived-mode-p 'my/cheatsheet-mode)
(let ((inhibit-read-only t))
(erase-buffer)
(insert my/cheatsheet-text)
(my/cheatsheet-mode)
(goto-char (point-min)))))
buf))
(defun my/toggle-cheatsheet ()
"Toggle a non-blocking side window showing this config's cheat sheet.
Same show/hide-only-the-window behaviour as `my/toggle-build-panel' —
doesn't rebuild the buffer each time, just shows or hides it."
(interactive)
(let* ((buf (my/cheatsheet-buffer))
(win (get-buffer-window buf)))
(if win
(delete-window win)
(display-buffer buf))))
;; Bound globally (not evil-scoped), matching the build panel keys, so it
;; works the same from normal state, insert state, or anywhere else.
(global-set-key (kbd "C-c /") #'my/toggle-cheatsheet)
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-c /" "cheatsheet"))
(provide 'init-cheatsheet)
;;; init-cheatsheet.el ends here
+123
View File
@@ -0,0 +1,123 @@
;;; init-completion.el --- Minibuffer + in-buffer completion -*- lexical-binding: t; -*-
;;; Commentary:
;; Vertico + Orderless + Marginalia + Consult for minibuffer completion.
;; Corfu provides in-buffer completion.
;; Embark adds contextual actions.
;;; Code:
;;;; Helper functions
(defun my/find-file-below (file)
"Open FILE in a split below."
(interactive "fFile: ")
(select-window (split-window-below))
(find-file file))
(defun my/find-file-right (file)
"Open FILE in a split to the right."
(interactive "fFile: ")
(select-window (split-window-right))
(find-file file))
(defun my/vertico-split-below ()
"Open the selected Vertico file candidate below."
(interactive)
(setq unread-command-events (listify-key-sequence "2"))
(embark-act))
(defun my/vertico-split-right ()
"Open the selected Vertico file candidate to the right."
(interactive)
(setq unread-command-events (listify-key-sequence "3"))
(embark-act))
;;;; Vertico
(use-package vertico
:init
(vertico-mode)
:custom
(vertico-count 25))
(use-package vertico-posframe
:after vertico
:custom
(vertico-posframe-poshandler #'posframe-poshandler-frame-center)
(vertico-posframe-border-width 2)
(vertico-posframe-width 120)
(vertico-posframe-max-width 300)
:config
(vertico-posframe-mode))
;;;; Completion styles
(use-package orderless
:custom
(completion-styles '(orderless basic))
(completion-category-defaults nil)
(completion-category-overrides
'((file (styles partial-completion)))))
(use-package marginalia
:init
(marginalia-mode))
;;;; In-buffer completion
(use-package corfu
:init
(global-corfu-mode)
:custom
(corfu-auto t)
(corfu-auto-delay 0.15)
(corfu-auto-prefix 2)
(corfu-cycle t)
(corfu-preselect 'first)
:bind
(:map corfu-map
("C-n" . corfu-next)
("C-p" . corfu-previous)
("RET" . corfu-insert)
("<escape>" . corfu-quit))
:config
(with-eval-after-load 'evil
(evil-define-key 'insert 'global
(kbd "C-n") #'corfu-next
(kbd "C-p") #'corfu-previous)))
;;;; Minibuffer commands
(use-package consult)
;;;; Contextual actions
(use-package embark
:after vertico
:bind
(("C-." . embark-act)
:map embark-file-map
("2" . my/find-file-below)
("3" . my/find-file-right)
:map vertico-map
("C-s" . my/vertico-split-below)
("C-v" . my/vertico-split-right)))
(use-package embark-consult
:after (embark consult)
:hook
(embark-collect-mode . consult-preview-at-point-mode))
;;;; Help/discovery
(use-package which-key
:config
(which-key-mode))
(provide 'init-completion)
;;; init-completion.el ends here
+95
View File
@@ -0,0 +1,95 @@
;;; init-dev.el --- Language tooling -*- lexical-binding: t; -*-
;;; Commentary:
;; Per-language setup. Currently Odin (tree-sitter + odin-ts-mode + eglot/OLS)
;; plus markdown-mode. Add each language as its own section.
;;; Code:
;;;; Markdown
(use-package markdown-mode)
;;;; Odin
(setq treesit-language-source-alist
'((odin "https://github.com/tree-sitter-grammars/tree-sitter-odin")))
(use-package odin-ts-mode
:vc (:url "https://github.com/Sampie159/odin-ts-mode")
:mode "\\.odin\\'")
;;;; Lua
(use-package lua-mode
:ensure t)
;;;; TOML
(use-package toml-mode
:ensure t)
;;;; Eglot LSP
(use-package eglot
:hook (odin-ts-mode . eglot-ensure)
:bind
(:map eglot-mode-map
("C-c r" . eglot-rename)
("C-c a" . eglot-code-actions))
:config
;; Signature-only ElDoc: OLS hover already includes the signature, so
;; showing both would duplicate it in the echo area.
(add-hook
'eglot-managed-mode-hook
(lambda ()
(setq-local eldoc-documentation-functions
'(eglot-signature-eldoc-function))))
;; K is context-aware: on a diagnostic (red underline) it shows the error
;; text, otherwise the full hover payload — both in the same focused pane
;; (dismiss with q or K). The echo area keeps the live signature.
;; ] e / [ e jump between diagnostics, echoing each as you land.
(define-derived-mode my/eglot-hover-mode special-mode "Eglot-Hover"
"Major mode for the eglot hover / diagnostic pane.")
(with-eval-after-load 'evil
(evil-define-key '(normal motion) my/eglot-hover-mode-map
(kbd "q") #'quit-window
(kbd "K") #'quit-window))
(defun my/eglot-hover--display (string &rest _)
"Show STRING in a focused *eglot-hover* pane, or message if empty."
(if (and string (not (string-empty-p string)))
(let ((buf (get-buffer-create "*eglot-hover*")))
(with-current-buffer buf
(let ((inhibit-read-only t))
(erase-buffer)
(insert string)
(goto-char (point-min)))
(my/eglot-hover-mode))
(select-window (display-buffer buf))) ; focus the pane
(message "Nothing to show at point")))
(defun my/eglot-help-at-point ()
"Show diagnostics at point if any, else eglot hover docs — in the pane."
(interactive)
(let ((diags (flymake-diagnostics (point))))
(if diags
(my/eglot-hover--display
(mapconcat #'flymake-diagnostic-text diags "\n\n"))
(eglot-hover-eldoc-function #'my/eglot-hover--display))))
(with-eval-after-load 'evil
(evil-define-key 'normal 'eglot-mode-map
(kbd "K") #'my/eglot-help-at-point
(kbd "] e") #'flymake-goto-next-error
(kbd "[ e") #'flymake-goto-prev-error))
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-c r" "rename"
"C-c a" "code actions")))
(provide 'init-dev)
;;; init-dev.el ends here
+54
View File
@@ -0,0 +1,54 @@
;;; init-dired.el --- Dired with evil-flavoured navigation -*- lexical-binding: t; -*-
;;; Commentary:
;; Dired stays vanilla — every binding points at a stock dired command.
;; They're remapped only so evil's normal-state keys don't shadow dired's
;; own single-letter commands.
;;; Code:
;; Reuse the current dired buffer when descending/ascending (no pileup).
(setq dired-kill-when-opening-new-dired-buffer t)
;; Order directories first
(setq dired-listing-switches "-agho --group-directories-first")
(with-eval-after-load 'dired
(evil-define-key 'normal dired-mode-map
;; Navigation. RET and <return> both bound so evil's global <return>
;; doesn't shadow them here.
(kbd "j") #'dired-next-line
(kbd "k") #'dired-previous-line
(kbd "h") #'dired-up-directory
(kbd "l") #'dired-find-file
(kbd "RET") #'dired-find-file
[return] #'dired-find-file
(kbd "q") #'quit-window
;; Marking.
(kbd "SPC") #'dired-mark
(kbd "m") #'dired-mark
(kbd "u") #'dired-unmark
(kbd "U") #'dired-unmark-all-marks
(kbd "t") #'dired-toggle-marks
;; Deletion: d flags, x executes flags, D deletes now.
(kbd "d") #'dired-flag-file-deletion
(kbd "x") #'dired-do-flagged-delete
(kbd "D") #'dired-do-delete
;; File operations.
(kbd "R") #'dired-do-rename
(kbd "C") #'dired-do-copy
(kbd "y") #'dired-copy-filename-as-kill
(kbd "!") #'dired-do-shell-command
(kbd "+") #'dired-create-directory
(kbd "f") #'dired-create-directory
(kbd "a") #'dired-create-empty-file
;; Misc.
(kbd "g r") #'revert-buffer ; refresh listing
(kbd "(") #'dired-hide-details-mode)) ; toggle perms/size/date columns
(provide 'init-dired)
;;; init-dired.el ends here
+84
View File
@@ -0,0 +1,84 @@
;;; init-evil.el --- Evil, scoped to buffer editing -*- lexical-binding: t; -*-
;;; Commentary:
;; Evil is deliberately narrow: modal editing in text buffers only, no
;; evil-collection. Dired/Magit/etc. keep native bindings on purpose.
;; The nav keys below use `my/project-or': project-scoped command inside a
;; project, consult fallback everywhere else.
;;; Code:
(require 'project)
(defmacro my/project-or (project-cmd fallback-cmd)
"Call PROJECT-CMD inside a known project, else FALLBACK-CMD.
Both are unquoted command symbols."
`(lambda ()
(interactive)
(call-interactively
(if (project-current)
#',project-cmd
#',fallback-cmd))))
(defun my/insert-tab ()
"Insert a literal tab.
Bound to both TAB and <tab> to override major-mode indent in insert state;
`self-insert-command' can't handle the <tab> event (its arg is the symbol
`tab', not a character)."
(interactive)
(insert "\t"))
(use-package evil
:init
(setq evil-want-keybinding nil)
:config
(evil-mode 1)
;; Vim commands
(evil-ex-define-cmd "w" #'save-buffer)
(evil-ex-define-cmd "q" #'kill-current-buffer)
(evil-ex-define-cmd "wq" #'save-buffers-kill-terminal)
;; Half-page movement
(define-key evil-normal-state-map (kbd "C-d") #'evil-scroll-down)
(define-key evil-normal-state-map (kbd "C-u") #'evil-scroll-up)
;; Editor shortcuts
(dolist (map '(evil-normal-state-map
evil-motion-state-map))
(define-key (symbol-value map)
(kbd "C-e")
(my/project-or project-dired dired-jump))
(define-key (symbol-value map)
(kbd "C-s")
#'consult-line)
;; C-m is \r (= RET). Bind [return] to evil-ret so GUI Enter never falls
;; back to this C-m binding, leaving C-m free as its own key (GUI only).
(define-key (symbol-value map)
(kbd "C-m")
(my/project-or project-switch-to-buffer consult-buffer))
(define-key (symbol-value map)
[return]
#'evil-ret)
;; consult-ripgrep everywhere: it already scopes to the project root when
;; there is one, and shows hits in the posframe (RET jumps) like the other
;; finders — unlike project-find-regexp's separate xref pane.
(define-key (symbol-value map)
(kbd "C-f")
#'consult-ripgrep)
(define-key (symbol-value map)
(kbd "C-p")
(my/project-or project-find-file consult-fd))))
;; Literal tab on TAB/<tab> in insert state, overriding major-mode indent.
(with-eval-after-load 'evil
(evil-define-key 'insert 'global (kbd "TAB") #'my/insert-tab)
(evil-define-key 'insert 'global [tab] #'my/insert-tab))
(provide 'init-evil)
;;; init-evil.el ends here
+83
View File
@@ -0,0 +1,83 @@
;;; init-git.el --- Magit configuration -*- lexical-binding: t; -*-
;;; Commentary:
;; Use Magit with its native keybindings while preserving Evil's window
;; management.
;;
;; Magit buffers are displayed in a dedicated side window on the right.
;;
;; Custom keybindings:
;; C-c C-g - Open Magit
;; C-w - Evil window map
;; j - Next section
;; k - Previous section
;; q - Kill Magit buffer and close the side window
;;; Code:
(use-package magit
:after evil
:bind
(("C-c C-g" . magit-status))
:config
;; Magit appearance and behaviour.
;; Highlight the exact characters that changed.
(setq magit-diff-refine-hunk 'all)
;; Show more surrounding context in diffs.
(setq magit-diff-context-lines 5)
;; Show all transient options.
(setq transient-default-level 5)
;; Don't ask about saving modified buffers.
(setq magit-save-repository-buffers 'dontask)
;; Display Magit in a dedicated side window on the right.
(add-to-list
'display-buffer-alist
'("\\*magit"
(display-buffer-in-side-window)
(side . right)
(slot . 0)
(window-width . 0.45)
(dedicated . t)))
;; Custom commands.
(defun my/magit-quit ()
"Kill the current Magit buffer and close its window."
(interactive)
(quit-window t))
;; Keybindings.
(dolist (map '(magit-mode-map
magit-status-mode-map
magit-log-mode-map
magit-diff-mode-map
magit-revision-mode-map))
(with-eval-after-load 'magit
(when (boundp map)
(let ((keymap (symbol-value map)))
;; Keep Evil window commands.
(define-key keymap (kbd "C-w") evil-window-map)
;; Vim-style movement.
(define-key keymap (kbd "j") #'magit-section-forward)
(define-key keymap (kbd "k") #'magit-section-backward)
;; Quit Magit.
(define-key keymap (kbd "q") #'my/magit-quit)))))
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-c C-g" "Magit")))
(provide 'init-git)
;;; init-git.el ends here
+113
View File
@@ -0,0 +1,113 @@
;;; init-project.el --- Project switching, scoped to ~/Projects -*- lexical-binding: t; -*-
;;; Commentary:
;; Built on built-in project.el, no third-party manager. Projects are the
;; direct children of ~/Projects, rescanned authoritatively each startup so
;; the list never accumulates stale entries. A "pin" keeps the current
;; project from hijacking to a dependency's checkout on jump-to-definition.
;;; Code:
(require 'project)
;; A directory is a project root if under VC or containing one of these.
(setq project-vc-extra-root-markers '("build.sh"))
(defvar my/project-search-dirs
'("~/Projects")
"Parent directories scanned for known projects.")
;;;; Project list: rebuilt from a fresh scan each startup
;; project-remember-project would APPEND to the persisted list and leak stale
;; entries (old paths, stdlib dirs, the config repo). Instead scan ~/Projects
;; and assign project--list directly; a real list also stops project.el
;; re-reading the stale file this session.
(defun my/scan-projects-under (parent)
"Return `project--list' entries for direct child projects of PARENT.
Ignore symlinks; don't recurse."
(when (file-directory-p parent)
(let (entries)
(dolist (dir (directory-files parent t directory-files-no-dot-files-regexp))
(when (and (file-directory-p dir)
(not (file-symlink-p dir)))
(let ((default-directory dir))
(when-let ((project (project-current nil)))
(push (list (abbreviate-file-name (project-root project)))
entries)))))
entries)))
(setq project--list
(sort (mapcan #'my/scan-projects-under
(mapcar #'expand-file-name my/project-search-dirs))
(lambda (a b)
(time-less-p
(file-attribute-modification-time (file-attributes (car b)))
(file-attribute-modification-time (file-attributes (car a)))))))
;; Persist so the on-disk file matches (no stale artifact left behind).
(project--write-project-list)
;;;; Sticky project detection
;; project.el recomputes the project from the visited buffer's directory, so
;; jumping into a dependency's VC checkout (via `gd') would make THAT the
;; project. Instead pin the last project under our roots and reuse it for any
;; buffer outside them.
(defvar my/pinned-project nil
"Last project resolved from a buffer under `my/project-search-dirs'.")
(defun my/own-project-dir-p (dir)
"Return non-nil when DIR lives under one of `my/project-search-dirs'."
(and dir
(seq-some (lambda (parent)
(file-in-directory-p dir (expand-file-name parent)))
my/project-search-dirs)))
(defun my/project-pinned (dir)
"Resolve DIR to the pinned project when DIR is outside our roots.
Runs ahead of `project-try-vc', so foreign buffers never become current."
(and my/pinned-project
(not (my/own-project-dir-p dir))
my/pinned-project))
(defun my/remember-pinned-project (project)
"Record PROJECT as the pin when the current buffer is one of ours."
(when (and project (my/own-project-dir-p default-directory))
(setq my/pinned-project project))
project)
(add-hook 'project-find-functions #'my/project-pinned) ; before project-try-vc
(advice-add 'project-current :filter-return #'my/remember-pinned-project)
;;;; Switching and cleanup
;; Skip the "what do you want to do" menu entirely.
(setq project-switch-commands #'project-dired)
;; Widen what counts as "belongs to this project" for cleanup.
(setq project-kill-buffer-conditions
'(buffer-file-name
(major-mode . compilation-mode)
(major-mode . dired-mode)
(major-mode . shell-mode)
(major-mode . eshell-mode)
(major-mode . term-mode)))
(defun my/project-switch-fresh ()
"Save and close the current project before switching."
(interactive)
(when (project-current nil)
(save-some-buffers t)
(project-kill-buffers t))
(call-interactively #'project-switch-project))
(global-set-key (kbd "C-c C-p") #'my/project-switch-fresh)
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-c C-p" "switch project (fresh)"
"C-x p" "project"))
(provide 'init-project)
;;; init-project.el ends here
+21
View File
@@ -0,0 +1,21 @@
;;; init-theme.el --- Gruvbox, dark-hard variant -*- lexical-binding: t; -*-
;;; Commentary:
;; gruvbox-dark-hard, with background/fringe/line-numbers flattened to
;; #111111 and the vertico posframe matched to it.
;;; Code:
(use-package gruvbox-theme
:config
(load-theme 'gruvbox-dark-hard t)
(set-face-background 'vertico-posframe "#111111")
(set-face-background 'vertico-posframe-border "#6a9589"))
(set-face-attribute 'default nil :background "#111111")
(set-face-attribute 'fringe nil :background "#111111")
(set-face-attribute 'line-number nil :background "#111111")
(set-face-attribute 'line-number-current-line nil :background "#111111")
(provide 'init-theme)
;;; init-theme.el ends here