A lean and minimal Emacs configuration
Note: I may or may not be the original author of various scripts used in this post. Since they are acquired/written over several years, I honestly don’t remember the history/source of each. If its not me, its probably from EmacsWiki or Reddit.
- Where is it?
- Let’s Start
- Basic text editing
- Searching
- Buffers and Windows
- Moving around windows
- Highlighting
- Minibuffer Completion
- Programming
- Project and Version Control
- Dired
- Appearance and other tweaks
- Conclusion
Emacs is an amazing text editor for almost anything. However, it takes time and experimentation to make it feel more productive than traditional options out there (Visual Studio Code, Sublime, IntelliJ, Google Docs …). Often, we end up with giant configurations with dozens of scripts and external packages. They are great, practical and productive, but also slow to install and start.
This got me thinking if I can trim down my configuration into a single .emacs
file, that only depends on builtin/shipped packages. Turns out, you can! And you
don’t really lose much. Particularly, if you are using Emacs 29. In this post, I
will go through a lean, minimal and productive single file .emacs
configuration.
The goals of this configuration are:
- Be lightweight, both in size and load times.
- Should work reasonably in terminal mode.
- Sane defaults and keybindings.
- Quick searching and moving around.
- Some modern features like auto-completion, project search, version control.
- Tailed for software development.
- Only use the packages shipped in vanilla Emacs.
Where is it?
You can find the complete configuration in this GitHub Gist.
Let’s Start
First, we will configure the package manager and
use-package
(ships with Emacs 29,
but also available in ELPA). Highly recommended if you are not using it already.
It’s a very standard
configuration.
Basic text editing
For general editing, I like to set these defaults:
(prefer-coding-system 'utf-8)
;; Default major mode for new files.
(setq-default major-mode 'text-mode)
;; History of copied text.
(setq kill-ring-max 500)
;; Save clipboard contents into kill-ring before replace them
(setq save-interprogram-paste-before-kill t)
;; Repeating C-SPC after popping mark pops it again
(setq set-mark-command-repeat-pop t)
;; Don't wrap lines.
(setq-default truncate-lines t)
Delete Selection mode does copy/paste in a bit more traditional way. It replaces the selected text when you paste something.
(use-package delsel
:hook (after-init . delete-selection-mode))
Next we change the behavior of some basic keybindings for moving around. The variable names imply what they do.
(bind-key "C-a" 'back-to-indentation-or-beginning-of-line)
(bind-key "M-g" 'goto-line)
(bind-key "C-M-'" 'match-paren)
(bind-key "C-w" 'kill-region-or-backward-word)
(bind-key "M-w" 'kill-region-or-thing-at-point)
(bind-key "C-S-k" 'kill-whole-line)
(bind-key "C-o" 'open-line-below)
(bind-key "C-c C-o" 'open-line-above)
(bind-key "C-S-o" 'open-line-above)
(bind-key "M-j" 'join-line)
In case, you are not familiar with Emacs terminology, (1) kill = copy, (2) yank = paste, (3) region = selection. These tweaks are mainly tailed for programming, as they take indentation and symbols into consideration unlike the defaults.
(defun back-to-indentation-or-beginning-of-line ()
"Moves cursor to beginning of line, taking indentation into account"
(interactive)
(if (= (point) (save-excursion (back-to-indentation) (point)))
(beginning-of-line)
(back-to-indentation)))
(defun match-paren (&optional arg)
"Go to the matching parenthesis character if one is adjacent to point."
(interactive "^p")
(cond ((looking-at "\\s(") (forward-sexp arg))
((looking-back "\\s)" 1) (backward-sexp arg))
;; Now, try to succeed from inside of a bracket
((looking-at "\\s)") (forward-char) (backward-sexp arg))
((looking-back "\\s(" 1) (backward-char) (forward-sexp arg))))
(defun kill-region-or-backward-word ()
"If the region is active and non-empty, call `kill-region'.
Otherwise, call `backward-kill-word'."
(interactive)
(call-interactively
(if (use-region-p) 'kill-region 'backward-kill-word)))
(defun kill-region-or-thing-at-point (beg end)
"If a region is active kill it, or kill the thing (word/symbol) at point"
(interactive "r")
(unless (region-active-p)
(save-excursion
(setq beg (re-search-backward "\\_<" nil t))
(setq end (re-search-forward "\\_>" nil t))))
(kill-ring-save beg end))
(defun match-paren (&optional arg)
"Go to the matching parenthesis character if one is adjacent to point."
(interactive "^p")
(cond ((looking-at "\\s(") (forward-sexp arg))
((looking-back "\\s)" 1) (backward-sexp arg))
;; Now, try to succeed from inside of a bracket
((looking-at "\\s)") (forward-char) (backward-sexp arg))
((looking-back "\\s(" 1) (backward-char) (forward-sexp arg))))
(defun open-line-below ()
"Starts a new line below the current line."
(interactive)
(move-end-of-line 1)
(newline)
(indent-according-to-mode))
(defun open-line-above ()
"Starts a new line above the current line."
(interactive)
(move-beginning-of-line 1)
(newline)
(forward-line -1)
(indent-according-to-mode))
Next, we add rectangle-mode which I primarily use for adding or deleting text from multiple lines. Alternatively, iedit-mode and multiple-cursors are excellent unofficial choices.
(use-package rect
:ensure nil
:bind (("<C-return>" . rectangle-mark-mode)))
Goto Address mode identifies URLs and makes them clickable.
(use-package goto-addr
:ensure nil
:hook ((text-mode . goto-address-mode)
(prog-mode . goto-address-prog-mode)))
We’ll get into more sophisticated completion, but hippie-expand is quick and convenient, and I don’t mind having a dedicated keybinding for it.
(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))
(bind-key "M-/" 'hippie-expand)
Searching
I like to quickly search the currently open file. M-s o
is already a good
default for invoking occur. For incremental search, I just add couple tweaks
that tries to add some familiar keybindings to result selection:
;; Make backspace delete the search term not goto last address
(define-key isearch-mode-map [remap isearch-delete-char] 'isearch-del-char)
(defun isearchp-kill-ring-save ()
"Copy the current search string to the kill ring. Credits: isearch+"
(interactive)
(kill-new isearch-string)
(sit-for 1)
(isearch-update))
;; Default query-replace shortcut on macOS would conflict with system shortcut
;; to take screenshot. So lets rebind these.
(bind-key "C-?" #'query-replace)
(bind-keys :map isearch-mode-map
("C-?" . isearch-query-replace)
("M-w" . isearchp-kill-ring-save))
Good integration with grep or ripgrep is critical to my work. Emacs,
already ships with grep
package that has many useful commands (grep
/rgrep
is what I mostly use). However, they always opens up a new result buffer, which
quickly gets annoying for short lived greps. Packages like
consult and
counsel address this issue very well.
For this config, we’ll just hack up a quick fix. (1) It takes the selected
region or symbol under point, (2) invokes grep/ripgrep, (3) displays results as
minibuffer completions. It is based on completing-read
, the elisp function
used by the minibuffer
completion
system (by default bound to TAB
).
(bind-key "C-c g" 'grep-completing-read)
(defun grep-completing-read ()
"Execute grep/ripgrep search using completing-read"
(interactive)
(let* ((default-term (if (region-active-p)
(substring-no-properties
(buffer-substring (mark) (point)))
(thing-at-point 'symbol)))
(term (read-string "search for: " default-term))
(execute-search
(lambda (term)
(if (executable-find "rg")
(process-lines "rg" "--line-number" term)
(process-lines "git" "grep" "-niH" "-e" term))))
(results (funcall execute-search term))
(line-list (split-string (completing-read "results: " results) ":"))
(rfile (car line-list))
(rlnum (string-to-number (car (cdr line-list)))))
(find-file rfile)
(goto-line rlnum)
(recenter)))
I recommend using this with icomplete/fido completion modes, which we’ll
look at later in this post. Project and Version Control (VC) packages
come with few convenient grep
commands of its own.
Buffers and Windows
These are my preferred bindings for quickly switching between buffers.
(bind-key "M-_" 'previous-buffer)
(bind-key "M-+" 'next-buffer)
(bind-key "C-x C-b" 'switch-to-buffer)
IBuffer is an excellent package, which I mostly use to kill several buffers together. You write your own function to group buffers together. There are unofficial packages to automatically create groups based on active project, projectile or VCS repository.
(use-package ibuffer :bind (("C-x b" . ibuffer-bs-show)))
Auto Revert automatically updates my buffers that got changed by other
programs. Save Place saves current cursor position, and restores it when you
open the same file later. Recentf simply keeps track of recent files
(C-c <RET>
is quite standard binding for it).
(use-package autorevert
:diminish auto-revert-mode
:hook (after-init . global-auto-revert-mode))
(use-package saveplace
:hook (after-init . save-place-mode))
(use-package recentf
;; lazy load recentf
:hook (find-file . (lambda () (unless recentf-mode
(recentf-mode)
(recentf-track-opened-file))))
:bind ("C-c <RET>" . recentf)
:init
(add-hook 'after-init-hook #'recentf-mode)
(setq recentf-max-saved-items 200)
:config
(add-to-list 'recentf-exclude (expand-file-name package-user-dir))
(add-to-list 'recentf-exclude ".objs")
(add-to-list 'recentf-exclude ".cache")
(add-to-list 'recentf-exclude ".cask")
(add-to-list 'recentf-exclude "bookmarks")
(add-to-list 'recentf-exclude "COMMIT_EDITMSG\\'"))
Emacs really suffers when you open large files. Nowhere near the perfect solution, but it does make it tolerable:
(defun fast-file-view-mode ()
"Makes the buffer readonly and disables fontlock and other bells and whistles
for faster viewing"
(interactive)
(setq buffer-read-only t)
(buffer-disable-undo)
(fundamental-mode)
(font-lock-mode -1)
(when (boundp 'anzu-mode)
(anzu-mode -1)))
(defun large-find-file-hook ()
"If a file is over a given size, make the buffer read only."
(when (> (buffer-size) (* 1024 1024))
(fast-file-view-mode)))
(add-hook 'find-file-hook 'large-find-file-hook)
Moving around windows
Unlike most IDEs and many text editors, Emacs GUI is very minimal. This is
great, as there is even more screen real-estate to split windows. Standard
keybindings C-x {2, 3, 1, 0}
work great for creating splits. I have these
additional keybindings to move around. Since I use them a lot more than creating
splits, I selected keybindings that are easy to repeat.
;; Cycles through windows.
(bind-key "C-q" 'other-window)
;; Directional window-selection routines. Binds shift-{left,up,right,down}
;; to move around windows.
(use-package windmove
:ensure nil
:hook (after-init . windmove-default-keybindings))
Winner mode keeps track of changes in window configuration, and lets you
undo/redo changes using C-c <Left>
/C-c <Right>
.
(use-package winner
:ensure nil
:hook (after-init . winner-mode)
:init (setq winner-boring-buffers '("*Completions*"
"*Compile-Log*"
"*inferior-lisp*"
"*Fuzzy Completions*"
"*Apropos*"
"*Help*"
"*cvs*"
"*Buffer List*"
"*Ibuffer*"
"*esh command on file*")))
Highlighting
Some are just for appearance, and others are actually useful. But they all ship with emacs and are fairly lightweight. So, why not!
- hl-line to highlight current line.
(use-package hl-line :hook (after-init . global-hl-line-mode))
- paren for highlighting matching parenthesis/brackets.
(use-package paren :hook (after-init . show-paren-mode) :config (setq show-paren-when-point-inside-paren t show-paren-when-point-in-periphery t))
-
whitespace to highlight bad whitespaces. Particularly useful for source code.
(use-package whitespace :ensure nil :bind ("C-x w" . whitespace-mode) :hook ((prog-mode outline-mode conf-mode) . whitespace-mode) :config (setq whitespace-line-column fill-column) ;; limit line length ;; Automatically clean up bad whitespace (setq whitespace-action '(auto-cleanup)) ;; Only show bad whitespace (setq whitespace-style '(face trailing indentation empty)))
In my regular config, I use
symbol-overlay to highlight and
navigate symbols. In vanilla emacs, we can instead use the highlight
package
(check highlight-symbol-at-point
and highlight-regexp
).
(use-package hi-lock
:bind
(("M-n" . isearch-forward-symbol-at-point)
("M-l" . my/toggle-mark-symbol-at-point))
:config
(defun my/toggle-mark-symbol-at-point ()
(interactive)
(if hi-lock-interactive-patterns
(unhighlight-regexp (car (car hi-lock-interactive-patterns)))
(highlight-symbol-at-point))))
Lastly, pulse can flash a line after certain events (e.g. switch window, jump to definition/declaration/error, etc ..).
;; Source: Centaur Emacs
(use-package pulse
:ensure nil
:preface
(defun my-pulse-momentary (&rest _)
"Pulse the current line."
(pulse-momentary-highlight-one-line (point) 'next-error))
(defun my-recenter (&rest _)
"Recenter and pulse the current line."
(recenter)
(my-pulse-momentary))
:hook ((bookmark-after-jump next-error other-window
dumb-jump-after-jump imenu-after-jump) . my-recenter)
:init (dolist (cmd '(recenter-top-bottom
other-window windmove-do-window-select
pop-to-mark-command pop-global-mark
pager-page-down pager-page-up))
(advice-add cmd :after #'my-pulse-momentary)))
Minibuffer Completion
Completions are awesome, whether it is code auto-completion, or file/command suggestions. Emacs comes with two completion systems, (1) default minibuffer completion (2) icomplete/fido.
First lets set tab as the completion trigger. In minibuffer, tab will trigger completion.
(setq tab-always-indent 'complete)
Minibuffer completion is very bare bones, and shows up completions in a
dedicated buffer. It’s not my preferred way, but is still worth having it
configured in a way that feels slightly more natural. It also sets few a
variables that are relevant to icomplete
. The most notable one is partial and
flex matching.
(use-package minibuffer
:ensure nil
:bind (:map minibuffer-mode-map
("C-n" . minibuffer-next-completion)
("C-p" . minibuffer-previous-completion)
("M-<RET>" . my/minibuffer-choose-completion))
:bind (:map completion-in-region-mode-map
("C-n" . minibuffer-next-completion)
("C-p" . minibuffer-previous-completion)
("M-<RET>" . my/minibuffer-choose-completion))
:init
(setq enable-recursive-minibuffers t
completion-styles '(initials partial-completion flex)
completions-format 'one-column
completions-header-format nil
completion-auto-help t
completion-show-help nil
completions-max-height 10
resize-mini-windows t
completion-flex-nospace nil
completion-pcm-complete-word-inserts-delimiters t
completion-auto-select nil
completion-ignore-case t
read-buffer-completion-ignore-case t
read-file-name-completion-ignore-case t)
(defun my/minibuffer-choose-completion (&optional no-exit no-quit)
(interactive "P")
(with-minibuffer-completions-window
(let ((completion-use-base-affixes nil))
(choose-completion nil no-exit no-quit)))))
My preference is to use icomplete/fido mode vertically, which feels a bit more like vertico and ivy packages.
(use-package icomplete
:bind (:map icomplete-fido-mode-map
("TAB" . icomplete-force-complete)
("C-n" . icomplete-forward-completions)
("C-p" . icomplete-backward-completions))
:hook (icomplete-minibuffer-setup . (lambda () (setq-local truncate-lines t)))
:init
(setq icomplete-in-buffer t
icomplete-with-completion-tables t
icomplete-hide-common-prefix nil
icomplete-prospects-height 6
icomplete-compute-delay 0)
(fido-mode +1)
(fido-vertical-mode +1))
To invoke auto-completion when editing buffers, I’ve a dedicated keybinding:
(bind-key "M-s i" 'completion-at-point)
However, it will not show completion using fido-mode
and fallback to the old
minibuffer completion system. To make that happen, we will have to implement
completion-in-region-function
on top of completing-read
. As mentioned
earlier, completing-read
is the function behind minibuffer completion, and now
its changed by icomplete
.
;; For making completion-at-point work with icomplete/fido.
;; https://www.reddit.com/r/emacs/comments/ulsrb5/what_have_you_recently_removed_from_your_emacs/
(defun completing-read-at-point (start end col &optional pred)
(if (minibufferp) (completion--in-region start end col pred)
(let* ((init (buffer-substring-no-properties start end))
(all (completion-all-completions init col pred (length init)))
(completion (cond
((atom all) nil)
((and (consp all) (atom (cdr all))) (car all))
(t (completing-read "Completions: " col pred t init)))))
(if completion
(progn
(delete-region start end)
(insert completion)
t)
(message "No completions") nil))))
(setq completion-in-region-function #'completing-read-at-point)
Its still not ideal, and if I’m spending non-trivial time on a machine with this config, I just install corfu or company.
Lastly, on top of completion, you can save the minibuffer history using savehist.
(use-package savehist
:hook (after-init . savehist-mode)
:init (setq enable-recursive-minibuffers t ; Allow commands in minibuffers
history-length 1000
savehist-additional-variables '(mark-ring
global-mark-ring
search-ring
regexp-search-ring
extended-command-history)
savehist-autosave-interval 60))
Programming
Let’s start with the indentation settings. This will expand tabs to space, so skip it if you prefer tabs.
(setq-default c-basic-offset 4
tab-width 4
indent-tabs-mode nil)
Next, we add an indicator column that helps us keep the line width under control.
(setq-default fill-column 100)
(global-display-fill-column-indicator-mode)
Close brackets/quotes automatically using electric-pair-mode:
(electric-pair-mode +1)
Enable line numbers using display-line-numbers-mode:
(bind-key "C-6" 'global-display-line-numbers-mode)
(use-package display-line-numbers
:hook ((prog-mode . display-line-numbers-mode)
(prog-mode . which-function-mode)
(markdown-mode . display-line-numbers-mode)
(text-mode . display-line-numbers-mode)))
Some extra keybindings to indent text left or right manually:
(bind-keys ("C-M->" . indent-rigidly-right)
("C-M-<" . indent-rigidly-left))
More helpers bindings for common programming tasks:
(bind-key "C-c c" 'compile)
(bind-key "M-;" 'comment-dwim)
(bind-key "C-7" 'comment-line)
IMenu displays the outline of the current buffer. Really great way to find classes/functions in the current buffer and jump to them.
(bind-key "M-\\" 'imenu)
Next, we’ll set up eglot, which is the official Language Server Protocol (LSP) client since Emacs 29. This package, in my opinion, makes this no-external-dependency requirement actually practical for long term use.
(use-package eglot
:hook ((c++-mode . eglot-ensure)
(c-mode . eglot-ensure)
(python-mode . eglot-ensure))
:bind (:map eglot-mode-map
("C-c a r" . #'eglot-rename)
("C-c h" . #'eldoc)
("C-<down-mouse-1>" . #'xref-find-definitions)
("C-S-<down-mouse-1>". #'xref-find-references)
("C-c C-c" . #'eglot-code-actions)
("C-M-<tab>" . #'eglot-format))
:config
(defun eglot-clangd-find-other-file (&optional new-window)
"Switch between the corresponding C/C++ source and header file."
(interactive)
(let* ((server (eglot--current-server-or-lose))
(rep
(jsonrpc-request
server
:textDocument/switchSourceHeader
(eglot--TextDocumentIdentifier))))
(unless (equal rep nil)
(funcall #'find-file (eglot--uri-to-path rep)))))
;; I mostly use C/C++ for work. My favorite option here is
;; `--all-scopes-completion`, which looks into modules that are not
;; yet included. Makes coding feel way more fluid and interruption free.
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'(c-mode c++-mode
. ("clangd"
"-j=4"
"--background-index"
"--clang-tidy"
"--all-scopes-completion"
"--completion-style=detailed"
"--header-insertion=iwyu"
"--header-insertion-decorators=0"
"--malloc-trim"
"--cross-file-rename"
"--pch-storage=memory"))))
(setq eldoc-echo-area-use-multiline-p nil
eglot-extend-to-xref t
eldoc-idle-delay 0.1
eglot-autoshutdown t))
;; Use the `eglot-clangd-find-other-file` implemented above, to quickly switch
;; b/w source and header files. If you are not using LSP, there is
;; `ff-find-other-file`.
(use-package cc-mode
:bind (:map c-mode-base-map
("C-c o" . eglot-clangd-find-other-file)))
LSP servers also provides diagnostics (lint, errors etc …). *Flymake is responsible for working with those:
(use-package flymake
:bind (("C-c l a" . flymake-show-project-diagnostics)
("C-c l b" . flymake-show-buffer-diagnostics)
("C-x ]" . flymake-goto-next-error)
("C-x [" . flymake-goto-prev-error)))
Flyspell works with both text and programming modes:
(use-package flyspell
:hook ((text-mode . flyspell-mode)
(pro-mode . flyspell-prog-mode)))
Unfortunately, emacs doesn’t ship with support for Rust or Go. There is no getting around that, except downloading external packages.
;; For rust. `rustic-enable-detached-file-support` so that LSP works with
;; files that do not belong to a cargo project.
(use-package rustic
:hook (rustic-mode . eglot-ensure)
:init
(setq rustic-lsp-client 'eglot
rustic-enable-detached-file-support t))
;; For Go. Originally taken from Centaur Emacs.
(use-package go-mode
:functions (go-packages-gopkgs go-update-tools)
:hook (go-mode . eglot-ensure)
:init
(setq-default eglot-workspace-configuration
'((:gopls . ((usePlaceholders . t)))))
:config
(use-package go-dlv)
(use-package go-fill-struct)
(use-package go-impl)
(use-package gotest
:bind (:map go-mode-map
("C-c t a" . go-test-current-project)
("C-c t m" . go-test-current-file)
("C-c t ." . go-test-current-test)
("C-c t x" . go-run))))
Eglot doesn’t install LSP servers by itself. You can automate it however. Here’s an example from Centaur Emacs:
(defvar go--tools '("golang.org/x/tools/gopls"
"golang.org/x/tools/cmd/goimports"
"github.com/go-delve/delve/cmd/dlv"
"github.com/josharian/impl"
"github.com/cweill/gotests/..."
"github.com/davidrjenni/reftools/cmd/fillstruct"))
(defun go-install-tools ()
"Install or update go tools."
(interactive)
(unless (executable-find "go")
(user-error "Unable to find `go' in `exec-path'!"))
(message "Installing go tools...")
(let ((proc-name "go-tools")
(proc-buffer "*Go Tools*"))
(dolist (pkg go--tools)
(set-process-sentinel
(start-process proc-name proc-buffer "go" "install" (concat pkg "@latest"))
(lambda (proc _)
(let ((status (process-exit-status proc)))
(if (= 0 status)
(message "Installed %s" pkg)
(message "Failed to install %s: %d" pkg status))))))))
Project and Version Control
Emacs ships with a pretty good project management package called project
. A
famous unofficial alternative is projectile
. However, project does most of
what I want. My favorite commands are find-file-in-project-or-directory
bound
to C-t
, projectile-switch-buffer
, projectile-find-regexp
. Type C-c p ?
to see list of default keybindings.
(use-package project
:bind (("C-c M-k" . project-kill-buffers)
("C-c m" . project-compile)
("C-x f" . find-file)
("C-t" . find-file-in-project-or-directory))
:bind-keymap ("C-c p" . project-prefix-map)
:defines find-file-in-project-or-directory
:custom
(project-switch-commands
'((project-find-file "Find file")
(project-find-regexp "Grep" ?h)))
:config
(defun find-file-in-project-or-directory ()
(interactive)
(if (project-current nil)
(project-find-file)
(call-interactively #'find-name-dired))))
Emacs also ships with a package for Version Control called vc. I’ve tweaked it to show commit stats along with the message, and slightly different formatting. Log view and blaming is what I like to use from this package. Otherwise, I’m not a fan, and prefer Magit for anything remotely non-trivial.
(use-package vc
:bind
(("C-c i" . vc-git-grep)
("C-x g" . vc-dir-root))
:config
(defun vc-git-expanded-log-entry (revision)
"Just adds commit stats to expanded commit entry in log-view."
(with-temp-buffer
(apply #'vc-git-command t nil nil
(list "log" revision "-1" "--stat" "--no-color" "--stat" "--"))
(goto-char (point-min))
(unless (eobp)
;; Indent the expanded log entry.
(while (re-search-forward "^" nil t)
(replace-match " ")
(forward-line))
(concat "\n" (buffer-string))))))
Dired
Highly recommend it if you don’t use it already. It works fairly well out of the
box. I’ve just unset the keymap for image-dired
as I prefer that for finding
files in the current directory or project. It also prefers GNU ls if
available, as it has --group-directories-first
option.
(use-package dired
:ensure nil
:bind (:map dired-mode-map
("C-c C-p" . wdired-change-to-wdired-mode))
:hook ((dired-mode . (lambda () (local-unset-key (kbd "C-t"))))) ; image-dired
:config
(setq dired-recursive-deletes 'always
dired-recursive-copies 'always
dired-isearch-filenames 'dwim)
(when (executable-find "gls")
;; Use GNU ls when possible.
(setq dired-use-ls-dired nil)
(setq ls-lisp-use-insert-directory-program t)
(setq insert-directory-program "gls")
(setq dired-listing-switches "-alh --group-directories-first"))
(use-package dired-aux :ensure nil))
Appearance and other tweaks
There are a bunch of random small tweaks that are not quite necessary, but really good to have.
Control the size of frame:
(when (display-graphic-p)
(add-to-list 'default-frame-alist '(height . 60))
(add-to-list 'default-frame-alist '(width . 100)))
Disable some GUI features (also makes it load a bit faster):
(menu-bar-mode -1)
(when window-system
(tool-bar-mode -1)
(scroll-bar-mode -1))
;; Go directly to scratch buffer.
(setq inhibit-splash-screen t)
(setq use-file-dialog nil
use-dialog-box nil
inhibit-startup-screen t
inhibit-startup-echo-area-message t)
(global-unset-key (kbd "<C-wheel-down>"))
(global-unset-key (kbd "<C-wheel-up>"))
;; Don't show native comp warnings.
(setq native-comp-async-report-warnings-errors 'silent)
;; Never kill scratch.
(with-current-buffer "*scratch*"
(emacs-lock-mode 'kill))
Change some other aspects:
;; Show path if names are same
(setq uniquify-buffer-name-style 'post-forward-angle-brackets)
;; Secondary click open context menu in GUI.
(context-menu-mode)
;; Ask yes or no, explicitely.
(fset 'yes-or-no-p 'y-or-n-p)
;; Menubar in terminal.
(bind-key "M-`" #'tmm-menubar)
;; Show column numbers in mode line.
(setq column-number-mode t)
;; Load remote environment when using tramp-mode.
(eval-after-load 'tramp '(setenv "SHELL" "/bin/bash"))
;; Show a message instead of beeping.
(setq visible-bell t)
(setq ring-bell-function (lambda () (message "*woop*")))
;; Don’t compact font caches during GC. IIRC, improves performance.
(setq inhibit-compacting-font-caches t)
;; Some settings around text auto-filling. This is probably unnecessary.
(setq adaptive-fill-regexp "[ t]+|[ t]*([0-9]+.|*+)[ t]*"
adaptive-fill-first-line-regexp "^* *$"
sentence-end "\\([。!?]\\|……\\|[.?!][]\"')}]*\\($\\|[ \t]\\)\\)[ \t\n]*"
sentence-end-double-space nil)
I never found the position of Meta
key comfortable on Macs. This swaps Super
and Meta/Option
key:
;; OS specific tweaks
(cond
((string-equal system-type "darwin")
(setq ns-use-thin-smoothing t)
(setq mac-option-key-is-meta t
mac-command-key-is-meta nil
mac-command-modifier 'meta
mac-option-modifier 'super
ns-use-native-fullscreen nil))
((string-equal system-type "gnu/linux") t))
Emacs 29 comes with much better scrolling called pixel-scroll-precision-mode
.
Also, the default scrolling feels a bit odd to me, and these options make it
feel a bit more natural.
(when (and window-system (>= emacs-major-version 29))
(pixel-scroll-precision-mode 1))
(setq scroll-preserve-screen-position 'always
mouse-wheel-scroll-amount '(1 ((shift) . 1)) ; Scroll one line at a time.
mouse-wheel-progressive-speed nil
scroll-step 1
scroll-margin 1
scroll-conservatively 10000)
Minor mode names can quickly clutter your modeline. diminish is great, but there is a DIY solution that I found somewhere on Reddit:
(define-minor-mode minor-mode-blackout-mode
"Hides minor modes from the mode line."
t)
(catch 'done
(mapc (lambda (x)
(when (and (consp x)
(equal (cadr x) '("" minor-mode-alist)))
(let ((original (copy-sequence x)))
(setcar x 'minor-mode-blackout-mode)
(setcdr x (list "" original)))
(throw 'done t)))
mode-line-modes))
Emacs uses a dedicated buffer to show compilation output. You can set it to scroll automatically while its still running the compile process. We also add a hook to apply colors to the output, as some compilation programs produce fancier outputs. It may or may not interfere with your grep-mode output.
;; Compilation output, autoscroll
(setq compilation-scroll-output t)
(use-package ansi-color
:config
(defun --compilation-filter (fn proc string)
"Wrap `compilation-filter' (FN PROC STRING) to support `ansi-color'."
(let ((buf (process-buffer proc)))
(when (buffer-live-p buf)
(with-current-buffer buf
(funcall fn proc string)
(when (not (equal major-mode 'grep-mode))
(let ((inhibit-read-only t))
(ansi-color-apply-on-region (point-min) (point-max))))))))
(advice-add 'compilation-filter :around #'--compilation-filter))
Lastly, I’ve a dedicated file for some extra useful things that don’t ship with Emacs. Currently, I’ve got go-mode, rustic, winnow, persistent-scratch, popper and corfu added to that file. Winnow is great for filtering compilation/grep outputs. The default behavior around popups is very unpredictable, and Popper forces those popup buffers to show up in the right place.
;; For local configurations.
(load (expand-file-name "local.el" user-emacs-directory) 'noerror)
Conclusion
That’s it, we are done! I mainly use it as-is whenever I’m temporarily working
on a new server, container or a virtual machine. With some extra packages that I
mentioned earlier, I find myself using it more than my “more complete” .emacs
.
I keep it updated as a GitHub
Gist, where
you can also leave comments.