My Emacs config with custom modules.
This is a work-in-progress document. The rest of the configuration is in good, usable state.
Even though this is idiomatic Doom Emacs configuration, I do not assume reader’s recognition of any Doom-specific features.
Unless specified explicitly, all the code snippets and discussed techniques can be directly applied to vanilla Emacs.
Links leading to the code in this config can in most cases be copy-pasted with minimal adjustments, functions 100%.
defadvice! syntax is fairly straightforward.
If you’re not familiar with advising functions (in general, not specifically Doom) - M-x info-display-manual RET elisp RET then M-x Info-goto-node, find advising functions.
If you’re using Doom, remember, there’s undefadvice! macro - a convenient way to remove advising - useful when experimenting.
add-hook! similarly, is very simple Doom macro. Mind that unlike regular add-hook it can add multiple functions to multiple hooks - convenient, but might confuse vanilla Emacs users.
This configuration uses use-package structure and things should be relatively easy to find. Make sure to eval every function when copying over, so you don’t miss anything in the call chain. Certain things might prove difficult to cherry-pick without pulling a bunch of other items, please don’t hesitate to complain and I will consider moving specific features into self-contained packages.
Hello, stranger. Hello, friend. Whatever path brought you to this repository, you may find yourself wondering how it might benefit you.
If you’re not very interested in hearing my personal reasons why this is still Doom-based config you may skip directly to Tips & Tricks.
My name is Ag. I am also known as “M-x Tips”. A while back, long before Elon turned Twitter to X-itter (playfully pronounced like “zshitter”), I started sharing helpful Emacs-related tips there. Overtime I earned some street creds in the community. While that alone never entitled me to self-proclaim an “Emacs white-beard” or call myself an “expert”, that still opened ways for me to engage in interesting projects and discussions. I met many interesting people and I would love to share any “secrets” I’ve learned over the years, with you.
Why should you give me any attention? Am I a highly successful programmer? Not exactly. Sure, I have been coding for may years, but that doesn’t make me remarkable - I’m neither great nor terrible; I am merely a-okay. Over the years, I’ve formed certain opinions, developed unique perspectives, made numerous mistakes, and taken several wrong turns. Still, I assure you that any hacks, techniques, concepts, workflows, ideas and strategies I discuss (either in conversations or through code) are genuine, effective, practical solutions viewed from an honest and pragmatic perspective and I promise you to keep it that way.
I also have recorded some lame videos, check out my YT channel iLemming - YouTube
You likely already have the answer to this question. If you still feel any uncertainty, please reach out to me. Let’s be honest - not everyone needs Emacs. If you pledge to be sincere and avoid trolling, I might be able to offer some compelling arguments to help you decide, and I promise to remain as unbiased as possible. Aside from technical rationale for choosing Emacs, there are certain psychological, neuroscientific reasons that certainly will affect your system of work. It may not happen quickly - you may not even notice the changes as they perhaps come rather gradually.
I am a die-hard vimmer. Oh yes, I am. I do use Neovim, but Emacs still is my main driver, and for many reasons it will likely remain in this position for many decades to come.
I personally find these two (Vim and Emacs) to be exactly the paradigm shift - in comparison to my prior experience when I didn’t have them. But let me be more specific - the paradigm shift I see is not in Emacs nor Vim/Neovim as concrete tools. The paradigm shift I sense is in the grand ideas behind these tools - modal navigation and Lisp. I honestly don’t know why more people don’t gravitate towards these ideas. Perhaps, because these ideas are akin to the concept of meditation. People say: one can only experience the mind shift only after practicing mediation for some time. I wouldn’t know - I have never developed a habitual mediation routine.
There’s some “tacit” knowledge involved - it’s just like inability to explain the feeling of riding a bicycle to anyone who never experienced that before. You may draw diagrams; bring scientific, logical arguments, use factoids; explain how caster effect of the fork’s angle steers the wheel back under the center of mass; gyroscopic effect of the spinning wheels, yada-yada, and yet, all that, still will not explain the feeling of a simple bicycle ride.
There’s concept in science called ’umwelt’ - specific way in which organisms of a particular species perceive and experience the world, shaped by the capabilities of their sensory organs and perceptual systems.
Learning vim-navigation or Lisp or even a language with an advanced type system (e.g. Haskell) can be umwelt-transformative. Vim changes how you perceive text as a structured, navigable space. Lisp reveals code-as-data and makes you see programs as transformable structures. These aren’t just new skills - they’re new sensory-cognitive modalities. You literally cannot “unsee” the patterns once internalized. They become part of your computational umwelt, shaping what problems you notice, what solutions seem natural, and even how you conceptualize everyday processes outside programming. It’s similar to how learning music theory changes how you hear songs, or how learning a tonal language might affect how you perceive pitch. The tools become part of your extended cognition, restructuring your problem-space perception. When Lispers say “code is data” they’re not just stating a fact - they’re describing a lived perceptual reality where parentheses dissolve into tree structures and programs become sculptable material.
This creates a profound pedagogical challenge: you can explain the mechanics of Lisp and REPL-driven workflow endlessly, but until someone has that “aha” moment where they start thinking in s-expressions, they don’t really get it. It’s like trying to explain color to someone who’s never seen, or echolocation to someone without that sense. That’s why who’s never given a truthful and heartfelt attempt to understand Lisp, often never gets it or ends up disliking it. The umwelt shift is precisely what makes these tools powerful - they’re not just different syntax but different ways of being-in-computational-world.
Do you really need modality in Emacs? Many experienced Emacs users say maybe you don’t. They assert that it doesn’t really bring any benefits - it’s not “better”, just “different”. “Once you build Emacs-native muscle memory, you can be as efficient or even better without any modal modes”, they say.
While that might be true for some people, I don’t believe this to be generally accurate, for one simple reason - when they mention modality, they typically speak of one specific aspect of modality and that’s text-editing modality (switching between Insert and Normal modes for typing), while vim-navigation is much more than that. Besides, modality critics and “never-vimmers” somehow like to ignore the fact that Emacs is already, inherently a modal editor - key chords are modal, transients are modal, isearch is modal, repeat-mode is modality. Some of these modal modes are complex (arguably more than vim motions are), they have to maintain state, etc.
The only thing the idea of vim-navigation brings on top of the built-in machinery of Emacs is a simple, structured and memorable language/grammar to deal with modality, that’s all it is. Both approaches are modal; Vim just codified its modality into a more learnable, linguistic system while Emacs distributed its modality across context-sensitive mini-languages. Emacs has this same power scattered throughout - the kill-ring, rectangle operations, keyboard macros - but Vim’s genius was packaging it into a coherent, immediately graspable system that feels less like using a computer and more like speaking a concise language about text. The best thing about learning that language is that you can apply it anywhere where keyboard input is still relevant - your browser, your terminal, your entire system - e.g., you can control your music, jump between apps, navigate between windows, etc., using only the home row keys. I have done that successfully on Macs and Linux - it works beautifully.
Most people think of keyboard input as a linear, one-dimensional approach to text editing.
Consider window management in vanilla Emacs:
C-x 2splits horizontally (mnemonic: “two windows”)C-x 3splits vertically (less intuitive—why 3?)C-x 4becomes a prefix for operations in other windows
The logic breaks down quickly. There’s no consistent mental model connecting these commands. C-x 4 abandons the splitting metaphor entirely, becoming a namespace for “other window” operations rather than a window count.
Vim-style navigation offers something fundamentally different - a spatial “language” that maps directly to how we conceptualize our workspace. Doom and Spacemacs vim-inspired approach utilized that coherent spatial language:
SPC w s- splitSPC w v- vertical splitSPC w h/j/k/l- jump left/down/up/right windowSPC w m- toggle window maximizeSPC w d- delete window- etc.
The hjkl directional keys mirror vim’s movement commands, creating a consistent spatial metaphor. Your fingers learn to “think” in terms of physical direction rather than arbitrary key combinations.
This spatial approach transforms window management from memorizing disconnected shortcuts into developing spatial intuition. Instead of thinking “what was that split command again?”, you think “I want to move right” and your fingers naturally press l. The keybindings become an extension of spatial reasoning rather than abstract symbol manipulation.
This principle extends beyond window management - vim’s entire philosophy treats text editing as navigation through dimensional space, making complex operations feel natural and memorable.
And like I already said, this model can easily be extended beyond just the text editor - you can navigate things in your browser, terminal, etc. Why would I even try to learn and memorize new keyboard shortcuts e.g., every time I switch browsers?
I wouldn’t! Knowing vanilla Emacs default keys comes very nice. Some may prefer quick press of C-l, instead of Esc z z, also C-a/C-e/C-b/C-f are universally supported in bash; on Macs they work in every input. Rarely, yet it happens, you may need to emacs --debug-init, or emacs -Q where Evil might not yet be initialized or simply inaccessible.
Also, if you’re planning to use Emacs in terminal… I’d recommend GUI (for many good reasons), but sometimes you just need it. In TTY, text input modality can be detrimental, for example, you may feel a tiny delay between switching modes. That can be fixed, but it may feel like a hassle to make it work just right in some elaborate setting when you need them all play well - terminal app, multiplexer, shell and Emacs. Abstraction comes with a cost, and when you have layers of abstraction - the cost may increase.
So maybe do learn some Emacs-native keybindings, but don’t hate me years later, helping a friend with Windows, while frantically yelling at the screen for popping up a new browser window whenever you want for cursor to “go down”.
h/j/k/l and other vim keys seem logical on Qwerty layout, but what about Dvorak, Colemak, etc.? Honestly, I don’t know. I have never tried myself switching to other layouts, because I have never felt that my typing speed and accuracy are my bottlenecks. However, I have heard from many others that the overhead is very minimal - most users reported keeping h/j/k/l, some done minimal re-mapping - trivial thing for Emacs.
There are other modal modes available in Emacs and they are quite popular - god-mode, meow, etc. You may want to explore those. I’ve heard many success stories of happy users of meow.
Okay, modality is a good idea, maybe let’s use it. Or don’t - it’s your choice. You can use Doom completely without Evil-mode features. But do you really need to use Doom? After all, there are multiple different other Emacs starter kits - There’s Spacemacs, Prelude, Crafted Emacs and more. You don’t even have to use any starter kits, right? So, what’s so specifically nice about Doom?
Just like many Emacs users I have gone through stages in my config evolution. Multiple times I declared config bankruptcy and started anew. I have used minimal and complicated, confusing setups. There were some years I spent using Spacemacs until finally switching to Doom.
Doom (today) just makes sense to me. Someday that may change, but so far (and I’ve been using it for years) it has not disappointed. It’s not “bloated” as many perceive it to be. And it doesn’t “get in the way” whenever you want to do something non-trivial - it’s the same, good, ol’ Emacs. The “bloated” perception comes from the number of things included, Doom comes with the “batteries”. But guess what? You can treat Doom as a “cookbook” instead of an end-product and use its modules [only] for inspiration and ideas. You don’t need to use every interesting Doom module, in fact you may actually choose not to use any of its modules and instead roll a set of your own, custom ones, or use minimal set of its modules combined with yours - that’s what I do.
You can make Emacs run only a tiny core of Doom and it would be just fine. Core of Doom contains a few, very handy Lisp macros that can significantly reduce (otherwise unavoidable) boilerplate of elisp. If I ever decide to move away from Doom, I still probably will borrow these macros - they are simply too nice to ignore.
I also like modular structure of Doom. I simply stoped worrying about the state of my config - I tend it as an an abundant, cornucopian garden. My metaphorical trees perpetually yield the sweet fruit of satisfaction. Just like a regular garden, mine also has some dark, not so pretty parts, and that’s okay. I write Lisp every day - to achieve specific, well-defined goals, most of the time that happens in a scratch buffer first. Later my experiments may get moved into the config. Occasionally I perform “weed-whacking” cleanup - remove old, unused, needless and deprecated things, but I almost never have to make sweeping changes. That is the essense of using Emacs. It is not about “using editor features”, it’s about “talking to it in Lisp”, and where that Lisp is sitting isn’t really that important, as long as you can quickly locate things, and Emacs gives that to you. You have complete freedom to explore source code of any function that runs Emacs - built-in, third-party and your own.
Doom and Spacemacs also give you a good foundational recipe for structuring your keybindings. Once you start using “mnemonically recognizable” chords, it simplifies navigation. And when you need to add additional keys, you’d know where to place them. For example, SPC s for ‘search’ category; SPC f - for ‘files’. If someday you’d want to add a key for a command that searches for things in your logs, you may choose to use either SPC s l or SPC f l, or both. Nothing really is stopping you from adding another level - SPC f s l - which you may mnemonically recognize as “files -> search -> logs”. You may choose to go a different route - SPC l s - for “logs -> search”, etc. I assure you, it only may sound overwhelming to have tons of keys structured in that manner - in practice, once you start using them, it doesn’t take long for muscle memory to form. I often don’t even think what I just pressed to achieve a certain effect. It’s almost weird - my fingers are capable of reproducing the sequence, but I can’t even “read it out loud” - it’s like playing the “Guitar Hero” - you may be extremely good at that game, your fingers know how to score, but you never can recite the entire sequence of colored keys for a song off your head, right?
For now, I decided I will be adding tips directly in this document, slowly expanding the portfolio with ideas of my own discovery and interesting things I borrowed from various other authors.
- You may like to read this document in Emacs, without even cloning the repo, simply do:
M-x eww- Paste this:
https://raw.githubusercontent.com/agzam/.doom.d/refs/heads/main/readme.org - Force Org-mode -
M-x org-mode
- Alternatively,
M-x (eval-expression)- it’s same as pressingM-:, and paste this code:(progn (eww "https://raw.githubusercontent.com/agzam/.doom.d/refs/heads/main/readme.org") (run-at-time 0.5 nil (lambda () (with-current-buffer "*eww*" (org-mode)))))
We do need that delay because eww is asynchronous. Properly handling it would require more than just two lines - plese don’t do it this way in a command.
- I wouldn’t recommend simply cloning and using the config as is - it has too many tweaks of my personal flavor perhaps unsuitable for your taste. Instead, I’d recommend using this project as a recipe book of specific tips, tricks, and ideas, but hey, if you insist on wanting to use it as your doom config:
git clone git@github.com:agzam/.doom.d.git ~/.doom.d cd ~/.doom.d && git submodule update --init --recursive
- Mind that this is heavily opinionated configuration - do not expect things to run smoothly off the bat! Make sure to run doom CLI checks after you do:
doom installDo these:
doom build doom doctorI also suggest first time running it with:
emacs --debug-initThat will tell you about things config expects - missing fonts, etc.
I’ve been using this config mostly with the latest versions of Emacs. On Mac I prefer using emacs-plus homebrew formulae, i.e. GNU Emacs instead of Mitsuharu Yamamoto’s mac port 1 - mainly for tiny compatibility issues - I need my Emacs to work both - in Linux and Mac. Note that emacs-mac can be built it --with-metal flag that supposedly improves performance, especially on Apple Silicon. I’ve tried many times, yet failed to see any difference - my GNU Emacs instance feels equally snappy on Mac.
1 emacs-mac is not considered official GNU Emacs. It’s a third-party distribution that includes additional features and optimizations specifically for macOS. While it’s based on GNU Emacs, it contains modifications that are not part of the official GNU Emacs distribution. The official GNU Emacs does have macOS support, but Mitsuharu’s port offers some Mac-specific enhancements that aren’t found in the standard version.
Emacs has a built-in lunar and solar calendars. And it also has facilities to deal with daylight saving rules, etc. There are numerous interesting applications you can think of, like using Emacs to optimize solar panel usage, tracking daylight exposure for health, finding prayer times, start of Sabbath and Ramadan, etc.
Let’s see how we can use Emacs for automatically changing color theme to reduce eye strain. There are two different packages for that:
https://github.com/BBoal/theme-buffet and
https://guidoschmidt.github.io/circadian.el
Both of them are simple and straightforward, both can use discrete time values. Circadian also can track sunrise and sunset times based on given latitude and longitude.
I use Circadian, only because back when I needed to solve this I didn’t know about theme-buffet.
You can find my Circadian preferences here
I use a simple transient for that, with a couple of specialized commands. In order to switch to the next theme in the list of circadian-themes, I decided to turn the list into a “ring” so I can cycle through the elements. The “ring” in Emacs is a type of structure also known as a “circular buffer” or “cyclic buffer”. Think of Emacs’ “kill ring”, “mark ring”, etc.
Once you convert the ordinary list of themes with ring-convert-sequence-to-ring, you can then do ring-next and ring-previous.
pdf-tools has this feature when you can change the colors of the document to match your Emacs color theme, it’s pretty nice - your left eye won’t be twitching anymore, trying to simultaneously follow your notes in dark colors, while right eye has to be on the blinding whiteness of the pdf.
It’s very simple - you just turn pdf-view-themed-minor-mode on, and voila!
There’s a tiny annoyance though. If you change your theme, you have to re-toggle the themed-minor mode in every pdf buffer. Typically, there isn’t too many open pdfs, so, usually that ain’t a big deal. Still, why not find a workaround?
Whenever Emacs loads and enables a theme, it runs (enable-theme) function, which adds a hook. A hook in Emacs is a list of functions to run in response to some event, so whenever (enable-theme) runs, it will execute every item on that list, or nothing - if it’s empty.
(defun adjust-pdf-colors-on-theme-change-h (_)
"Keep pdf-view-themed buffers in sync with main color-theme changes."
(thread-last
(buffer-list)
(seq-filter
(lambda (b)
(with-current-buffer b (eq major-mode 'pdf-view-mode))))
(seq-do
(lambda (b)
(with-current-buffer b
(when pdf-view-themed-minor-mode
(pdf-view-themed-minor-mode +1)))))))
(add-hook 'enable-theme-functions #'adjust-pdf-colors-on-theme-change-h)- Current convention for naming hook functions is to append “-h” suffix to the name
enable-theme-functionsis a hook that gets called with a single argument -theme, but this time we don’t care what theme that is - thus_for the parameter.thread-lastis similar to Clojure’s threading macro ->>, it would pass the first arg -(buffer-list)as the last param in every subsequent expressionseq-filteris a typical filter function, andseq-dois just for side-effects. In this case, the effect is to re-enable the minor mode. Minor modes in Emacs expose both a variable and a function with the same name. If the argument given to the function is positive, it enables the mode, if zero or negative - disables it.
Let’s look into this “vars & functions can share names” thing a bit closer.
Emacs Lisp is so called “Lisp-2” - functions and vars live in separate namespaces and unlike in most other mainstream languages can carry the same name. Most other “modern” Lisps - Clojure, Scheme, Janet, Fennel, etc. are “Lisp-1” Lisps, so binding to the same name will simply overwrite (shadow) previous declaration.
This has implications. Sometimes, you will see that instead of calling functions directly (like in most other languages), you’d have to do something like this:
(funcall foo)
Let’s say, you want to store a pointer to a function in a variable…
(setq my-mode-toggler pdf-view-themed-minor-mode)
What do you think happens here? Yup, you probably guessed it right, mode-toggler won’t be getting the pointer to the function, instead it will receive the current value of our variable (of the same name). Makes sense, right?
If we really want to assign the function, we would have to do it like this:
(setq my-mode-toggler #'pdf-view-themed-minor-mode) - Now our variable points to the function, not the var.
But what do you think happens if we’d try to execute it?
(my-mode-toggler +1)
This will fail, but why? Because my-mode-toggler is not really a function. It is only a variable holding a function. Thus we’d have to do it like this:
(funcall my-mode-toggler +1)
That’s why you may encounter patterns similar to:
(let ((toggle-func (if some-condition
#'pdf-view-themed-minor-mode
#'some-other-minor-mode)))
(funcall toggle-func 1)) ; we can't just (toggle-func 1) hereAllow me another tiny bit of pedantics to explain these seemingly weird hashtag characters. You may at some point see the code like this:
(setq my-mode-toggler 'pdf-view-themed-minor-mode)
and you may wonder how does that differ from:
(setq my-mode-toggler #'pdf-view-themed-minor-mode)
Because if you call it (funcall my-mode-toggler), the code still runs with no apparent difference.
The semantic difference that in the first case the variable holds the symbol. While in the second, it holds the function itself. Confusing, innit?
Try these:
(setq f1 '(lambda () "hello")) ; f1 is a symbol holding a list
(type-of f1) ; => cons - a pair of two values
; But guess what? We still can execute it
(funcall f1) ; => "hello"
; Because for the interpreter,
; it doesn't really matter - is it data, is it code?
; With hashtag is almost the same,
; yet slightly different
(setq f2 #'(lambda () "hello")) ; f2 is a function object
(eq f1 f2) ; => nil - they are not the same
(type-of f2) ; => interpreted functionSymbols in Elisp are named objects with multiple storage slots. For a name, value, function, and a property bag. I’m not gonna get into details here, but I’d encourage you to try out these:
(symbol-name f1)
(symbol-value f1)
(symbol-function f1)
(symbol-plist f1) So why then in most cases it just works as if there’s no difference? Because in most cases, Elisp interpreter receiving the symbol would do the lookup (what’s in the function slot for this symbol).
But you always should use #' syntax in your code, because it’s more explicit and slightly more efficient.