01 Modifier Key Patterns
Emacs keybindings follow a consistent logic. Learning the patterns lets you predict and remember bindings rather than memorizing each one individually.
The modifier hierarchy
| Pattern | Scope | Examples |
| C- (Control) | Character-level operations | C-f char forward, C-d delete char |
| M- (Meta/Option) | Word/sexp-level operations | M-f word forward, M-d delete word |
| C-M- (Control+Meta) | Expression/structural level | C-M-f sexp forward, C-M-d down list |
| C-x prefix | Global actions (files, buffers, frames) | C-x C-f find-file, C-x b switch-buffer |
| C-c prefix | Major-mode commands (user/mode-specific) | C-c C-c execute, C-c C-t org-todo |
| C-h prefix | Help system | C-h k describe-key, C-h f describe-function |
| s- (Super/⌘) | macOS-native operations | s-s save, s-c copy, s-f find |
Parallel motion bindings
Notice how the same letter does the same conceptual thing at different scales depending on modifier:
| Key | C- (character) | M- (word) | C-M- (sexp) |
| f | Forward character | Forward word | Forward sexp |
| b | Backward character | Backward word | Backward sexp |
| d | Delete character | Kill word forward | Down into list |
| k | Kill to end of line | Kill to end of sentence | Kill sexp |
| a | Beginning of line | Beginning of sentence | Beginning of defun |
| e | End of line | End of sentence | End of defun |
| h | (help prefix) | Mark paragraph | Mark defun |
Prefix argument (universal argument)
| Key sequence | Effect |
| C-u | Multiply next command by 4 (default prefix) |
| C-u C-u | Multiply by 16 |
| C-u 8 | Prefix argument of 8 |
| M-5 | Prefix argument of 5 (shorthand) |
| C-u 0 C-x e | Repeat keyboard macro until error |
| C-u C-SPC | Jump to previous mark position |
| C-u C-s | Repeat last search regexp |
💡 Prefix changes meaning, not just count
Many commands behave qualitatively differently with a prefix argument rather than just repeating. C-u C-l scrolls so the current line is at the top (not middle). C-u M-q justifies text (not just fills it). C-u C-x = gives detailed character info. Always try C-h k + the key to see what the prefix does.
02 Sexp & Structural Navigation
A sexp (s-expression) is a balanced unit of code: a word, number, string, or anything in matching delimiters () [] {} "". These bindings work in all programming modes.
| Key | Action |
| Horizontal movement |
| C-M-f | Move forward over one sexp |
| C-M-b | Move backward over one sexp |
| C-M-n | Move forward over list (parenthesized group) |
| C-M-p | Move backward over list |
| Vertical movement (nesting) |
| C-M-d | Move down into list (into parentheses) |
| C-M-u | Move up out of list (out of parentheses) |
| Defun navigation |
| C-M-a | Move to beginning of current function/defun |
| C-M-e | Move to end of current function/defun |
| C-M-h | Mark the current function (select it) |
| Killing by structure |
| C-M-k | Kill sexp forward |
| C-M-t | Transpose sexps |
| C-M-@ | Mark sexp (select balanced expression) |
| Indentation |
| C-M-q | Re-indent the current sexp |
| C-M-\ | Indent the selected region |
Paredit & Smartparens
Install paredit or smartparens for structured editing — these packages keep parentheses balanced as you type and add power-user operations:
;; Paredit — strict structural editing
(use-package paredit
:hook ((emacs-lisp-mode lisp-mode clojure-mode) . enable-paredit-mode))
;; Smartparens — more flexible alternative
(use-package smartparens
:hook (prog-mode . smartparens-mode)
:config (require 'smartparens-config))
| Paredit key | Action |
| C-) | Slurp forward — pull next sexp into current list |
| C-} | Barf forward — push last sexp out of current list |
| C-( | Slurp backward |
| C-{ | Barf backward |
| M-s | Splice — remove surrounding parentheses |
| M-r | Raise — replace parent sexp with current sexp |
| M-S | Split list at point |
| M-J | Join two adjacent lists |
03 Advanced Editing Operations
Sorting & Aligning
| Command (M-x) | Action |
sort-lines | Alphabetically sort lines in region |
sort-words | Sort words in region alphabetically |
sort-columns | Sort lines by the column content in region |
reverse-region | Reverse the order of lines in region |
align-regexp | Align text to a regexp pattern (great for tables) |
align | Align by current mode's alignment rules |
delete-duplicate-lines | Remove duplicate lines in region (Emacs 29+) |
Whitespace manipulation
| Key | Action |
| M-\ | Delete all whitespace around point |
| M-SPC | Collapse whitespace to a single space |
| C-x C-o | Delete blank lines around point |
| M-^ | Join current line with previous line |
| C-M-o | Split line at point (move rest to new line) |
M-x delete-trailing-whitespace | Remove trailing spaces from all lines |
M-x untabify | Convert tabs to spaces in region |
M-x tabify | Convert spaces to tabs where possible |
Zap-to-char and variations
| Key | Action |
| M-z c | Zap to character c — kill from point to next occurrence of c |
| M-Z c | Zap up to (not including) character c (Emacs 29+) |
| C-u M-z c | Zap to Nth occurrence of c |
Multiple cursors (package)
;; Install multiple-cursors
(use-package multiple-cursors
:bind (("C-S-c C-S-c" . mc/edit-lines)
("C->" . mc/mark-next-like-this)
("C-<" . mc/mark-previous-like-this)
("C-c C-<" . mc/mark-all-like-this)
("s-d" . mc/mark-next-like-this)))
| Key | Action |
| C-> | Add cursor at next occurrence of current word/region |
| C-< | Add cursor at previous occurrence |
| C-c C-< | Add cursors at all occurrences in buffer |
| C-S-c C-S-c | Edit all lines in region simultaneously |
| C-g | Return to single cursor |
04 Rectangle Operations
A rectangle is defined by the top-left and bottom-right corners of a column selection. Mark a region normally (with C-SPC) and then apply rectangle operations to the column block it defines.
| Key | Action |
| C-x r k | Kill rectangle (cut) |
| C-x r M-w | Copy rectangle to kill ring (Emacs 29+) |
| C-x r y | Yank (paste) last killed rectangle |
| C-x r d | Delete rectangle (remove text, close gap) |
| C-x r c | Clear rectangle (replace with spaces) |
| C-x r o | Open rectangle (insert blank space columns) |
| C-x r t | Replace rectangle with typed string (each line) |
| C-x r N | Insert line numbers into rectangle |
| C-x r r r | Copy rectangle to register r |
| C-x r i r | Insert rectangle from register r |
Rectangle Mark Mode (Emacs 26+)
For a more visual experience, use rectangle-mark-mode to see a highlighted column selection:
| Key | Action |
| C-x SPC | Toggle rectangle-mark-mode — visually highlights column |
💡 Rectangle use case: add column prefix
Position point at column 0, line 1. Set mark, move to end of the block. Then C-x r t and type // to comment out every line in the block simultaneously. Or use C-x r o to insert space before each line for indentation adjustments.
05 Registers & Bookmarks
Registers are single-character named slots that can hold positions, text, rectangles, numbers, window configurations, or framesets. They reset when Emacs exits.
Register overview
| Key | Stores… | Retrieves with… |
| C-x r SPC r | Buffer position (point) | C-x r j r |
| C-x r s r | Selected region as text | C-x r i r |
| C-x r r r | Rectangle | C-x r i r |
| C-x r w r | Window configuration | C-x r j r |
| C-x r f r | Frameset (all frames + windows) | C-x r j r |
| C-x r n r | Number (integer) | C-x r i r inserts it |
| C-x r + r | Increment number in register r | — |
Viewing registers
| Command | Action |
M-x list-registers | Show all registers and their contents |
M-x view-register | Display a specific register's contents |
Bookmarks — persistent across sessions
Unlike registers, bookmarks are saved to disk (~/.emacs.d/bookmarks) and persist between Emacs sessions.
| Key | Action |
| C-x r m | Set a bookmark at point (prompts for name) |
| C-x r b | Jump to a bookmark (with completion) |
| C-x r l | List all bookmarks in *Bookmark List* buffer |
M-x bookmark-save | Manually save bookmarks to disk now |
M-x bookmark-delete | Delete a bookmark by name |
M-x bookmark-rename | Rename a bookmark |
In the Bookmark List buffer
- d — mark for deletion, x — execute deletions
- r — rename bookmark, e — edit annotation
- o — open in other window, RET — open in this window
06 Keyboard Macros
Keyboard macros let you record a sequence of keystrokes and replay them, either once or thousands of times. They're one of Emacs' most powerful productivity features.
Start recordingC-x (
→
Do your editingtype, move, etc.
→
Stop recordingC-x )
→
Execute onceC-x e
→
Repeat N timesC-u 50 C-x e
| Key | Action |
| Basic recording & playback |
| C-x ( or F3 | Start recording a keyboard macro |
| C-x ) or F4 | Stop recording (or execute last macro if not recording) |
| C-x e | Execute last keyboard macro once; press e repeatedly to repeat |
| C-u 0 C-x e | Execute macro indefinitely until an error (end of buffer, etc.) |
| C-u N C-x e | Execute macro exactly N times |
| Naming and saving |
| C-x C-k n | Name the last defined macro (makes it an Emacs command) |
| C-x C-k b | Bind last macro to a key (prompts for key) |
M-x insert-kbd-macro | Insert Lisp definition of named macro into buffer (for saving) |
| Editing macros |
| C-x C-k e | Open the macro editor (kbd-macro-edit) |
| C-x C-k r | Apply macro to each line in region |
| C-x C-k C-t | Swap the last two macros in the ring |
| Macro ring |
| C-x C-k C-p | Previous macro in the macro ring |
| C-x C-k C-n | Next macro in the macro ring |
Inserting a query in a macro
During recording, press C-x q to insert a pause point. When the macro runs, Emacs will stop and prompt: press SPC to continue, DEL to skip this iteration, or RET to exit macro execution.
Saving macros between sessions
;; After naming a macro (C-x C-k n my-macro), save it in init.el:
M-x insert-kbd-macro RET my-macro RET
;; This inserts the defun into your current buffer — move it to init.el
⚡ Step-edit macros
Press C-x C-k C-e to open the step-edit interface. You can walk through a macro step by step — substituting different keystrokes mid-playback, inserting new steps, or deleting wrong ones. Essential for fixing complex macros.
07 Narrowing & Folding
Narrowing
Narrowing restricts the visible (and editable) portion of a buffer to a region. The rest of the buffer is invisible and untouchable — extremely useful for focused editing or batch operations on a subset of a file.
| Key | Action |
| C-x n n | Narrow to region (between point and mark) |
| C-x n w | Widen — restore full buffer view |
| C-x n d | Narrow to current defun (function) |
| C-x n p | Narrow to current page (delimited by ^L) |
| C-x n b | Narrow to org block (in org-mode) |
| C-x n s | Narrow to org subtree (in org-mode) |
💡 Enable narrowing
Narrowing is disabled by default (Emacs asks if you really meant it the first time). Enable it cleanly: (put 'narrow-to-region 'disabled nil) in your init.el.
Hide-Show (hs-minor-mode)
Built-in code folding for structured languages. Enable with M-x hs-minor-mode.
| Key | Action |
| C-c @ C-c | Toggle hide/show of current block |
| C-c @ C-h | Hide current block |
| C-c @ C-s | Show current block |
| C-c @ C-M-h | Hide all blocks in buffer |
| C-c @ C-M-s | Show all blocks in buffer |
08 Advanced Search Techniques
Isearch extras
| Key (during isearch) | Action |
| M-s . | Search for symbol under cursor (respects word boundaries) |
| M-s w | Toggle word search mode (whole words only) |
| M-s _ | Toggle symbol search mode |
| M-s r | Toggle regexp mode during isearch |
| M-s c | Toggle case-sensitivity during search |
| M-e | Edit the search string in minibuffer |
| M-p | Previous search string (isearch history) |
| M-n | Next search string (isearch history) |
| C-w | Add word at point to search string |
| C-y | Yank rest of line into search string |
| M-TAB | Complete search string from available choices |
| C-M-w | Delete last character added to search string |
Occur — list all matches
| Key | Action |
| M-s o | Run occur with the current search term |
M-x occur | Prompt for a regexp, show all matching lines |
M-x multi-occur | Run occur across multiple buffers |
M-x multi-occur-in-matching-buffers | Occur in buffers matching a regexp name |
In the *Occur* buffer:
- n / p — next / previous match
- RET — go to match in source buffer
- o — go to match in other window
- e — enter occur-edit-mode (edit matches in place)
- C-c C-c — finish editing (in occur-edit-mode)
- q — quit *Occur*
Highlight commands
| Key | Action |
| M-s h r | Highlight lines matching a regexp (different color each call) |
| M-s h l | Highlight phrases matching a regexp |
| M-s h p | Highlight phrase (literal string) |
| M-s h u | Unhighlight regexp under cursor |
| M-s h w | Highlight word at point |
M-x unhighlight-regexp | Remove a specific highlight |
Grep integration
| Command | Action |
M-x grep | Run grep and show results in a navigable buffer |
M-x rgrep | Recursive grep — prompts for pattern and directory |
M-x lgrep | Local grep — searches the current directory |
M-x grep-find | Run find | grep |
09 Abbrev, Dabbrev & Completion
Abbrev mode — text expansion
Define abbreviations that auto-expand as you type. For example, define btw → by the way.
| Key | Action |
| C-x a g | Add global abbrev (word before point → expansion you type) |
| C-x a l | Add local abbrev (only in current major mode) |
| C-x a i g | Inverse: type expansion first, then abbrev |
| C-x a e | Explicitly expand abbrev at point |
| M-' | Expand abbrev before point |
M-x abbrev-mode | Toggle abbrev-mode (auto-expand on SPC/RET) |
M-x list-abbrevs | Show all defined abbreviations |
M-x edit-abbrevs | Edit abbrevs in a buffer |
M-x write-abbrev-file | Save abbrevs to a file |
;; Auto-load abbrevs and enable abbrev-mode globally
(setq save-abbrevs 'silently)
(setq-default abbrev-mode t)
(quietly-read-abbrev-file)
Dabbrev — dynamic completion from buffer
| Key | Action |
| M-/ | Complete word by searching backward in buffer (dabbrev-expand) |
| M-/ again | Find the next completion candidate |
| C-M-/ | Complete with list of all dabbrev candidates |
Completion at point (capf)
| Key | Action |
| C-M-i or M-TAB | Complete symbol at point (mode-aware) |
| TAB | Indent or complete (depends on context and mode) |
Hippie Expand
A smarter expander that tries multiple completion strategies in order:
;; Replace dabbrev with hippie-expand (tries more strategies)
(global-set-key (kbd "M-/") 'hippie-expand)
(setq hippie-expand-try-functions-list
'(try-expand-dabbrev
try-expand-dabbrev-all-buffers
try-expand-dabbrev-from-kill
try-complete-file-name-partially
try-complete-file-name
try-expand-all-abbrevs
try-expand-list
try-expand-line
try-complete-lisp-symbol-partially
try-complete-lisp-symbol))
10 Shell & Process Integration
Inline shell commands
| Key | Action |
| M-! | Run shell command, show output in *Shell Command Output* |
| C-u M-! | Run shell command and insert output at point |
| M-| | Pipe selected region through shell command |
| C-u M-| | Pipe region through command, replace region with output |
| M-& | Run shell command asynchronously (doesn't block Emacs) |
Shell buffers
| Command | Description |
M-x shell | Start an interactive shell buffer (uses $SHELL) |
M-x eshell | Emacs' own Lisp-based shell (no terminal needed) |
M-x term | Full terminal emulator buffer |
M-x ansi-term | Terminal with ANSI color support |
M-x vterm | libvterm-based terminal (install separately; best performance) |
M-x compile | Run a compile command, navigate errors with C-x ` |
M-x async-shell-command | Run async command, show output in buffer |
In shell buffers:
- M-p / M-n — previous / next command in history
- C-c C-p / C-c C-n — move to previous / next prompt
- C-c C-c — interrupt current process (SIGINT)
- C-c C-z — suspend process
- C-c C-d — send EOF
Compilation mode navigation
| Key | Action |
| C-x ` | Jump to next error in compilation output |
| M-g n | Next error (works in compilation, grep, etc.) |
| M-g p | Previous error |
🍎 macOS PATH in shell buffers
Shell buffers inside Emacs.app inherit Emacs' PATH, not your shell's. Install exec-path-from-shell and run (exec-path-from-shell-initialize) in your init.el to fix this.
11 Built-in Version Control (VC)
Emacs has a built-in VC layer that works with Git, SVN, Mercurial, and others. It's separate from Magit (which is more powerful) but useful when Magit isn't installed.
| Key | Action |
| Core operations |
| C-x v v | vc-next-action — stage + commit current file (smart) |
| C-x v + | Update from remote (pull) |
| C-x v i | Register file with version control |
| Viewing changes |
| C-x v = | Show diff between working file and last revision |
| C-x v D | Show diff for entire directory |
| C-x v l | Show commit log for current file |
| C-x v L | Show commit log for current directory |
| C-x v a | Show annotated file (blame) |
| Branching & misc |
| C-x v b | Switch branch |
| C-x v s | Create a snapshot (tag) |
| C-x v u | Revert file to last revision |
| C-x v g | Annotate with git-blame |
12 Mode-Specific Key Bindings
Emacs Lisp mode
| Key | Action |
| C-M-x | Evaluate the defun at or before point |
| C-x C-e | Evaluate sexp before point |
| C-c C-b | Evaluate entire buffer |
| M-TAB | Complete Lisp symbol at point |
Python mode (python-ts-mode / python-mode)
| Key | Action |
| C-c C-c | Send buffer to Python process |
| C-c C-r | Send region to Python process |
| C-c C-z | Switch to Python REPL buffer |
| C-c C-d | Pydoc for symbol at point |
Markdown mode (package)
| Key | Action |
| C-c C-s b | Insert bold |
| C-c C-s i | Insert italic |
| C-c C-s c | Insert code span |
| C-c C-s C | Insert code block |
| C-c C-c p | Preview in browser |
Dired (revisited — advanced keys)
| Key | Action |
| M-% | Query-replace in all marked files |
| Q | Query replace across all marked files |
| ! | Run shell command on marked files |
| A | Search across all marked files (regexp) |
| W | Open marked files in external browser |
| m | Mark file, u — unmark, U — unmark all |
| * . | Mark all files with a given extension |
| * / | Mark all directories |
| i | Insert subdirectory into Dired listing |
| wdired (C-x C-q) | Edit filenames inline (writable Dired) |
13 Winner Mode, Tab Bar & Window History
Winner mode — undo window changes
(winner-mode 1) ; enable in init.el
| Key | Action |
| C-c ← | Undo last window configuration change |
| C-c → | Redo window configuration change |
Tab Bar (Emacs 27+)
Emacs has a native tab bar for window configurations — each tab holds an independent set of windows.
(tab-bar-mode 1) ; enable native tab bar
| Key | Action |
| C-x t 2 | Create new tab |
| C-x t 0 | Close current tab |
| C-x t o | Switch to next tab |
| C-x t r | Rename current tab |
| C-x t b | Switch to buffer in a specific tab |
| C-x t f | Open file in new tab |
| s-} / s-{ | Next / previous tab (macOS-style, if configured) |
Follow mode
Split a buffer into two side-by-side windows and follow-mode keeps them synchronized, showing consecutive pages of the same buffer:
(M-x) follow-mode ; toggle in any buffer after C-x 3
14 Configuration Architecture
A well-organized Emacs configuration avoids a monolithic init.el that grows unmanageable. Here are the main file locations and architectural patterns.
File locations
| File | Purpose | Loaded when |
~/.config/emacs/early-init.el | Pre-GUI, pre-package setup | Before anything else (Emacs 27+) |
~/.config/emacs/init.el | Main config entry point | After early-init, after packages |
~/.config/emacs/lisp/ | Your custom Elisp modules | When you require them |
~/.config/emacs/custom.el | Emacs' auto-generated customize output | When you call (load custom-file) |
Modular config pattern
;; init.el — loads other modules
(add-to-list 'load-path
(expand-file-name "lisp" user-emacs-directory))
;; Separate concerns into their own files
(require 'init-ui) ; UI settings, theme, fonts
(require 'init-editing) ; editing prefs, whitespace
(require 'init-completion) ; vertico, orderless, etc
(require 'init-git) ; magit config
(require 'init-org) ; org-mode setup
(require 'init-langs) ; language-specific config
(require 'init-macos) ; macOS-specific tweaks
;; Keep Customize's output out of init.el
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(when (file-exists-p custom-file) (load custom-file))
💡 Keep custom.el separate
Without specifying custom-file, Emacs appends auto-generated custom-set-variables blocks to the end of your init.el after running M-x customize. This pollutes hand-written config. Point it to a separate file and load it explicitly.
15 early-init.el
early-init.el is loaded before the package system and GUI are initialized (Emacs 27+). Use it for startup performance and pre-frame configuration.
;; ~/.config/emacs/early-init.el
;; ── PERFORMANCE: raise GC threshold during startup ──
(setq gc-cons-threshold most-positive-fixnum)
(setq gc-cons-percentage 0.6)
;; Restore reasonable GC after startup completes
(add-hook 'emacs-startup-hook
(lambda ()
(setq gc-cons-threshold 16777216) ; 16MB
(setq gc-cons-percentage 0.1)))
;; ── DISABLE PACKAGE.EL early if using straight.el ──
;; (setq package-enable-at-startup nil)
;; ── PREVENT FLASH OF UNSTYLED UI ──────────────────
;; Suppress UI elements before they paint
(push '(menu-bar-lines . 0) default-frame-alist)
(push '(tool-bar-lines . 0) default-frame-alist)
(push '(vertical-scroll-bars) default-frame-alist)
;; Set frame size before display (avoids resize flash)
(push '(width . 180) default-frame-alist)
(push '(height . 50) default-frame-alist)
;; ── macOS SPECIFIC ──────────────────────────────────
;; Transparent title bar (native-comp builds)
(push '(ns-transparent-titlebar . t) default-frame-alist)
(push '(ns-appearance . dark) default-frame-alist)
;; ── NATIVE COMPILATION (Emacs 28+) ─────────────────
(when (featurep 'native-compile)
;; Silence compiler warnings in *Warnings* buffer
(setq native-comp-async-report-warnings-errors 'silent)
(setq native-comp-deferred-compilation t))
⚡ Native compilation
Emacs 28+ can compile Elisp to native machine code via libgccjit, giving 40–80× speed gains for compute-intensive code. Install with brew install emacs-plus@29 --with-native-comp. Packages are compiled asynchronously on first load, and cached at ~/.cache/emacs/eln-cache/. After initial setup the speed gains are permanent.
16 use-package Deep Dive
use-package (built into Emacs 29+) is the standard way to declare, configure, and lazy-load packages. Every keyword controls a different aspect of the package lifecycle.
;; Fully annotated use-package example
(use-package some-package
;; :ensure — auto-install from package archive
:ensure t
;; :pin — install only from a specific archive
:pin melpa-stable
;; :demand — force eager loading (no lazy load)
:demand t
;; :defer — delay loading (t = any positive trigger)
;; :defer 2 = load 2 seconds after startup
:defer 2
;; :after — load only after these packages are loaded
:after (other-pkg another-pkg)
;; :if — only use-package if condition is true
:if (eq system-type 'darwin)
;; :when / :unless — conditional loading aliases
:when window-system
;; :init — runs BEFORE package is loaded
:init
(setq some-var 42)
;; :config — runs AFTER package is loaded
:config
(some-package-mode 1)
(setq some-package-setting t)
;; :custom — sets variables via customize system
:custom
(some-package-delay 0.3)
(some-package-feature-enabled t "Enable the feature")
;; :custom-face — sets faces via customize
:custom-face
(some-package-face ((t (:foreground "#2dd4bf"))))
;; :bind — global key bindings (also causes deferred load)
:bind
("C-c s" . some-command)
("C-c S" . some-other-command)
;; :bind-keymap — bind a prefix key to a keymap
:bind-keymap
("C-c p" . some-package-map)
;; :bind* — override mode-local bindings globally
:bind*
("C-j" . some-very-important-command)
;; :hook — add to hooks (loads package when hook fires)
:hook
(prog-mode . some-package-mode)
((text-mode org-mode) . some-package-mode)
;; :mode — auto-activate for file extensions
:mode
("\\.xyz\\'" . some-package-mode)
;; :interpreter — auto-activate for script shebangs
:interpreter ("python3" . some-package-mode)
;; :magic — activate based on file content (magic bytes)
:magic ("\\(BEGIN\\)" . some-package-mode)
;; :commands — declare commands so autoloads work
:commands (some-command some-other-command)
;; :functions / :defines — suppress byte-compiler warnings
:functions (some-external-fn)
:defines (some-external-var)
;; :no-require — don't (require 'some-package) in :config
:no-require t
;; :preface — evaluated first, before everything else
:preface
(defun my-helper () ...))
Practical patterns
;; Vertico + Orderless + Marginalia (completion stack)
(use-package vertico
:init (vertico-mode)
:custom (vertico-cycle t))
(use-package orderless
:custom
(completion-styles '(orderless basic))
(completion-category-overrides
'((file (styles basic partial-completion)))))
(use-package marginalia
:after vertico
:init (marginalia-mode))
(use-package consult
:bind (("C-x b" . consult-buffer)
("M-y" . consult-yank-pop)
("M-g g" . consult-goto-line)
("C-c r" . consult-recent-file)
("C-s" . consult-line)))
17 The Hooks System
Hooks are lists of functions called at specific points in Emacs' lifecycle. They're the primary mechanism for mode-dependent and event-driven configuration.
Event fires →
prog-mode-hook →
[display-line-numbers-mode, flycheck-mode, company-mode, ...]
;; Basic hook usage
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
(add-hook 'before-save-hook #'delete-trailing-whitespace)
(add-hook 'text-mode-hook #'visual-line-mode)
;; Remove a hook
(remove-hook 'prog-mode-hook #'display-line-numbers-mode)
;; Hook with an inline lambda
(add-hook 'emacs-lisp-mode-hook
(lambda ()
(setq-local tab-width 2)
(flycheck-mode -1)))
;; Depth argument: lower runs earlier (-100 to 100, default 0)
(add-hook 'after-init-hook #'my-startup-fn 90) ; run late
(add-hook 'after-init-hook #'early-setup-fn -50) ; run early
Essential hooks reference
| Hook name | When it fires |
| Startup & init |
emacs-startup-hook | After init.el is fully loaded (best for most startup tasks) |
after-init-hook | After normal init, before emacs-startup-hook |
window-setup-hook | After frame is fully set up and visible |
| File & buffer |
find-file-hook | After visiting any file |
before-save-hook | Just before saving a buffer |
after-save-hook | Just after saving a buffer |
kill-buffer-hook | Before killing a buffer |
after-revert-hook | After reverting a buffer from disk |
| Mode hooks (each major mode has one) |
prog-mode-hook | Any programming mode (parent of python-mode, etc.) |
text-mode-hook | Any text mode (parent of org-mode, markdown-mode, etc.) |
emacs-lisp-mode-hook | Specifically Emacs Lisp files |
org-mode-hook | Org-mode files |
| Minibuffer |
minibuffer-setup-hook | When minibuffer opens |
minibuffer-exit-hook | When minibuffer closes |
Mode-local variables with setq-local
;; Set variable only in this buffer (not globally)
(add-hook 'python-mode-hook
(lambda ()
(setq-local fill-column 88) ; Black formatter width
(setq-local tab-width 4)
(setq-local indent-tabs-mode nil)))
18 The Advice System
Advice lets you modify the behavior of any existing Emacs function without changing its source code. You wrap it with code that runs before, after, or around the original.
:before
your code runs first — can inspect args
:original
the-advised-function (runs normally)
:after
your code runs last — can inspect result
;; Modern advice API (advice-add)
;; :before — run before the original, can see args
(advice-add 'find-file :before
(lambda (filename &optional wildcards)
(message "Opening: %s" filename)))
;; :after — run after the original
(advice-add 'save-buffer :after
(lambda (&rest args)
(message "Saved at %s" (format-time-string "%H:%M:%S"))))
;; :around — wraps the original; you control if/when it runs
;; orig-fn is the original function passed as first arg
(advice-add 'message :around
(lambda (orig-fn &rest args)
(unless (string-match-p "^Loading" (car args))
(apply orig-fn args)))) ; suppress "Loading..." messages
;; :override — completely replaces the original
(advice-add 'yes-or-no-p :override #'y-or-n-p) ; always use y/n
;; :before-while — run before; if returns nil, skip original
;; :before-until — run before; if returns non-nil, skip original
;; :after-while — run after; original must return non-nil
;; :filter-args — transform args before passing to original
;; :filter-return — transform original's return value
;; Remove advice
(advice-remove 'find-file 'my-find-file-advice)
;; Name an advice so you can remove it later
(advice-add 'kill-buffer :before #'my-before-kill :name "my-kill-advice")
(advice-remove 'kill-buffer "my-kill-advice")
Practical advice examples
;; Make C-x C-c ask for confirmation before quitting
(advice-add 'save-buffers-kill-terminal :before-while
(lambda (&rest _)
(y-or-n-p "Really quit Emacs? ")))
;; Automatically create parent directories when visiting a new file
(advice-add 'find-file :before
(lambda (filename &rest _)
(let ((dir (file-name-directory filename)))
(when (and dir (not (file-exists-p dir)))
(make-directory dir t)))))
19 Keybindings in Depth
The keymap hierarchy
Emacs looks up a keybinding through a hierarchy of keymaps. The most local wins:
- overriding-local-map — highest priority (rarely used)
- Text properties — key maps on text (links, buttons)
- Minor mode maps — active minor modes, in reverse order of activation
- Local map — the current major mode's keymap
- Global map — the fallback for everything
Binding functions
;; global-set-key — adds to the global map
(global-set-key (kbd "C-c j") 'jump-to-register)
;; define-key — more flexible, specify the keymap
(define-key global-map (kbd "C-c j") 'jump-to-register)
(define-key org-mode-map (kbd "C-c ]") 'org-ref-insert-link)
(define-key emacs-lisp-mode-map (kbd "C-c C-e") 'eval-defun)
;; local-set-key — set in current buffer's local map
(local-set-key (kbd "C-c x") 'my-local-command)
;; keymap-global-set (Emacs 29+ cleaner API)
(keymap-global-set "C-c j" 'jump-to-register)
(keymap-set org-mode-map "C-c ]" 'org-ref-insert-link)
Creating prefix maps
;; Create your own C-c p prefix for personal commands
(define-prefix-command 'my-personal-map)
(global-set-key (kbd "C-c p") 'my-personal-map)
(define-key my-personal-map (kbd "d") 'my-insert-date)
(define-key my-personal-map (kbd "f") 'my-find-file)
(define-key my-personal-map (kbd "r") 'my-reload-config)
Unbinding keys
;; Unbind a key (set to nil)
(global-unset-key (kbd "C-z")) ; disable suspend-frame
(global-unset-key (kbd "s-p")) ; disable macOS print
(global-unset-key (kbd "s-t")) ; disable macOS new tab
;; Or bind to ignore (passes no-op)
(global-set-key (kbd "C-z") 'ignore)
Hydra — transient key menus
;; Hydra: hold a key, get a temporary keymap with hints
(use-package hydra)
(defhydra hydra-zoom (global-map "C-=")
"zoom"
("+" text-scale-increase "in")
("-" text-scale-decrease "out")
("0" (text-scale-set 0) "reset" :exit t)
("q" nil "quit" :exit t))
;; After C-=, press +/-/0/q to control zoom — hint shown in echo area
Which-key configuration
;; which-key shows available next keys as you type a prefix
(use-package which-key
:init (which-key-mode)
:custom
(which-key-idle-delay 0.5) ; show after 0.5s pause
(which-key-max-display-columns 4) ; 4 columns
(which-key-sort-order 'which-key-key-order-alpha)
:config
;; Add descriptions to your prefix keys
(which-key-add-key-based-replacements
"C-c p" "personal"
"C-x r" "registers"
"C-x 4" "other-window"
"C-c &" "yasnippet"))
20 Faces, Themes & Colors
A face is a named set of text display properties (foreground color, background, font weight, underline, etc.). Every visual element in Emacs — syntax highlighting, the mode line, the cursor — is controlled by a face.
Setting faces in init.el
;; set-face-attribute — the primary face configuration function
;; Arguments: face, frame (nil = all frames), then keyword/value pairs
(set-face-attribute 'default nil
:family "JetBrains Mono"
:height 140 ; height in 1/10ths of a point
:weight 'regular)
;; Set a specific coding font just for comments
(set-face-attribute 'font-lock-comment-face nil
:slant 'italic
:family "Menlo")
;; Customize the cursor
(set-default 'cursor-type 'bar) ; bar | box | hollow | hbar
(set-face-attribute 'cursor nil :background "#2dd4bf")
;; Customise fringe
(set-face-attribute 'fringe nil
:background "#f2f0eb") ; match page background
;; Make line numbers less intrusive
(set-face-attribute 'line-number nil
:foreground "#c0bcc0"
:background "#f8f7f2")
Defining your own face
(defface my-important-face
'((((class color) (min-colors 88) (background dark))
(:foreground "#fbbf24" :weight bold :underline t))
(((class color) (min-colors 88) (background light))
(:foreground "#92400e" :weight bold :underline t))
(t (:weight bold)))
"Face for important text."
:group 'my-faces)
Loading and customizing themes
;; Load a built-in theme
(load-theme 'modus-operandi t) ; light — best for Retina
(load-theme 'modus-vivendi t) ; dark
;; Or use doom-themes
(use-package doom-themes
:config
(load-theme 'doom-one t)
(doom-themes-visual-bell-config)
(doom-themes-org-config))
;; Override specific faces AFTER loading a theme
(with-eval-after-load 'doom-themes
(set-face-attribute 'line-number-current-line nil
:foreground "#2dd4bf"
:weight 'bold)
(set-face-attribute 'mode-line nil
:height 120)) ; 12pt mode line text
Writing a minimal theme from scratch
;; Save as ~/.config/emacs/themes/my-theme-theme.el
(deftheme my-theme "My personal theme.")
(custom-theme-set-faces
'my-theme
`(default ((t (:background "#1a1b2e" :foreground "#e2d9f3"))))
`(cursor ((t (:background "#2dd4bf"))))
`(fringe ((t (:background "#1a1b2e"))))
`(mode-line ((t (:background "#0d1117" :foreground "#8b949e"
:box (:line-width 1 :color "#21262d")))))
`(mode-line-inactive ((t (:background "#161b22" :foreground "#484f58"))))
`(font-lock-keyword-face ((t (:foreground "#ff7b72" :weight bold))))
`(font-lock-string-face ((t (:foreground "#a5d6ff"))))
`(font-lock-comment-face ((t (:foreground "#6e7681" :slant italic))))
`(font-lock-function-name-face ((t (:foreground "#79c0ff" :weight bold))))
`(font-lock-type-face ((t (:foreground "#ffa657"))))
`(font-lock-variable-name-face ((t (:foreground "#e6edf3"))))
`(org-level-1 ((t (:foreground "#2dd4bf" :weight bold :height 1.3))))
`(org-level-2 ((t (:foreground "#a78bfa" :weight bold :height 1.15))))
`(region ((t (:background "#2d313a")))))
(custom-theme-set-variables
'my-theme
'(line-spacing 0.15))
(provide-theme 'my-theme)
;; In init.el, load it:
(add-to-list 'custom-theme-load-path
(expand-file-name "themes" user-emacs-directory))
(load-theme 'my-theme t)
21 Mode Line Customization
The mode line is fully customizable through the mode-line-format variable — a list of elements that are evaluated and concatenated.
Built-in elements
;; Default mode-line-format for reference
mode-line-format
;; ⇒ ("%e" mode-line-front-space mode-line-mule-info
;; mode-line-client mode-line-modified mode-line-remote
;; mode-line-frame-identification mode-line-buffer-identification
;; " " mode-line-position
;; (vc-mode vc-mode) " " mode-line-modes mode-line-misc-info)
;; Custom minimal mode line
(setq-default mode-line-format
'("%e"
; modified indicator: ** if modified, -- if not
(:eval (if (buffer-modified-p) " ◉ " " ○ "))
; buffer name
mode-line-buffer-identification
" "
; line:column position
("%l:%c")
" "
; major mode
mode-name
" "
; git branch (if vc is active)
(:eval (when vc-mode
(concat " ⎇ "
(string-trim vc-mode "^ Git[:-]")))))))
doom-modeline configuration
(use-package doom-modeline
:init (doom-modeline-mode 1)
:custom
(doom-modeline-height 28)
(doom-modeline-bar-width 4)
(doom-modeline-icon t) ; requires nerd-icons
(doom-modeline-major-mode-icon t)
(doom-modeline-buffer-file-name-style 'truncate-upto-project)
(doom-modeline-github nil) ; disable GitHub notifications
(doom-modeline-lsp t)
(doom-modeline-time t))
;; Install icon fonts (run once)
(use-package nerd-icons
:if (display-graphic-p))
;; M-x nerd-icons-install-fonts ← run once to download fonts
22 Display & Visual Tweaks
Line display settings
;; Line numbers
(global-display-line-numbers-mode t)
(setq display-line-numbers-type 'relative) ; or t, 'visual
(setq display-line-numbers-width 4)
;; Line wrapping
(setq-default truncate-lines t) ; no wrapping by default
(global-visual-line-mode 1) ; soft wrap at window edge
;; 80-column indicator
(global-display-fill-column-indicator-mode t)
(setq fill-column 80)
(set-face-attribute 'fill-column-indicator nil
:foreground "#e0ddd8")
;; Line spacing (extra pixels between lines)
(setq-default line-spacing 0.2) ; 20% of line height
Fringe and margin
;; Fringe width (left and right gutters)
(fringe-mode '(8 . 8)) ; 8px left, 8px right
(fringe-mode 0) ; no fringe at all
;; Left margin (extra text indent)
(setq-default left-margin-width 2)
(setq-default right-margin-width 2)
;; Padding around the text in a frame
(add-to-list 'default-frame-alist '(internal-border-width . 20))
Scrolling behavior
;; Smooth scrolling
(setq scroll-margin 5) ; keep 5 lines of context
(setq scroll-conservatively 101) ; scroll minimally
(setq scroll-preserve-screen-position t)
;; Pixel scrolling (Emacs 29+) — smooth like macOS
(when (fboundp 'pixel-scroll-precision-mode)
(pixel-scroll-precision-mode t))
;; Mouse scrolling speed
(setq mouse-wheel-scroll-amount '(2 ((shift) . 5)))
(setq mouse-wheel-progressive-speed nil) ; don't accelerate
Font configuration for macOS
;; Multiple fonts for different face categories
(defun my/setup-fonts ()
"Set up fonts for macOS Retina display."
;; Monospace: JetBrains Mono or SF Mono
(set-face-attribute 'default nil
:family "JetBrains Mono" :height 140)
;; Variable pitch for prose (org, markdown)
(set-face-attribute 'variable-pitch nil
:family "SF Pro Text" :height 150)
;; Fixed pitch for code blocks in org
(set-face-attribute 'fixed-pitch nil
:family "JetBrains Mono" :height 130)
;; Emoji fallback
(set-fontset-font t 'emoji (font-spec :family "Apple Color Emoji")))
(add-hook 'emacs-startup-hook #'my/setup-fonts)
(; Also run when creating a new frame)
(add-hook 'server-after-make-frame-hook #'my/setup-fonts)
;; Enable mixed-pitch in org-mode (variable pitch prose, fixed pitch code)
(use-package mixed-pitch
:hook (org-mode . mixed-pitch-mode))
23 Directory-Local Variables
A .dir-locals.el file in any directory sets Emacs variables for all files in that directory tree. Essential for per-project settings without polluting your global config.
;; .dir-locals.el — place in your project root
((nil ; nil = applies to all modes
(indent-tabs-mode . nil)
(fill-column . 120)
(require-final-newline . t))
(python-mode ; only for Python files
(python-indent-offset . 4)
(flycheck-python-flake8-executable . "~/.venv/bin/flake8"))
(c-mode ; only for C files
(c-file-style . "linux")
(tab-width . 8)
(indent-tabs-mode . t))
(js-mode
(js-indent-level . 2)))
Create a .dir-locals.el file with M-x add-dir-local-variable (prompts for mode, variable, and value), or add one manually. Emacs asks for confirmation before loading unsafe variables.
Marking variables as safe
;; Silence the "risky variable" prompt for specific variables
(put 'python-indent-offset 'safe-local-variable #'integerp)
(put 'fill-column 'safe-local-variable #'integerp)
(put 'c-file-style 'safe-local-variable #'stringp)
24 Emacs Daemon Mode on macOS
Running Emacs as a daemon means it starts once and stays resident — opening files via emacsclient is near-instant. On macOS you can auto-start the daemon at login via launchd.
Manual daemon usage
# Start the daemon from terminal
$ emacs --daemon
# Open a file in the running daemon (GUI window)
$ emacsclient -c myfile.txt
# Open in terminal (no GUI frame)
$ emacsclient -t myfile.txt
# Open file in existing frame
$ emacsclient -n myfile.txt
# Shutdown the daemon gracefully
$ emacsclient -e '(kill-emacs)'
Shell aliases in ~/.zshrc
# Add to ~/.zshrc for convenience
alias e='emacsclient -c' # open in new GUI frame
alias et='emacsclient -t' # open in terminal
alias em='emacsclient -n' # open, no wait
alias ekill='emacsclient -e "(kill-emacs)"'
# Set EDITOR/VISUAL to emacsclient for git, etc.
export EDITOR='emacsclient -t'
export VISUAL='emacsclient -c'
Auto-start at login with launchd
# Create plist file for launchd
$ mkdir -p ~/Library/LaunchAgents
<!-- ~/Library/LaunchAgents/gnu.emacs.daemon.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>gnu.emacs.daemon</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/emacs</string> <!-- Apple Silicon -->
<string>--fg-daemon</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/tmp/emacs-daemon.log</string>
</dict>
</plist>
# Load the plist into launchd (enable at login)
$ launchctl load -w ~/Library/LaunchAgents/gnu.emacs.daemon.plist
# Check status
$ launchctl list | grep emacs
# Unload (disable)
$ launchctl unload ~/Library/LaunchAgents/gnu.emacs.daemon.plist
init.el tweaks for daemon mode
;; Defer font/theme setup to when a frame is created
;; (daemon starts without a frame)
(add-hook 'server-after-make-frame-hook
(lambda ()
(when (display-graphic-p)
(my/setup-fonts)
(load-theme 'doom-one t))))
;; Prevent daemon from quitting when last client disconnects
(setq server-kill-new-buffers nil)
(add-hook 'server-done-hook
(lambda ()
(kill-buffer (current-buffer))))
🍎 macOS app alternative
If you use Emacs.app (from emacsformacosx.com), you can set it to open quickly from the Dock. The app itself handles its own startup; each double-click or Open-With re-uses the running instance. Combine with emacsclient by adding /Applications/Emacs.app/Contents/MacOS/bin to your PATH.
25 Performance Tuning
Measuring startup time
# Quick measurement from the terminal
$ emacs --batch --eval '(message "%.2f" (float-time (emacs-init-time)))'
# In-depth profiling with esup package
(use-package esup
:commands esup)
;; Run: M-x esup
Key performance settings
;; ── GARBAGE COLLECTION ───────────────────────────
;; Use gcmh (Garbage Collector Magic Hack)
(use-package gcmh
:init (gcmh-mode 1)
:custom
(gcmh-idle-delay 5) ; GC after 5s idle
(gcmh-high-cons-threshold 100000000)) ; 100MB threshold during work
;; ── FILE NAME HANDLER ────────────────────────────
;; Temporarily disable during startup (saves ~200ms)
(defvar my/file-name-handler-alist file-name-handler-alist)
(setq file-name-handler-alist nil)
(add-hook 'emacs-startup-hook
(lambda ()
(setq file-name-handler-alist my/file-name-handler-alist)))
;; ── LSP PERFORMANCE ──────────────────────────────
(setq read-process-output-max (* 1024 1024)) ; 1MB
(setq lsp-idle-delay 0.5)
(setq lsp-log-io nil) ; don't log LSP traffic
;; ── LAZY LOADING ─────────────────────────────────
;; Use :defer t in use-package where possible
(use-package magit :defer t)
(use-package org :defer t)
;; ── FONT RENDERING ON MACOS ──────────────────────
(setq inhibit-compacting-font-caches t) ; faster large-font rendering
Profiling with the built-in profiler
;; Start profiler, do slow action, then report
(M-x) profiler-start
;; ... do whatever is slow ...
(M-x) profiler-report ; shows a tree of time spent per function
(M-x) profiler-stop
26 Writing Your Own Commands
Any function declared with (interactive) becomes a command that can be run with M-x or bound to a key. Here are useful patterns for macOS users.
;; ── BASIC INTERACTIVE COMMAND ────────────────────
(defun my/insert-date ()
"Insert the current date in ISO format at point."
(interactive)
(insert (format-time-string "%Y-%m-%d")))
(global-set-key (kbd "C-c i d") #'my/insert-date)
;; ── COMMAND WITH PREFIX ARG ──────────────────────
(defun my/duplicate-line (n)
"Duplicate the current line N times."
(interactive "p") ; "p" = numeric prefix arg
(let ((line (buffer-substring (line-beginning-position)
(line-end-position))))
(dotimes (_ n)
(end-of-line)
(newline)
(insert line))))
(global-set-key (kbd "C-c d") #'my/duplicate-line)
;; ── COMMAND PROMPTING FOR INPUT ──────────────────
(defun my/wrap-region (open close)
"Wrap the selected region with OPEN and CLOSE strings."
(interactive "sOpen with: \nsClose with: ")
(let ((beg (region-beginning))
(end (region-end)))
(save-excursion
(goto-char end) (insert close)
(goto-char beg) (insert open))))
;; ── macOS: OPEN TERMINAL HERE ────────────────────
(defun my/open-terminal-here ()
"Open macOS Terminal at current file's directory."
(interactive)
(let ((dir (if (buffer-file-name)
(file-name-directory (buffer-file-name))
default-directory)))
(shell-command
(format "open -a Terminal %s"
(shell-quote-argument dir)))))
(global-set-key (kbd "C-c o t") #'my/open-terminal-here)
;; ── TOGGLE BETWEEN LIGHT AND DARK THEME ─────────
(defvar my/dark-theme 'doom-one)
(defvar my/light-theme 'modus-operandi)
(defvar my/current-theme 'dark)
(defun my/toggle-theme ()
"Toggle between light and dark theme."
(interactive)
(if (eq my/current-theme 'dark)
(progn
(disable-theme my/dark-theme)
(load-theme my/light-theme t)
(setq my/current-theme 'light))
(progn
(disable-theme my/light-theme)
(load-theme my/dark-theme t)
(setq my/current-theme 'dark))))
(global-set-key (kbd "C-c t t") #'my/toggle-theme)
;; ── RENAME CURRENT FILE ──────────────────────────
(defun my/rename-file-and-buffer (new-name)
"Rename both current buffer and its visited file to NEW-NAME."
(interactive (list (read-string "New name: "
(buffer-name))))
(let ((old-name (buffer-file-name)))
(rename-file old-name new-name t)
(rename-buffer new-name)
(set-visited-file-name new-name)
(set-buffer-modified-p nil)
(message "File renamed to %s" new-name)))
27 Complete Production init.el for macOS
A fully-commented, macOS-optimized init.el integrating everything from this guide. Copy and adapt it as your starting point.
;; ─────────────────────────────────────────────────────────────────
;; ~/.config/emacs/init.el
;; Complete macOS Emacs configuration
;; ─────────────────────────────────────────────────────────────────
;; ══ PACKAGE SYSTEM ════════════════════════════════════════════════
(require 'package)
(setq package-archives
'(("gnu" . "https://elpa.gnu.org/packages/")
("melpa" . "https://melpa.org/packages/")
("nongnu" . "https://elpa.nongnu.org/nongnu/")))
(package-initialize)
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package))
(require 'use-package)
(setq use-package-always-ensure t
use-package-always-defer t) ; lazy load everything by default
;; ══ KEEP CUSTOM OUT OF THIS FILE ═════════════════════════════════
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(when (file-exists-p custom-file) (load custom-file 'noerror))
;; ══ macOS MODIFIER KEYS ══════════════════════════════════════════
(setq mac-command-modifier 'super) ; ⌘ = super
(setq mac-option-modifier 'meta) ; ⌥ = meta
(setq mac-right-option-modifier 'none) ; right ⌥ = normal chars
(setq mac-control-modifier 'control) ; ⌃ = control
(setq ns-use-native-fullscreen t)
;; macOS-style super keybindings
(global-set-key (kbd "s-s") #'save-buffer)
(global-set-key (kbd "s-c") #'kill-ring-save)
(global-set-key (kbd "s-v") #'yank)
(global-set-key (kbd "s-x") #'kill-region)
(global-set-key (kbd "s-z") #'undo)
(global-set-key (kbd "s-a") #'mark-whole-buffer)
(global-set-key (kbd "s-f") #'isearch-forward)
(global-set-key (kbd "s-g") #'isearch-repeat-forward)
(global-set-key (kbd "s-n") #'make-frame-command)
(global-set-key (kbd "s-w") #'delete-frame)
(global-set-key (kbd "s-`") #'other-frame)
(global-set-key (kbd "s-=") #'text-scale-increase)
(global-set-key (kbd "s--") #'text-scale-decrease)
(global-set-key (kbd "s-0") (lambda () (interactive) (text-scale-set 0)))
(global-set-key (kbd "s-<return>") #'toggle-frame-fullscreen)
;; ══ UI ════════════════════════════════════════════════════════════
(setq inhibit-startup-message t
inhibit-startup-echo-area-message (user-login-name))
(tool-bar-mode -1)
(scroll-bar-mode -1)
(blink-cursor-mode 0)
(column-number-mode 1)
(global-hl-line-mode 1)
(winner-mode 1)
(tab-bar-mode 0) ; set to 1 if you want tabs
(fringe-mode '(8 . 4))
(setq-default
cursor-type 'bar
line-spacing 0.15
fill-column 80
truncate-lines t)
(when (fboundp 'pixel-scroll-precision-mode)
(pixel-scroll-precision-mode t))
;; ══ FONTS ═════════════════════════════════════════════════════════
(defun my/setup-fonts ()
(set-face-attribute 'default nil
:family "JetBrains Mono" :height 140 :weight 'regular)
(set-face-attribute 'variable-pitch nil
:family "SF Pro Text" :height 150)
(set-fontset-font t 'emoji (font-spec :family "Apple Color Emoji")))
(add-hook 'emacs-startup-hook #'my/setup-fonts)
(add-hook 'server-after-make-frame-hook #'my/setup-fonts)
;; ══ EDITING ═══════════════════════════════════════════════════════
(setq-default indent-tabs-mode nil
tab-width 4)
(electric-pair-mode 1)
(show-paren-mode 1)
(delete-selection-mode 1)
(global-auto-revert-mode 1)
(setq auto-revert-check-vc-info t)
(put 'narrow-to-region 'disabled nil) ; enable narrowing
(put 'downcase-region 'disabled nil) ; enable case commands
(global-set-key (kbd "M-/") #'hippie-expand)
(global-set-key (kbd "C-z") #'ignore) ; disable accidental suspend
(global-set-key (kbd "C-c q") #'auto-fill-mode)
;; ══ BACKUPS ═══════════════════════════════════════════════════════
(let ((backup-dir (concat user-emacs-directory "backups/"))
(auto-dir (concat user-emacs-directory "auto-saves/")))
(dolist (dir (list backup-dir auto-dir))
(unless (file-exists-p dir) (make-directory dir t)))
(setq backup-directory-alist `(("." . ,backup-dir))
auto-save-file-name-transforms `((".*" ,auto-dir t))
backup-by-copying t
kept-new-versions 8
kept-old-versions 4
delete-old-versions t))
;; ══ COMPLETION: VERTICO + ORDERLESS + CONSULT ════════════════════
(use-package vertico :demand t :init (vertico-mode)
:custom (vertico-cycle t))
(use-package orderless :demand t
:custom (completion-styles '(orderless basic))
(completion-category-overrides
'((file (styles basic partial-completion)))))
(use-package marginalia :demand t :after vertico
:init (marginalia-mode))
(use-package consult
:bind (("C-x b" . consult-buffer)
("C-x B" . consult-buffer-other-window)
("M-y" . consult-yank-pop)
("M-g g" . consult-goto-line)
("C-s" . consult-line)
("C-c f" . consult-find)
("C-c G" . consult-ripgrep)))
;; ══ COMPANY (IN-BUFFER COMPLETION) ══════════════════════════════
(use-package company
:hook (prog-mode . company-mode)
:custom
(company-idle-delay 0.25)
(company-minimum-prefix-length 2)
(company-tooltip-limit 12))
;; ══ WHICH-KEY ════════════════════════════════════════════════════
(use-package which-key :demand t
:init (which-key-mode)
:custom (which-key-idle-delay 0.5))
;; ══ MAGIT ════════════════════════════════════════════════════════
(use-package magit
:bind ("C-x g" . magit-status)
("C-x M-g" . magit-dispatch)
:custom (magit-display-buffer-function
#'magit-display-buffer-same-window-except-diff-v1))
;; ══ EXEC-PATH-FROM-SHELL (MACOS ESSENTIAL) ═══════════════════════
(use-package exec-path-from-shell
:demand t
:if (memq window-system '(mac ns x))
:config (exec-path-from-shell-initialize))
;; ══ THEME ════════════════════════════════════════════════════════
(use-package doom-themes :demand t
:config (load-theme 'doom-one t))
(use-package doom-modeline :demand t
:init (doom-modeline-mode 1)
:custom (doom-modeline-height 28))
;; ══ ORG-MODE ═════════════════════════════════════════════════════
(use-package org
:hook (org-mode . visual-line-mode)
(org-mode . org-indent-mode)
:custom
(org-directory "~/Documents/org")
(org-default-notes-file (concat org-directory "/inbox.org"))
(org-agenda-files (list org-directory))
(org-hide-emphasis-markers t)
(org-startup-folded 'content)
:bind (("C-c a" . org-agenda)
("C-c c" . org-capture)
("C-c l" . org-store-link)))
;; ══ LINE NUMBERS & FILL COLUMN ═══════════════════════════════════
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
(add-hook 'prog-mode-hook #'display-fill-column-indicator-mode)
(add-hook 'before-save-hook #'delete-trailing-whitespace)
;; ══ PERSONAL KEY PREFIX ══════════════════════════════════════════
(define-prefix-command 'my-personal-map)
(global-set-key (kbd "C-c p") 'my-personal-map)
(define-key my-personal-map (kbd "d") #'my/insert-date)
(define-key my-personal-map (kbd "t") #'my/open-terminal-here)
(define-key my-personal-map (kbd "T") #'my/toggle-theme)
(define-key my-personal-map (kbd "r") #'my/rename-file-and-buffer)
;; ══ END OF INIT.EL ═══════════════════════════════════════════════
(message "Emacs ready in %.2fs" (float-time (emacs-init-time)))
⚡ Next steps
From here, add language-specific packages as you need them: lsp-mode or eglot for LSP support; python-mode / rustic / web-mode for specific languages; yasnippet for text snippets; projectile or the built-in project.el for project management; and vterm for the best terminal experience. The config above is a solid, clean foundation to build from.