config.phundrak.com/docs/emacs/basic-config.org
Lucien Cartier-Tilet fa1194552d
All checks were successful
deploy / build (push) Successful in 2m42s
fix(Emacs gc): fix syntax of garbage-collecting function
2024-02-08 06:31:12 +01:00

17 KiB
Raw Blame History

Emacs — Basic Configuration

Basic Configuration

Early Init

The early init file is the file loaded before anything else in Emacs. This is where I put some options in order to disable as quickly as possible some built-in features of Emacs before they can be even loaded, speeding Emacs up a bit.

(setq package-enable-at-startup nil
      inhibit-startup-message   t
      frame-resize-pixelwise    t  ; fine resize
      package-native-compile    t) ; native compile packages
(scroll-bar-mode -1)               ; disable scrollbar
(tool-bar-mode -1)                 ; disable toolbar
(tooltip-mode -1)                  ; disable tooltips
(set-fringe-mode 10)               ; give some breathing room
(menu-bar-mode -1)                 ; disable menubar
(blink-cursor-mode 0)              ; disable blinking cursor

Better garbage collection

Emacs sometimes freezes up a bit when doing some garbage collection, which is not super. So, in order to fix that, I do two things:

  1. Set up a really high threshold (I have a lot of RAM to spare, so I dont really care),
  2. Hook garbage collection thirty seconds after Emacs loses focus and every thirty seconds after that,
  3. Cancel garbage collection if Emacs gains focus back within this thirty seconds window.

With a garbage collection threshold, Emacs should never garbage collect on its own, and Emacs is free to freeze up for a few seconds when it loses focus since Im looking away.

(setq garbage-collection-messages t            ;; tell me when garbage collecting
      gc-cons-threshold (* 16 1024 1024 1024)) ;; 16GiB of RAM

(defmacro my/time (&rest body)
  `(let ((time (current-time)))
     ,@body
     (float-time (time-since time))))

(defun my/garbage-collect ()
  "Garbage collect and tell the user how much time it took."
  (message "Garbage collector ran for %.06fs"
           (my/time (garbage-collect))))

(defvar my/gc-timer nil
  "Timer for garbage collection. See
`my/garbage-collect-on-focus-lost'.")

(defun my/garbage-collect-on-focus-lost ()
  "Garbage collect when Emacs loses focus.

Garbage collection is only triggered thirty seconds after losing
focus, and only once."
  (if (frame-focus-state)
      (when (timerp my/gc-timer)
       (cancel-timer my/gc-timer))
    (setq my/gc-timer (run-with-idle-timer 30 nil #'my/garbage-collect))))

(add-function :after after-focus-change-function #'my/garbage-collect-on-focus-lost)

To write this, I mostly based myself on this HackerNews thread and its related article.

Emacs Behavior

Editing Text in Emacs

I never want to keep trailing spaces in my files, which is why Im doing this:

(add-hook 'before-save-hook #'whitespace-cleanup)

I dont understand why some people add two spaces behind a full stop, I sure dont. Lets tell Emacs.

(setq-default sentence-end-double-space nil)

There is a minor mode in Emacs which allows having a finer way of jumping from word to word: global-subword-mode. It detects if what Emacs usually considers a word can be understood as several words, as in camelCase words, and allows us to jump words on this finer level.

(global-subword-mode 1)

Changing half my screen each time my cursor goes too high or too low is not exactly ideal. Fortunately, if we set scroll-conservatively high enough we can have the cursor stay on top or at the bottom of the screen while the text scrolls progressively.

(setq scroll-conservatively 1000)

Lastly, I want the default mode for Emacs to be Emacs Lisp.

(setq-default initial-major-mode 'emacs-lisp-mode)
Indentation

I dont like tabs. They rarely look good, and if I need it I can almost always tell Emacs to use them through a .dir-locals.el file or through the config file of my code formatter. So by default, lets disable them:

(setq-default indent-tabs-mode nil)
(add-hook 'prog-mode-hook (lambda () (setq indent-tabs-mode nil)))

Just to go on a little tangent here: I dont exactly hate tabs, but I find them really annoying when your text editor knows only them. Sure, for indentation they work great, and they allow different people getting different settings in their text editor depending on their preferred tastes —some may prefer 2 spaces tabs, some may prefer 4 spaces tabs, some deranged people prefer 8 spaces tabs, and some monsters prefer 3!

But the thing is, once you indented your code, and then you need alignment, tabs dont work any more! Or they may on your text editor but not on your coworkers! (Hes the one using 3 spaces tabs by the way).

So, is the answer to use spaces instead of tabs, and screw peoples preferences in terms of tabs width? No, I say the answer is more moderate than that, and it might frighten or anger some of you at first: use both spaces and tabs. Now, before you lynch me on the main avenue in front of everyone, let me tell you absolutely no one should ever be mixing spaces and tabs for indentation, that would be absolutely terrible and would bring the worst of both worlds. Whats the best of both worlds then?

Tabs for indentation

Spaces for alignment

I havent found a way to automate that in Emacs yet aside from formatters config file, and tabs look bat in EmacsLisp anyway, so Ill stick with spaces by default and change it where needed.

GPG pinentry

Id like Emacs to be responsible for decrypting my GPG encrypted files when in Emacs. This can be done with the following line.

(setq epg-pinentry-mode 'loopback)

Programming Modes

First off, my definition of what makes a “programming mode” doesnt exactly fit mine, so on top of prog-mode, lets add a few other modes.

Modes
prog-mode
latex-mode
(mapconcat (lambda (mode) (format "%s-hook" (car mode)))
           modes
           " ")
Line Number

Since version 26, Emacs has a built-in capacity of displaying line numbers on the left-side of the buffer. This is a fantastic feature that should actually be the default for all programming modes.

(dolist (mode '(<<prog-modes-gen()>>))
  (add-hook mode #'display-line-numbers-mode))
Folding code

Most programming languages can usually have their code folded, be it code between curly braces, chunks of comments or code on another level of indentation (Python, why…?). The minor-mode that enables that is hs-minor-mode, lets enable it for all of these programming modes:

(dolist (mode '(<<prog-modes-gen()>>))
  (add-hook mode #'hs-minor-mode))

Stay Clean, Emacs!

As nice as Emacs is, it isnt very polite or clean by default: open a file, and it will create backup files in the same directory. But then, when you open your directory with your favourite file manager and see almost all of your files duplicated with a ~ appended to the filename, it looks really uncomfortable! This is why I prefer to tell Emacs to keep its backup files to itself in a directory it only will access.

(setq backup-directory-alist `(("." . ,(expand-file-name ".tmp/backups/"
                                                         user-emacs-directory))))

When using LSP with Typescript projects, my tsconfig.json or its equivalent would constantly get polluted with symlinks going through my home directory, creating some horror imports. This is because the server tries to import some files through the symlink of the backup of the file I want to import. By forbidding Emacs to create symlinks for backups, we should avoid this problem.

(setq backup-by-copying t)

It also loves to litter its init.el with custom variables here and there, but the thing is: I regenerate my init.el each time I tangle this file! How can I keep Emacs from adding stuff that will be almost immediately lost? Did someone say custom file?

(setq-default custom-file (expand-file-name ".custom.el" user-emacs-directory))
(when (file-exists-p custom-file) ; Dont forget to load it, we still need it
  (load custom-file))

If we delete a file, we want it moved to the trash, not simply deleted.

(setq delete-by-moving-to-trash t)

Finally, the scratch buffer always has some message at its beginning, I dont want it!

(setq-default initial-scratch-message nil)

Stay Polite, Emacs!

When asking for our opinion on something, Emacs loves asking us to answer by “yes” or “no”, but in full! Thats very rude! Fortunately, we can fix this. Note that the configuration changed in Emacs 29.

(if (version<= emacs-version "28")
    (defalias 'yes-or-no-p 'y-or-n-p)
  (setopt use-short-answers t))

This will make Emacs ask us for either hitting the y key for “yes”, or the n key for “no”. Much more polite!

It is also very impolite to keep a certain version of a file in its buffer when said file has changed on disk. Lets change this behaviour:

(global-auto-revert-mode 1)

Much more polite! Note that if the buffer is modified and its changes havent been saved, it will not automatically revert the buffer and your unsaved changes wont be lost. Very polite!

Misc

Lets raise Emacs undo memory to 10 MB, and make Emacs auto-save our files by default.

(setq undo-limit        100000000
      auto-save-default t)
(setq window-combination-resize t) ; take new window space from all other windows

Personal Information

Emacs needs to know its master! For various reasons by the way, some packages rely on these variables to know who it is talking to or dealing with, such as mu4e which will guess who you are if you havent set it up correctly.

(setq user-full-name       "Lucien Cartier-Tilet"
      user-real-login-name "Lucien Cartier-Tilet"
      user-login-name      "phundrak"
      user-mail-address    "lucien@phundrak.com")

Visual Configuration

The first visual setting in this section will activate the visible bell. What it does is I get a visual feedback each time I do something Emacs doesnt agree with, like trying to go up a line when Im already at the top of the buffer.

(setq visible-bell t)

It is nicer to see a cursor cover the actual space of a character.

(setq x-stretch-cursor t)

When text is ellipsed, I want the ellipsis marker to be a single character of three dots. Lets make it so:

(with-eval-after-load 'mule-util
 (setq truncate-string-ellipsis "…"))

With Emacs 29.0.50 onwards, a new frame parameter exists: alpha-background. Unlike alpha, this frame parameter only makes Emacs background transparent, excluding images and text.

(add-to-list 'default-frame-alist '(alpha-background . 0.9))

Modeline Modules

I sometimes use Emacs in fullscreen, meaning my usual taskbar will be hidden. This is why I want the current date and time to be displayed, in an ISO-8601 style, although not exactly ISO-8601 (this is the best time format, fight me).

(require 'time)
(setq display-time-format "%Y-%m-%d %H:%M")
(display-time-mode 1) ; display time in modeline

Something my taskbar doesnt have is a battery indicator. However, I want it enabled only if I am on a laptop or if a battery is available.

(let ((battery-str (battery)))
  (unless (or (equal "Battery status not available" battery-str)
              (string-match-p (regexp-quote "N/A") battery-str))
    (display-battery-mode 1)))

This isnt a modeline module per se, but we have an indicator of the current line in Emacs. And although it is useful, I also often wish to know which column Im on. This can be activated like so:

(column-number-mode)

The following code is, as will several chunks of code in this config, borrowed from TECs configuration. It hides the encoding information of the file if the file itself is a regular UTF-8 file with \n line ending. Be aware the doom-modeline-buffer-encoding variable is usabel here only because I use the Doom modeline as seen below.

(defun modeline-contitional-buffer-encoding ()
  "Hide \"LF UTF-8\" in modeline.

It is expected of files to be encoded with LF UTF-8, so only show
the encoding in the modeline if the encoding is worth notifying
the user."
  (setq-local doom-modeline-buffer-encoding
              (unless (and (memq (plist-get (coding-system-plist buffer-file-coding-system) :category)
                                 '(coding-category-undecided coding-category-utf-8))
                           (not (memq (coding-system-eol-type buffer-file-coding-system) '(1 2))))
                t)))

Now, lets automate the call to this function in order to apply the modifications to the modeline each time we open a new file.

(add-hook 'after-change-major-mode-hook #'modeline-contitional-buffer-encoding)

Fonts

I dont like the default font I usually have on my machines, I really dont. I prefer Cascadia Code, as it also somewhat supports the IPA.

(defvar phundrak/default-font-size 90
  "Default font size.")

(defvar phundrak/default-font-name "Cascadia Code"
  "Default font.")

(defun my/set-font ()
  (when (find-font (font-spec :name phundrak/default-font-name))
    (set-face-attribute 'default nil
                        :font phundrak/default-font-name
                        :height phundrak/default-font-size)))

(my/set-font)
(add-hook 'server-after-make-frame-hook #'my/set-font)

Frame Title

This is straight-up copied from TECs configuration. See their comment on the matter.

(setq frame-title-format
      '(""
        "%b"
        (:eval
         (let ((project-name (projectile-project-name)))
           (unless (string= "-" project-name)
             (format (if (buffer-modified-p) " ◉ %s" "  ●  %s - Emacs") project-name))))))

A better custom variable setter

Something people often forget about custom variables in Elisp is they can have a custom setter that will run some code if we set the variable properly with customize-set-variable, so setq shouldnt be the users choice by default. But repeatedly writing customize-set-variable can get tiring and boring. So why not take the best of both world and create csetq, a setq that uses customize-set-variable under the hood while it keeps a syntax similar to the one setq uses?

(defmacro csetq (&rest forms)
  "Bind each custom variable FORM to the value of its VAL.

FORMS is a list of pairs of values [FORM VAL].
`customize-set-variable' is called sequentially on each pair
contained in FORMS. This means `csetq' has a similar behaviour as
`setq': each VAL expression is evaluated sequentially, i.e. the
first VAL is evaluated before the second, and so on. This means
the value of the first FORM can be used to set the second FORM.

The return value of `csetq' is the value of the last VAL.

\(fn [FORM VAL]...)"
  (declare (debug (&rest sexp form))
           (indent 1))
  ;; Check if we have an even number of arguments
  (when (= (mod (length forms) 2) 1)
    (signal 'wrong-number-of-arguments (list 'csetq (1+ (length forms)))))
  ;; Transform FORMS into a list of pairs (FORM . VALUE)
  (let (sexps)
    (while forms
      (let ((form  (pop forms))
            (value (pop forms)))
        (push `(customize-set-variable ',form ,value)
              sexps)))
    `(progn ,@(nreverse sexps))))

I first got inspired by this blog article (archived article, just in case) but it seems the code snippet no longer works properly, so not only did I have to modify it to make it work with an arbitrary amount of arguments (as long as its pairs of variables and their value), but I also had to make the code simply work.