bandali’s GNU Emacs configuration

[ Last revised on 2026-05-13 20:15:25 -0400 with a word count of 21753. ]

This is my opinionated, literate GNU Emacs configuration. I tend to use the latest development trunk of emacs.git, but I try to maintain backward compatibility with a few of the recent GNU Emacs releases so I could more easily reuse it on machines stuck with older Emacsen.

I publish my GNU Emacs configuration as free software under the same license as GNU Emacs itself, under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the license, or (at your option) any later version. You’re welcome to copy and share parts or all of my configuration under these terms. However, I urge you to not copy-paste a code snippet from my configuration into yours without first understanding what it does.

This Org document is the single point of entry to my GNU Emacs setup, where I define and configure everything in a single Org file, from which all of my individual Emacs Lisp files are exported using Babel by pressing C-c C-v C-t or by evaluating the following code block.

(org-babel-tangle)

This document references GNU Emacs’s built-in documentation and manuals, including key sequences like C-h v user-full-name RET meant to be pressed in GNU Emacs, as well as links to the manuals included in GNU Emacs and available on the GNU Project website, like info emacs.

1. Early initialization of Emacs (early-init.el)

The early init file, first introduced in Emacs 27, is loaded during Emacs startup earlier than the regular init file, before the package system and the GUI are initialized. It’s useful for customizations that need to take effect early on before Emacs creates the initial frame.

See: info “(emacs) Early Init File”

;;; early-init.el --- bandali's early init -*- lexical-binding: t -*-

;; Copyright (c) 2019-2026 Amin Bandali <bandali@gnu.org>

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

1.1. early-init.el set package-enable-at-startup to nil

I currently do not use Emacs’s standard package.el (or any other package manager, for that matter), so tell Emacs to not try to make installed packages available during startup.

See: C-h v package-enable-at-startup RET and info “(emacs) Package Installation”

(setq package-enable-at-startup nil)

1.2. early-init.el set load-prefer-newer to prefer newest version of a file

I use byte-compiled (.elc) and native-compiled (.eln) Emacs Lisp files. Sometimes I edit the main .el source file while I’m hacking on a feature, without recompiling it right away. Setting load-prefer-newer to t tells Emacs to prefer the newest version of a file, to help avoid the confusing situation of loading stale code.

See: C-h v load-prefer-newer RET and info “(emacs) Lisp Libraries”

(setq load-prefer-newer t)

1.3. early-init.el basic frame and GUI-related tweaks

Here I tweak a few frame and GUI-related options for a more minimalist look. I normally don’t use the menu bar or the tool bar, and find the scroll bar and blinking cursors distracting. So, do away with these in early init. If I need the menu bar or the tool bar, I enable it briefly, use it, then disable it again.

If you are new to Emacs, I recommend keeping Emacs’s defaults; they can be very helpful in (re)discovering context-specific or general functionality as you get acquainted with Emacs. :)

(setq
 ;; Don't resize the frame when font size is adjusted
 frame-resize-pixelwise t
 frame-inhibit-implied-resize 'force
 ;; Skip the startup screen
 inhibit-startup-screen t
 ;; I don't like getting jump-scared out of my chair
 ring-bell-function #'ignore)

(menu-bar-mode -1)

(when (fboundp #'tool-bar-mode)
  (tool-bar-mode -1))

(when (fboundp #'scroll-bar-mode)
  (scroll-bar-mode -1))

(blink-cursor-mode -1)

1.4. early-init.el startup and garbage collection tweaks for faster initialization

Here we increase the values of gc-cons-threshold and gc-cons-percentage to help speed up Emacs initialization. We store the original values so we can restore them later in an after-init-hook.

Similarly, we temporary set file-name-handler-alist and vc-handled-backends to nil to avoid some potentially expensive operations during startup, and restore them later.

See: C-h v gc-cons-threshold RET, C-hv gc-cons-percentage RET, and info “(elisp) Garbage Collection” for more details.

(defconst bandali--gc-cons-threshold gc-cons-threshold)
(defconst bandali--gc-cons-percentage gc-cons-percentage)
(defvar bandali--file-name-handler-alist file-name-handler-alist)
(defvar bandali--vc-handled-backends vc-handled-backends)
(setq
 gc-cons-threshold (* 30 1024 1024) ; 30 MiB
 gc-cons-percentage 0.6
 file-name-handler-alist nil
 vc-handled-backends nil)

;; Set them back to their defaults once we're done initializing.
(defun bandali--post-init ()
  "My post-initialization function."
  (setq
   gc-cons-threshold bandali--gc-cons-threshold
   gc-cons-percentage bandali--gc-cons-percentage
   file-name-handler-alist bandali--file-name-handler-alist
   vc-handled-backends bandali--vc-handled-backends))
(add-hook 'after-init-hook #'bandali--post-init)

1.5. early-init.el set the user-lisp-directory

Emacs 31 has the wonderful new User Lisp feature, where the Emacs Lisp files in the directory specified by user-lisp-directory and its subdirectories will be recursively byte-compiled, scraped for autoload cookies, and ensured to be in load-path.

I essentially use User Lisp as part of my manual package management workflow. It works for me because I intentionally use a very small number of external Emacs packages, as I’d like to carefully scrutinize the software that runs on my machine to the extent possible.

At least as of now, I ignore test and tests directories since these tend to result in a large number of byte-compiler warnings from packages I’ve been using. I will reevaluate these later.

See: C-h v user-lisp-directory RET and info “(emacs) User Lisp Directory”

(setopt user-lisp-directory (locate-user-emacs-file "lisp/"))
(when (boundp 'user-lisp-ignored-directories)
  (add-to-list 'user-lisp-ignored-directories "test")
  (add-to-list 'user-lisp-ignored-directories "tests")
  (add-to-list 'user-lisp-ignored-directories "batch-tests"))

On older Emacsen, we will fall back to using site-lisp.el, the predecessor to User Lisp.

(when (version< emacs-version "31")
  (add-to-list 'load-path (locate-user-emacs-file "lisp/site-lisp"))
  (require 'site-lisp)
  (prepare-user-lisp))

When I first started using User Lisp, I was overwhelmed by all the byte-compiler warnings during startup, so I briefly suppressed it using the following. I’ve since disabled it, because I’ve addressed most of them in my configuration and/or sent patches to upstream projects.

;; (add-to-list 'warning-suppress-types '(bytecomp))

2. Main initialization of Emacs (init.el)

The init file is the main place for specifying how we’d like Emacs to be initialized and configured for us, by customizing and/or extending its features in Lisp. We could write most of our customizations in the init file - and for smaller Emacs configurations that works just fine - but as the size of my configuration has grown, I find it more practical to split my configuration into smaller modules, each of which specify the customizations and/or extensions to a functional area of Emacs.

See: info “(emacs) Init File”

;;; init.el --- bandali's emacs configuration -*- lexical-binding: t -*-

;; Copyright (c) 2018-2026 Amin Bandali <bandali@gnu.org>

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
(setq
 debug-on-error init-file-debug
 debug-on-quit init-file-debug)
(setq ; whoami
 user-full-name "Amin Bandali"
 user-mail-address "bandali@kelar.org")
;; Separate custom file - don't want it mixing with init.el
(setq custom-file (locate-user-emacs-file "custom.el"))
(with-eval-after-load 'custom
  (load custom-file 'noerror))
(require 'bandali-core)
(require 'bandali-theme)
(require 'bandali-essentials)
(require 'bandali-window)
(require 'bandali-mode-line)
(require 'bandali-completion)
(require 'bandali-search)
(require 'bandali-lang)
(require 'bandali-dired)
(require 'bandali-vc)
(require 'bandali-org)
(require 'bandali-gnus)
(require 'bandali-erc)
(require 'bandali-web)

3. Configuration modules (lisp/)

3.1. bandali-core.el

This module consists of convenience macros like bandali-define-key which will be widely used throughout my GNU Emacs configuration.

;;; bandali-core.el --- bandali's config core  -*- lexical-binding: t; -*-

;; Copyright (c) 2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: tools

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Code:

3.1.1. bandali-core.el macro for a configuration block (bandali-configure)

Note: I have since stopped using bandali-configure in my configuration blocks as I found the purported benefits negligible for me, but I’m preserving its definition here for posterity.

In the past I used use-package for configuring packages. While certainly convenient, it also felt a bit magical to me and too complex for my taste. So I stopped using it in favour of the manual approach. For the most part, I wrap configurations for package foo in a (with-eval-after-load 'foo ...) block so that they are only applied if/when I use that package. For customizing user options, I use the setopt macro (introduced in Emacs 29) instead of setq, because setopt executes any custom-set code associated with the variable, which will take care of properly handling the variable update in cases where extra care or steps are needed for the update to take effect. For hooks, I use add-hook directly, and for key bindings I use my bandali-define-key wrapper around define-key.

My bandali-configure macro, inspired by Protesilaos’s prot-emacs-configure, wraps condition-case around the configuration code, to catch any errors (and display them) so that Emacs can continue loading the other parts of my configuration without failing.

Further, each bandali-configure block includes code to measure its execution time, which can be useful for finding slow configuration blocks. This was inspired by Eshel Yaron’s esy/init-step macro. The bandali-configure-report-times function returns the execution times of all bandali-configure blocks. If called interactively, it will display the execution times in the echo area as well as the *Messages* buffer.

(defvar bandali--configure-times nil
  "Stores execution times of `bandali-configure' configuration blocks.")

(defmacro bandali-configure (name &rest body)
  "Evaluate BODY and catch any errors.
NAME will be included in the error report to help the user more easily
find the configuration block.

The execution time of BODY will be added to `bandali--configure-times'
for later inspection and/or reporting.

With inspiration from Protesilaos's `prot-emacs-configure' and Eshel
Yaron's `esy/init-step'."
  (declare (indent 1)
           (doc-string 1))
  (let ((start-time-symbol (gensym)))
    `(let ((,start-time-symbol (current-time)))
       (prog1
           (condition-case err
               (progn ,@body)
             ((error user-error quit)
              (message
               "bandali-configure: error in block `%S' due to `%S'"
               ',name (cdr err))))
         (push
          (cons ',name
                (time-subtract (current-time) ,start-time-symbol))
          bandali--configure-times)))))

;; Usage examples:

;; (bandali-configure package
;;   (with-eval-after-load 'package
;;     (setopt package-review-policy t)))

;; (bandali-configure "tabs and spaces"
;;   (setopt tab-always-indent 'complete)
;;   (setq-default
;;    indent-tabs-mode nil
;;    tab-width 4))

(defun bandali-configure-report-times (&optional sort)
  "Report execution times of `bandali-configure' blocks.
With optional SORT as a prefix argument, if greater than or equal to
zero, or one or more \\[universal-argument], the execusion times are
reported in order of increasing time.  If SORT is a negative number or
just `-', then the execution times are reported in order of decreasing
time.  Otherwise, if SORT is nil or not provided, the execution times
are reported as-is without any sorting, in order of occurrence."
  (interactive "P")
  (let* ((times-list (if (null sort)
                         (reverse bandali--configure-times)
                       (sort
                        bandali--configure-times
                        :key #'cdr
                        :lessp #'time-less-p
                        :reverse
                        (or (and (numberp sort) (< sort 0))
                            (and (symbolp sort) (eq sort '-))))))
         (times-str (mapconcat
                     (lambda (name-time)
                       (format "%f %s"
                               (float-time (cdr name-time))
                               (car name-time)))
                     times-list
                     "\n")))
    (if (called-interactively-p)
        (message "(bandali-configure) execution times:\n%s" times-str)
      times-str)))

3.1.2. bandali-core.el macro for defining key bindings (bandali-define-key)

The bandali-define-key convenience macro is a wrapper around Emacs’s define-key, taking a sequence of keys and definitions, allowing binding a key sequence to a command, nil to unset it, a keymap, or anything else allowed by define-key itself.

(defmacro bandali-define-key (keymap &rest definitions)
  "Expand key binding DEFINITIONS for the given KEYMAP.
DEFINITIONS is a sequence of string and command pairs.
With inspiration from Protesilaos's `prot-emacs-keybind'."
  (declare (indent 1))
  (unless (zerop (% (length definitions) 2))
    (error "Uneven number of key+command pairs"))
  `(when-let* (((keymapp ,keymap))
               (map ,keymap))
     ,@(mapcar
        (lambda (pair)
          (pcase-let ((`(,key ,def) pair))
            (unless (and (null key) (null def))
              `(define-key map ,(if (stringp key) `(kbd ,key) key) ,def))))
        (seq-partition definitions 2))))

;; Usage example:

;; (bandali-define-key global-map
;;   "C-z" nil
;;   "C-x b" #'switch-to-buffer
;;   "C-x k" #'kill-buffer
;;   "C-c p" project-prefix-map)

3.1.3. bandali-core.el call to provide

Finally, we provide the module. This is the mirror function of require.

(provide 'bandali-core)
;;; bandali-core.el ends here

3.2. bandali-theme.el

In this module I define and/or customize things related to Emacs’s appearance.

;;; bandali-theme.el --- appearance-related configs  -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: faces, theme

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Code:

(require 'bandali-core)

3.2.1. bandali-theme.el load doric-themes

I use the beautiful doric-themes by Protesilaos, specifically doric-oak. Let’s load that when we’re in a graphical environment or in a terminal with true colour support.

(when (or (display-graphic-p)
          (string= (getenv "COLORTERM") "truecolor"))
  (with-eval-after-load 'doric-themes
    (setopt doric-themes-to-toggle '(doric-oak doric-pine)))
  (unless (fboundp 'doric-themes-select)
    (autoload #'doric-themes-select "doric-themes" nil t))
  (declare-function doric-themes-select "doric-themes")
  (doric-themes-select 'doric-oak))

3.2.2. bandali-theme.el font and typeface settings

Here I designate the Sahel typeface for Arabic script (primarily for Persian), set an emoji font, and set Source Code Pro typeface in medium weight as the font for Emacs’s default and fixed-pitch faces.

(when (display-graphic-p)
  (set-fontset-font t 'arabic "Sahel WOL")

  (let ((emoji-font "Apple Color Emoji"))
    (when (member emoji-font (font-family-list))
      (set-fontset-font
       t 'emoji `(,emoji-font . "iso10646-1") nil 'prepend)))

  (with-eval-after-load 'faces
    (let ((font "Source Code Pro Medium"))
      (set-face-attribute 'default nil :font font :height 115)
      ;; (set-face-attribute 'fixed-pitch nil :inherit 'default)
      )))

3.2.3. bandali-theme.el tweaks for text scaling

Here I apply a few small tweaks to Emacs’s text scaling, namely gentler font resizing text-scale-adjust (global-text-scale-adjust uses a separate increment mechanism), and applying scaling to header line text as well.

(with-eval-after-load 'face-remap
  (setopt
   text-scale-mode-step 1.05
   text-scale-remap-header-line t))

Further, Emacs 29 introduced commands for resizing text globally (across all buffers) including the minibuffer, which I find more useful. So, redefine the default keys to swap the non-global variants with the global ones, to make the latter more easily accessible.

(bandali-define-key global-map
  "C-x C-+" #'global-text-scale-adjust
  "C-x C-=" #'global-text-scale-adjust
  "C-x C--" #'global-text-scale-adjust
  "C-x C-0" #'global-text-scale-adjust
  "C-x C-M-+" #'text-scale-adjust
  "C-x C-M-=" #'text-scale-adjust
  "C-x C-M--" #'text-scale-adjust
  "C-x C-M-0" #'text-scale-adjust)

3.2.4. bandali-theme.el display fill-column indicator

In some situations I care a great deal about making sure to stick to a certain line length limit, namely fill-column. Emacs 28 introduced a display-fill-column-indicator feature to display a vertical line at the fill-column position to give a visual cue. I used to use global-display-fill-column-indicator-mode which displays the indicator in all buffers, but I’ve since found that a bit over the top for my tastes, and instead just enable the non-global mode for a few modes and their derivatives.

(run-with-idle-timer
 0.2 nil #'require 'display-fill-column-indicator nil 'noerror)
(with-eval-after-load 'display-fill-column-indicator
  ;; (global-display-fill-column-indicator-mode 1)
  (add-hook 'prog-mode-hook #'display-fill-column-indicator-mode)
  (add-hook 'message-mode-hook #'display-fill-column-indicator-mode))

3.2.5. bandali-theme.el call to provide

Finally, we provide the module. This is the mirror function of require.

(provide 'bandali-theme)
;;; bandali-theme.el ends here

3.3. bandali-essentials.el

This module loads basic configurations that apply to most facets of Emacs.

;;; bandali-essentials.el --- essentials of my setup  -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: files, internal

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Code:

(require 'bandali-core)

3.3.1. bandali-essentials.el settings to always start with *scratch* buffer

Going with the theme of preferring a clean and minimalist environment, I like starting with a scratch buffer, keeping initial-major-mode set to its default value of lisp-interaction-mode, so that typing C-j (M-x eval-print-last-sexp RET) evaluates and prints the return value of the form before point.

(setopt
 initial-buffer-choice t
 initial-major-mode #'lisp-interaction-mode
 initial-scratch-message
 (format
  ";; This is `%s'.  Evaluate and print results with `%s'.\n\n"
  #'lisp-interaction-mode
  (substitute-command-keys
   "\\<lisp-interaction-mode-map>\\[eval-print-last-sexp]")))

3.3.2. bandali-essentials.el start Emacs server if not already running

Using Emacs as $EDITOR and/or $VISUAL editor has the inconvenience that a new Emacs process would be launched each time text edition is needed, as the new Emacs process would not share buffers or other information with any existing Emacs process.

This problem can be solved by setting up Emacs as an “edit server” to have it listen for and handle edit requests from other programs. With an Emacs server started, we can set EDITOR to emacsclient, which will connect to the existing Emacs process and have it visit the file to be edited.

See: info “(emacs) Emacs Server”

(run-with-idle-timer 0.5 nil #'require 'server)
(with-eval-after-load 'server
  (eval-when-compile
    (declare-function server-edit "server")
    (declare-function server-running-p "server")
    (declare-function server-start "server"))
  (bandali-define-key global-map "C-c s e r" #'server-edit)
  (unless (server-running-p)
    (server-start)))

3.3.3. bandali-essentials.el add a fundamental-mode hook

By default, Emacs’s fundamental-mode does not come with a hook. While I understand the reason for this, I’ve still found myself in situations where I really needed one. So here I implement one by defining an advice around fundamental-hook.

(defvar bandali-fundamental-mode-hook nil
  "A hook for `fundamental-mode'.")

(advice-add
 #'fundamental-mode
 :around
 (lambda (&rest args)
   (apply args)
   (run-hooks 'bandali-fundamental-mode-hook)))

3.3.4. bandali-essentials.el TAB key behaviour and preferring spaces

I customize tab-always-indent so that pressing TAB will first try to indent the current line, and if the line was already indented, then try to complete the thing at point.

Also, I prefer to use spaces instead of tabs for indentation, so I set the default indent-tabs-mode to nil, and set a default tab-width of 4.

(setopt tab-always-indent 'complete)
(setq-default
 indent-tabs-mode nil
 tab-width 4)

3.3.5. bandali-essentials.el visually indicate buffer boundaries

I find it very helpful to have the first and last line of the buffer marked in the left fringe on graphical displays, as well as whether the window can be scrolled and if the buffer has a trailing newline character.

(setq-default indicate-buffer-boundaries 'left)

3.3.6. bandali-essentials.el re-enable some disabled commands and disable a few others

Some Emacs commands are disabled by default, meaning Emacs displays a warning and asks for confirmation before invoking them. Here I enable some of these disabled commands, and disable a few I don’t normally use and don’t want to accidentally invoke.

(mapc
 (lambda (command)
   (put command 'disabled nil))
 '( narrow-to-page narrow-to-region
    upcase-region downcase-region
    diff-restrict-view list-timers))

(mapc
 (lambda (command)
   (put command 'disabled t))
 '( overwrite-mode iconify-frame))

3.3.7. bandali-essentials.el settings for the package.el package manager

I currently don’t use package.el (or any other package manager), but I thought the following new option is important enough that I’d like to highlight it by including it in my configuration, and also in case I go back to using package.el at some point.

Emacs 31 introduces a great feature for package.el, a configurable policy for reviewing incoming packages before they are installed. This allows reviewing and reading the source code before the package is installed or activated. I set package-review-policy to t, to review all packages. The policy can be fine-tuned based on archive and package names.

See: C-h v package-review-policy RET

(with-eval-after-load 'package
  (setopt package-review-policy t))

3.3.8. bandali-essentials.el settings for Info, the documentation browser

One of the best parts of GNU Emacs is its Info system and manuals, which I read and refer to on a regular basis.

Here I extend Info-directory-list to include the info subdirectory of source-directory, the directory the Emacs sources were found in when was built, which in my case would be my local clone of the Emacs source repository. Sometimes I build Emacs from source, but don’t run make install to do a full installation elsewhere. Rather, I create symlinks to the built executables from my ~/.local/bin directory, so I could still run Emacs by typing emacs in the shell rather than typing its full absolute path. This addition to Info-directory-list allows me to access the info manual included in the Emacs repository in this use-case.

(defvar Info-directory-list)
(with-eval-after-load 'info
  (setq
   Info-directory-list
   `(,@Info-directory-list
     ,(expand-file-name
       (convert-standard-filename "info/") source-directory)
     "/usr/share/info/")))

3.3.9. bandali-essentials.el settings for tracking recently visited files (recentf-mode)

Emacs can keep track of recently visited files, so we could revisit them using recent-open, which provides minibuffer completion.

In addition to configuring a larger recentf-max-saved-items and defining a key binding for recentf-open, I also add a hook for vc-dir and dired to add the visited directory to recentf-list if the directory is not the home directory.

(run-with-idle-timer 0.2 nil #'require 'recentf)
(with-eval-after-load 'recentf
  (setopt recentf-max-saved-items 100)
  (recentf-mode 1)
  (bandali-define-key global-map
    "C-c f r e" #'recentf-open)

  (declare-function recentf-add-file "recentf")
  (defun bandali-recentf-add-dir-if-not-home ()
    "Add `default-directory' to `recentf-list' if we're not at $HOME."
    (unless
        (string=
         (file-name-as-directory (expand-file-name default-directory))
         (file-name-as-directory (expand-file-name (getenv "HOME"))))
      (recentf-add-file default-directory)))

  (declare-function
   bandali-recentf-add-dir-if-not-home "bandali-essentials")
  (with-eval-after-load 'vc-dir
    (add-hook 'vc-dir-mode-hook #'bandali-recentf-add-dir-if-not-home))
  (with-eval-after-load 'dired
    (add-hook 'dired-mode-hook #'bandali-recentf-add-dir-if-not-home)))

3.3.10. bandali-essentials.el settings for mouse and scroll behaviour

Here are my customizations having to with the mouse and scrolling behaviour.

I’d like fairly slow, predictable behaviour, I don’t like things jumping around. So I set it so that the mouse wheel scrolls one light at a time and without any acceleration. Also, I set the mouse wheel to scroll the window under the mouse, so I could simply place the mouse over a window and scroll, and not have to manually switch the focus back and forth. There is also a mouse-autoselect-window user option to have focus follow the mouse, but I keep the default nil because especially on modern laptops it’s too easy to accidentally move the mouse by touching the touchpad when typing, and end up typing into another window.

I also enable pixel-scroll-mode introduced in Emacs 26, to scroll text pixel-by-pixel.

For the scroll behaviour, by default Emacs recenters the point if it moves off screen, which can be a bit jarring. So I set a higher value for scroll-conservatively to make sure that doesn’t happen when scrolling line-by-line. Also, the default behaviour of Emacs when scrolling with the mouse is to move the point as if scrolling up or down line-by-line. Setting scroll-preserve-screen-position to 1 causes it keep the point in place and move the buffer contents up or down, more similar to scrolling in other applications.

(run-with-idle-timer 0.4 nil #'require 'mwheel)
(with-eval-after-load 'mwheel
  (setopt
   mouse-wheel-scroll-amount '(1 ((shift) . 1)) ; one line at a time
   mouse-wheel-progressive-speed nil    ; don't accelerate scrolling
   mouse-wheel-follow-mouse t))         ; scroll window under mouse

(run-with-idle-timer 0.4 nil #'require 'pixel-scroll)
(with-eval-after-load 'pixel-scroll
  (pixel-scroll-mode 1))

(setopt
 ;; mouse-autoselect-window t
 scroll-conservatively 15
 scroll-preserve-screen-position 1)

3.3.11. bandali-essentials.el settings for auto revert

Emacs can detect when a buffer’s underlying file changes on disk, and automatically reload/refresh the buffer. This can be very useful when an open file is changed outside Emacs, so by automatically reloading the buffer we’ll lessen the chances of accidentally overwriting and discarding external changes. Of course, after an auto revert, we can press C-/ (undo) to temporarily restore the contents of the buffer before the auto revert, grab any unsaved changes we had previously made, and hit C-/ again to restore the new contents, then apply some or all of our unsaved changes manually if necessary.

Setting global-auto-revert-non-file-buffers to nil ensures that global-auto-revert-mode operates only on file-visiting buffers.

(run-with-idle-timer 0.1 nil #'require 'autorevert)
(with-eval-after-load 'autorevert
  (setopt
   global-auto-revert-non-file-buffers nil)
  (global-auto-revert-mode 1))

3.3.12. bandali-essentials.el settings for repeat-mode

Here I configure the repeat-mode feature introduced in Emacs 28. It makes my EXWM and other window-related configurations more convenient.

(run-with-idle-timer 0.5 nil #'require 'repeat)
(with-eval-after-load 'repeat
  (setopt
   repeat-exit-key "RET"
   repeat-exit-timeout nil)
  (repeat-mode 1))

3.3.13. bandali-essentials.el settings for date and time

Here I configure the display of the current date and time, primarily for display on the mode line using display-time-mode, but can also be used from outside Emacs by calling a function via emacsclient.

(defvar zoneinfo-style-world-list)
(run-with-idle-timer 0.1 nil #'require 'time)
(with-eval-after-load 'time
  (setopt
   ;; display-time-default-load-average nil
   display-time-format " %a %-d %b %-l:%M%P"
   display-time-mail-icon
   '(image :type xpm :file "gnus/gnus-pointer.xpm" :ascent center)
   display-time-use-mail-icon t
   zoneinfo-style-world-list
   '(("America/Los_Angeles" "San Francisco")
     ("America/Toronto" "Toronto")
     ("Etc/UTC" "UTC")
     ("Europe/Athens" "Cyprus")
     ("Asia/Tehran" "Tehran")))
  (display-time-mode 1))

3.3.14. bandali-essentials.el settings for displaying battery

Here I configure the display of the current battery level - percentage and remaining time - primarily for display on the mode line using display-battery-mode, but can also be used from outside Emacs by calling a function via emacsclient.

(defvar bandali-battery-format "%p%b %t")
(run-with-idle-timer 0.1 nil #'require 'battery)
(with-eval-after-load 'battery
  (setopt
   battery-mode-line-format (format " [%s]" bandali-battery-format))
  (display-battery-mode 1))

In fact, in one of my setups using swaywm, my b-bar script evaluates an Emacs Lisp snippet like the following sample via emacsclient to retrieve current battery levels from Emacs for display on the bar.

(battery-format
 bandali-battery-format (funcall battery-status-function))

3.3.15. bandali-essentials.el settings for trusted-content

Emacs 30 introduced the security feature to indicate the list of files and directories whose contents we trust.

(setopt
 trusted-content
 `(,(file-name-as-directory (locate-user-emacs-file "lisp/ffs"))))

3.3.16. bandali-essentials.el settings for password-store

pass is a Unix-style password manager. Here I configure the password-store package for convenient access to pass in Emacs.

(with-eval-after-load 'password-store
  (setopt password-store-time-before-clipboard-restore 5))

(defvar-keymap bandali-prefix-password-store-map
  :doc "Prefix keymap for password-store."
  :name "Password-store"
  :prefix 'bandali-prefix-password-store
  "p" #'password-store-copy
  "g" #'password-store-generate
  "i" #'password-store-insert
  "r" #'password-store-remove
  "m" #'password-store-rename
  "e" #'password-store-edit)

(bandali-define-key global-map
  "C-c p" #'bandali-prefix-password-store)

3.3.17. bandali-essentials.el settings for the eat terminal emulator

I use the excellent eat (Emulate A Terminal) for my terminal emulation needs inside GNU Emacs.

I’m using my personal fork of Eat with patches by myself or others that are awaiting review and merge upstream while the maintainer is unavailable.

(with-eval-after-load 'eat
  ;; (setq process-adaptive-read-buffering t)
  (setopt
   ;; eat-enable-shell-prompt-annotation nil
   eat-enable-shell-command-history nil)
  ;; (bandali-define-key eat-char-mode-map
  ;;   "<remap> <mouse-yank-primary>" #'eat-mouse-yank-primary
  ;;   "<remap> <mouse-yank-secondary>" #'eat-mouse-yank-secondary
  ;;   "M-RET" #'eat-line-mode)
  ;; (bandali-define-key eat-line-mode-map "M-RET" #'eat-char-mode)
  ;; (add-hook 'eat-exec-hook (lambda (_) (eat-char-mode)))
  (add-hook
   'eat-mode-hook
   (lambda ()
     (with-eval-after-load 'display-fill-column-indicator
       (display-fill-column-indicator-mode -1))))
  (with-eval-after-load 'info
    (add-to-list 'Info-directory-list
                 (locate-user-emacs-file "lisp/eat"))))
(defun bandali-eat ()
  (interactive)
  (let ((current-prefix-arg '(4))) ; C-u
    (call-interactively #'eat)))
(bandali-define-key global-map "C-c s e t" #'bandali-eat)

3.3.18. bandali-essentials.el settings for clipboard and primary selection

By default GNU Emacs uses the clipboard when copying and pasting. However some older applications like XTerm use primary selection. So I configure Emacs to enable both for a more seamless experience.

(with-eval-after-load 'select
  (setopt
   select-enable-clipboard t
   ;; select-enable-primary t
   ))

3.3.19. TODO bandali-essentials.el rest of init

(setq
 ;; line-spacing 3
 delete-by-moving-to-trash t
 max-mini-window-height 0.20
 ;; resize-mini-windows t
 message-log-max 20000)

(with-eval-after-load 'files
  (setopt
   make-backup-files nil
   ;; Insert newline at the end of files.
   ;; require-final-newline t
   ;; Open read-only file buffers in view-mode, to get `q' for quit.
   view-read-only t))
(bandali-define-key global-map "C-c f ." #'find-file)

(with-eval-after-load 'epg-config
  (setopt
   epg-gpg-program (executable-find "gpg")
   ;; Ask for GPG passphrase in minibuffer.
   ;; Will fail if gpg >= 2.1 is not available.
   epg-pinentry-mode 'loopback))

(with-eval-after-load 'help
  (temp-buffer-resize-mode 1)
  (setopt help-window-select t))

(with-eval-after-load 'help-mode
  (bandali-define-key help-mode-map
    "P" #'backward-button
    "N" #'forward-button
    "b" #'help-go-back
    "f" #'help-go-forward))

(with-eval-after-load 'man
  (setopt Man-width 80))

(with-eval-after-load 'tramp
  (tramp-set-completion-function
   "ssh"
   (append (tramp-get-completion-function "ssh")
           (mapcar (lambda (file) `(tramp-parse-sconfig ,file))
                   (directory-files
                    "~/.ssh/config.d/"
                    'full directory-files-no-dot-files-regexp)))))

(with-eval-after-load 'simple
  (setopt
   ;; See `bandali-gnus' for my Gnus configuration.
   mail-user-agent 'gnus-user-agent
   read-mail-command #'gnus
   ;; Save what I copy into clipboard from other applications into
   ;; Emacs' kill-ring, which would allow me to still be able to
   ;; easily access it in case I kill (cut or copy) something else
   ;; inside Emacs before yanking (pasting) what I'd originally
   ;; intended to.
   save-interprogram-paste-before-kill t)
  (column-number-mode 1)
  (line-number-mode 1))
(bandali-define-key global-map "C-x k" #'kill-current-buffer)
;; (add-hook 'text-mode-hook #'auto-fill-mode)
;; (add-hook 'tex-mode-hook #'auto-fill-mode)

;; Save minibuffer history.
(run-with-idle-timer 0.2 nil #'require 'savehist)
(with-eval-after-load 'savehist
  (savehist-mode 1)
  (add-to-list 'savehist-additional-variables 'kill-ring))

;; Automatically save place in files.
(run-with-idle-timer 0.2 nil #'require 'saveplace nil 'noerror)
(with-eval-after-load 'saveplace
  (save-place-mode 1))

;; `abbrev'
(add-hook 'text-mode-hook #'abbrev-mode)

(with-eval-after-load 'calendar
  (setopt
   calendar-date-style 'iso
   calendar-mark-diary-entries-flag t
   diary-file "~/usr/doc/diary"))
(bandali-define-key global-map "C-c c" #'calendar)
(with-eval-after-load 'diary-lib
  (setopt
   diary-comment-start ";")
  (add-hook 'diary-list-entries-hook 'diary-sort-entries t))
(if (version<= "31" emacs-version)
    (setopt
     holiday-other-holidays
     '((holiday-float 10 1 2 "Canadian Thanksgiving")))
  (setq ; must be `setq'd before `holidays' is loaded
   holiday-other-holidays
   '((holiday-float 10 1 2 "Canadian Thanksgiving"))))

;; `ffs'
(add-hook
 'ffs-present-mode
 (lambda ()
   (let ((arg (if ffs-present-mode -1 1)))
     (mapc
      (lambda (mode) (funcall mode arg))
      '(show-paren-local-mode
        display-fill-column-indicator-mode
        flyspell-mode)))
   (if ffs-present-mode
       (fringe-mode 0)
     (fringe-mode nil))))

3.3.20. bandali-essentials.el key bindings

Here I define or repurpose key bindings for some of the built-in Emacs commands I find useful and frequently use.

First, I define a bandali-call-interactively-insert helper function which will take a command name and a string as arguments, and return a lambada that when called will execute the command and prefill the minibuffer with the provided string.

(defun bandali-call-interactively-insert (command string)
  "Execute interactive COMMAND with STRING prefilled in minibuffer.
Returns a lambda that when executed will execute COMMAND interactively,
with STRING inserted into the minibuffer.  Useful for binding to a key."
  (lambda ()
    (interactive)
    (let ((cmd command)
          (str string))
      (minibuffer-with-setup-hook
          (lambda () (insert str))
        (call-interactively cmd)))))
(declare-function
 bandali-call-interactively-insert "bandali-essentials")

I can now use the above function to bind a few help keys below.

(bandali-define-key global-map
  "C-z" nil ; `suspend-frame' is already bound to `C-x C-z'
  ;; `time'
  "C-c e i" #'emacs-init-time
  "C-c e u" #'emacs-uptime
  ;; `version'
  "C-c e v" #'emacs-version
  ;; `ffap'
  "C-c f p" #'find-file-at-point
  ;; `find-func'
  "C-c f l" #'find-library
  ;; `frame'
  "C-c f r m" #'make-frame-command
  "C-c f r d" #'delete-frame
  ;; `help'-related
  "C-c h a" (bandali-call-interactively-insert
             #'execute-extended-command "apropos-")
  "C-c h d" (bandali-call-interactively-insert
             #'execute-extended-command "describe-")
  "C-c h f" (bandali-call-interactively-insert
             #'execute-extended-command "find-")
  ;; `simple'
  "M-=" #'count-words
  "M-o" #'delete-blank-lines
  "M-c" #'capitalize-dwim
  "M-l" #'downcase-dwim
  "M-u" #'upcase-dwim
  ;; `misc'
  "M-z" #'zap-up-to-char)

3.3.21. bandali-essentials.el call to provide

Finally, we provide the module. This is the mirror function of require.

(provide 'bandali-essentials)
;;; bandali-essentials.el ends here

3.4. bandali-window.el

;;; bandali-window.el --- window-related configs  -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: convenience, frames

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Includes my EXWM setup, which makes use of EXWM's simulation keys.

;;; Code:

(require 'bandali-core)
(require 'bandali-simple)

(with-eval-after-load 'frame
  (undelete-frame-mode 1))
(bandali-define-key global-map
  "C-c f r u" #'undelete-frame)

(defvar bandali-exwm-machines '("adelita" "marita")
  "Host name of machines where I use EXWM.")

(defvar bandali-hidpi-machines '(("marita" . 144))
  "Host name and DPI of of machines with HiDPI display.")

(when (and
       (display-graphic-p)
       ;; we're not running in another WM/DE
       (not (or
             (getenv "XDG_CURRENT_DESKTOP")
             (getenv "WAYLAND_DISPLAY")))
       (member (system-name) bandali-exwm-machines))
  (require 'exwm)

  (bandali-define-key global-map
    "C-x b" #'exwm-workspace-switch-to-buffer)

  (let ((e "emacsclient"))
    (setenv "EDITOR" e)
    (setenv "VISUAL" e))

  (menu-bar-mode -1)
  (tool-bar-mode -1)

  (defun bandali-exwm-rename-buffer ()
    "Make class name the buffer name, truncating beyond 25 characters."
    (interactive)
    (exwm-workspace-rename-buffer
     (concat exwm-class-name ":"
             (if (<= (length exwm-title) 25) exwm-title
               (concat (substring exwm-title 0 24) "...")))))

  ;; Initial number of workspaces
  (setopt exwm-workspace-number 5)

  (defvar bandali-shifted-ws-names
    '( 0 \) 1 \! 2 \@ 3 \# 4 \$
       5 \% 6 \^ 7 \& 8 \* 9 \()
    "Mapping of shifted numbers on my keyboard.")

  (defvar-keymap bandali-prefix-browser-launch-map
    :doc "Prefix keymap for launching browsers."
    :name "Browser launch"
    :prefix 'bandali-prefix-browser-launch
    "c" (lambda () (interactive)
          (start-process
           "" nil "chromium"))
    "C" (lambda () (interactive)
          (start-process
           "" nil "chromium" "--incognito"))
    "i" (lambda () (interactive)
          (start-process
           "" nil "b-browser" "-P" "ia"))
    "I" (lambda () (interactive)
          (start-process
           "" nil "b-browser" "-P" "ia" "-private-window"))
    "p" (lambda () (interactive)
          (start-process
           "" nil "b-browser" "-P" "personal"))
    "P" (lambda () (interactive)
          (start-process
           "" nil "b-browser" "-P" "personal" "-private-window")))

  (defvar-keymap bandali-prefix-exwm-map
    :doc "Prefix keymap for EXWM."
    :name "EXWM"
    :prefix 'bandali-prefix-exwm
    "r" #'exwm-reset ; to line mode
    "w" #'exwm-workspace-switch
    "SPC" #'async-shell-command
    "RET" #'bandali-eat
    "C-k" (lambda () (interactive) (exwm-manage--kill-client))
    "t" (lambda () (interactive) (start-process "" nil "xterm"))
    "T" (lambda () (interactive)
          (start-process "" nil "xterm" "-name" "floating"))
    "h" #'windmove-left
    "j" #'windmove-down
    "k" #'windmove-up
    "l" #'windmove-right
    "H" #'windmove-swap-states-left
    "J" #'windmove-swap-states-down
    "K" #'windmove-swap-states-up
    "L" #'windmove-swap-states-right
    "M-h" #'shrink-window-horizontally
    "M-j" #'enlarge-window
    "M-k" #'shrink-window
    "M-l" #'enlarge-window-horizontally
    "p" #'bandali-exwm-ws-prev
    "n" #'bandali-exwm-ws-next
    "P" #'bandali-exwm-move-ws-prev
    "N" #'bandali-exwm-move-ws-next
    "," (lambda () (interactive) (other-frame -1))
    ";" #'bandali-pactl-set-default-sink-volume
    ":" #'bandali-pactl-set-default-source-volume
    "'" #'bandali-brightnessctl-set
    "." #'exwm-floating-toggle-floating
    "f" #'exwm-layout-toggle-fullscreen
    "b" #'bandali-prefix-browser-launch)

  (defvar exwm-workspace--create-silently)
  (defun bandali-exwm-workspace-create (frame-or-index)
    "Create (up to) workspace FRAME-OR-INDEX without switching to it."
    (interactive
     (list
      (cond
       ((integerp current-prefix-arg)
        current-prefix-arg)
       (t 0))))
    (unless frame-or-index
      (setq frame-or-index 0))
    (unless (or (framep frame-or-index)
                (< frame-or-index (exwm-workspace--count)))
      (let ((count (1+ (- frame-or-index (exwm-workspace--count))))
            (exwm-workspace--create-silently t))
        (when (< count exwm-workspace-switch-create-limit)
          (dotimes (_ count) (make-frame))
          (run-hooks 'exwm-workspace-list-change-hook)))))

  (mapc
   (lambda (i)
     (bandali-define-key bandali-prefix-exwm-map
       (format "%d" i)
       (lambda () (interactive) (exwm-workspace-switch-create i))
       (format "%s" (plist-get bandali-shifted-ws-names i))
       (lambda ()
         (interactive)
         (bandali-exwm-workspace-create i)
         (exwm-workspace-move-window i))))
   (number-sequence 0 (1- exwm-workspace-switch-create-limit)))

  (defvar-keymap bandali-prefix-exwm-mvmt-repeat-map
    :doc "Keymap to repeat EXWM movement commands.  Used with `repeat-mode'."
    :repeat t
    "h" #'windmove-left
    "j" #'windmove-down
    "k" #'windmove-up
    "l" #'windmove-right
    "H" #'windmove-swap-states-left
    "J" #'windmove-swap-states-down
    "K" #'windmove-swap-states-up
    "L" #'windmove-swap-states-right
    "p" #'bandali-exwm-ws-prev
    "n" #'bandali-exwm-ws-next
    "P" #'bandali-exwm-move-ws-prev
    "N" #'bandali-exwm-move-ws-next)

  (with-eval-after-load 'exwm-input
    (push ?\s-, exwm-input-prefix-keys)
    (push ?\s-x exwm-input-prefix-keys))

  (setq
   ;; Global keybindings
   exwm-input-global-keys
   `(([?\C-c ?x] . bandali-prefix-exwm)
     ([?\s-x] . bandali-prefix-exwm)
     ([?\s-,] . bandali-prefix-exwm)
     ([XF86AudioMute]
      .
      (lambda ()
        (interactive)
        (start-process
         "" nil
         "pactl" "set-sink-mute" "@DEFAULT_SINK@" "toggle")))
     ([XF86AudioLowerVolume]
      .
      (lambda ()
        (interactive)
        (start-process
         "" nil
         "pactl" "set-sink-volume" "@DEFAULT_SINK@" "-5%")))
     ([XF86AudioRaiseVolume]
      .
      (lambda ()
        (interactive)
        (start-process
         "" nil
         "pactl" "set-sink-volume" "@DEFAULT_SINK@" "+5%"))))
   ;; Line-editing shortcuts
   exwm-input-simulation-keys
   '(;; movement
     ([?\C-b] . [left])
     ([?\M-b] . [C-left])
     ([?\C-f] . [right])
     ([?\M-f] . [C-right])
     ([?\C-p] . [up])
     ([?\C-n] . [down])
     ([?\C-a] . [home])
     ([?\C-e] . [end])
     ([?\M-v] . [prior])
     ([?\C-v] . [next])
     ([?\C-d] . [delete])
     ([?\C-k] . [S-end ?\C-x])
     ([?\M-<] . C-home)
     ([?\M->] . C-end)
     ;; selection/cut/copy/paste
     ([?\s-a] . [?\C-a])
     ([?\C-w] . [?\C-x])
     ([?\M-w] . [?\C-c])
     ([?\C-y] . [?\C-v])
     ([?\M-d] . [C-S-right ?\C-x])
     ([?\M-\d] . [C-S-left ?\C-x])
     ;; closing/quite
     ([?\s-w] . [?\C-w])
     ([?\s-q] . [?\C-q])
     ;; misc
     ([?\C-s] . [?\C-f])
     ([?\s-d] . [?\C-d])
     ([?\s-g] . [?\C-g])
     ([?\s-s] . [?\C-s])
     ([?\C-g] . [escape])
     ([?\C-/] . [?\C-z])))

  (with-eval-after-load 'exwm-manage
    (setq
     exwm-manage-configurations
     '(((equal exwm-instance-name "floating")
        ;; char-mode t
        floating t
        ;; floating-mode-line nil
        )
       ((member exwm-class-name '("Mate-terminal"))
        char-mode t)))
    (add-hook
     'exwm-manage-finish-hook
     (lambda ()
       (when exwm-class-name
         (cond
          ((member exwm-class-name '("XTerm" "Mate-terminal"))
           (exwm-input-set-local-simulation-keys
            '(([?\C-c ?\C-c] . [?\C-c])
              ([?\C-c ?\C-u] . [?\C-u]))))
          ((string= exwm-class-name "Zathura")
           (exwm-input-set-local-simulation-keys
            '(([?\C-p] . [C-up])
              ([?\C-n] . [C-down])))))))))

  ;; Enable EXWM
  (exwm-wm-mode 1)
  (add-hook 'exwm-update-class-hook #'bandali-exwm-rename-buffer)
  (add-hook 'exwm-update-title-hook #'bandali-exwm-rename-buffer)

  (when (executable-find "dunst")
    (defvar bandali--dunst-process
      (start-process "dunst" "*dunst*" "dunst"))
    (set-process-query-on-exit-flag bandali--dunst-process nil)
    (when (executable-find "dunstctl")
      (bandali-define-key bandali-prefix-exwm-map
        "d RET" (lambda () (interactive)
                  (start-process "" nil "dunstctl" "context"))
        "d d" (lambda () (interactive)
                (start-process "" nil "dunstctl" "close"))
        "d D" (lambda () (interactive)
                (start-process "" nil "dunstctl" "close-all"))
        "d r" (lambda () (interactive)
                (start-process "" nil "dunstctl" "history-pop")))))

  (require 'exwm-input)
  (defun bandali-exwm-ws-prev-index (&optional arg)
    "Return the index for the previous EXWM workspace, wrapping around if needed."
    (let ((max (if arg
                   exwm-workspace-switch-create-limit
                 (exwm-workspace--count))))
      (if (<= exwm-workspace-current-index 0)
          (1- max)
        (1- exwm-workspace-current-index))))

  (defun bandali-exwm-ws-next-index (&optional arg)
    "Return the index for the next EXWM workspace, wrapping around if needed."
    (let ((max (if arg
                   exwm-workspace-switch-create-limit
                 (exwm-workspace--count))))
      (if (>= exwm-workspace-current-index (1- max))
          0
        (1+ exwm-workspace-current-index))))

  (defun bandali-exwm-ws-prev (&optional arg)
    "Switch to previous EXWM workspace, wrapping around if needed.
If prefix argument is set, allow going beyond currently existing
workspaces and create new ones, respecting
`exwm-workspace-switch-create-limit'."
    (interactive "P")
    (exwm-workspace-switch-create
     (bandali-exwm-ws-prev-index arg)))

  (defun bandali-exwm-ws-next (&optional arg)
    "Switch to next EXWM workspace, wrapping around if needed.
If prefix argument is set, allow going beyond currently existing
workspaces and create new ones, respecting
`exwm-workspace-switch-create-limit'."
    (interactive "P")
    (exwm-workspace-switch-create
     (bandali-exwm-ws-next-index arg)))

  (defun bandali-exwm-move-ws-prev ()
    "Move window to previous workspace."
    (interactive)
    (exwm-workspace-move-window (bandali-exwm-ws-prev-index)))

  (defun bandali-exwm-move-ws-next ()
    "Move window to next workspace."
    (interactive)
    (exwm-workspace-move-window (bandali-exwm-ws-next-index)))


  ;; Shorten 'C-c C-q' to 'C-q'
  (define-key exwm-mode-map [?\C-q] #'exwm-input-send-next-key)

  (add-hook
   'exwm-init-hook
   (lambda ()
     (setq my-last-frame (selected-frame))))

  (add-hook
   'exwm-init-hook
   (lambda ()
     ;; Scroll up/down/left/right on the mode line
     (bandali-define-key global-map
       "<mode-line> <wheel-up>" #'bandali-exwm-ws-prev
       "<mode-line> <wheel-down>" #'bandali-exwm-ws-next
       "<mode-line> <wheel-left>" #'bandali-exwm-ws-prev
       "<mode-line> <wheel-right>" #'bandali-exwm-ws-next)))

  ;; Scroll up/down/left/right on the echo area
  (bandali-define-key minibuffer-inactive-mode-map
    [wheel-up] #'bandali-exwm-ws-prev
    [wheel-down] #'bandali-exwm-ws-next
    [wheel-left] #'bandali-exwm-ws-prev
    [wheel-right] #'bandali-exwm-ws-next)

  (require 'exwm-systemtray)
  (exwm-systemtray-mode 1)

  ;; (add-to-list 'load-path (locate-user-emacs-file "lisp/exwm-edit"))
  ;; (require 'exwm-edit)

  (defun bandali-exwm-xsettings ()
    (let* ((host-dpi (assoc (system-name) bandali-hidpi-machines))
           (dpi (if (and host-dpi
                         (= (length (display-monitor-attributes-list))
                            1))
                    (cdr host-dpi)
                  96)))
      `(("Xft/Hinting" . 1)
        ("Xft/AutoHint" . 0)
        ("Xft/HintStyle" . "hintslight")
        ("Xft/Antialias" . 1)
        ("Xft/RGBA" . "rgb")
        ("Xft/lcdfilter" . "lcddefault")
        ;; DPI is in 1024ths of an inch
        ("Xft/DPI" . ,(* dpi 1024)))))

  (with-eval-after-load 'exwm-xsettings
    (setopt exwm-xsettings (bandali-exwm-xsettings)))
  (require 'exwm-xsettings)
  (exwm-xsettings-mode 1)

  (require 'exwm-randr)
  (add-hook
   'exwm-randr-screen-change-hook
   (lambda ()
     (let ((xrandr-output-regexp "\n\\([^ ]+\\) connected ")
           default-output)
       (with-temp-buffer
         (call-process "xrandr" nil t nil)
         (goto-char (point-min))
         (re-search-forward xrandr-output-regexp nil 'noerror)
         (setq default-output (match-string 1))
         (forward-line)
         (if (not (re-search-forward xrandr-output-regexp nil 'noerror))
             (progn
               (call-process
                "xrandr" nil nil nil "--auto")
               (call-process
                "xrandr" nil nil nil "--output" default-output
                "--auto"))
           (call-process
            "xrandr" nil nil nil
            "--output" (match-string 1)
            "--right-of" default-output
            "--auto"
            "--output" default-output "--mode" "1920x1080")
           (setopt
            exwm-randr-workspace-monitor-plist
            (mapcan
             (lambda (i)
               (list i (if (< i 2) default-output (match-string 1))))
             (number-sequence
              0 (1- exwm-workspace-switch-create-limit)))))))))
  (add-hook
   'exwm-randr-refresh-hook
   (lambda () (setopt exwm-xsettings (bandali-exwm-xsettings))))
  (exwm-randr-mode 1)

  (with-eval-after-load 'exwm-workspace
    (bandali-define-key exwm-workspace-switch-map
      "p" #'next-history-element
      "n" #'previous-history-element)
    (setq exwm-workspace-show-all-buffers t)
    ;; Display current EXWM workspace in mode-line
    (setq-default
     mode-line-format
     (append
      mode-line-format
      '((:eval
         (format
          " [%s]" (number-to-string
                   exwm-workspace-current-index))))))))

(with-eval-after-load 'window
  (setopt split-width-threshold 140))

(run-with-idle-timer 0.5 nil #'require 'winner)
(with-eval-after-load 'winner
  (winner-mode 1)
  (when (featurep 'exwm)
    ;; prevent a bad interaction between EXWM and winner-mode, where
    ;; sometimes closing a window (like closing a terminal after
    ;; entering a GPG password via pinentry-gnome3's floating window)
    ;; results in a dead frame somewhere and effectively freezes EXWM.
    (advice-add
     'winner-insert-if-new
     :around
     (lambda (orig-fun &rest args)
       ;; only add the frame if it's live
       (when (frame-live-p (car args))
         (apply orig-fun args))))))

(run-with-idle-timer 0.5 nil #'require 'windmove)
(with-eval-after-load 'windmove
  (setopt windmove-wrap-around t))

(provide 'bandali-window)
;;; bandali-window.el ends here

3.5. bandali-mode-line.el

This module configures the Emacs mode line.

;;; bandali-mode-line.el --- mode-line configs  -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: convenience, frames

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Code:

3.5.1. bandali-mode-line.el basic settings for the mode line

Here I apply some basic settings for the mode line.

(setopt
 mode-line-compact nil ; Emacs 28
 mode-line-right-align-edge 'right-margin) ; Emacs 30

3.5.2. bandali-mode-line.el defining faces for my custom mode line constructs

Here I define the faces that I will use for my custom mode line constructs (bandali-mode-line.el defining my custom mode line constructs).

(defgroup bandali-mode-line nil
  "Custom mode line that is stylistically close to the default."
  :group 'mode-line)

(defgroup bandali-mode-line-faces nil
  "Faces for my custom modeline."
  :group 'bandali-mode-line)

(defface bandali-mode-line-indicator-button nil
  "Generic face used for indicators that have a background.
Modify this face to, for example, add a :box attribute to all
relevant indicators (combines nicely with my `spacious-padding'
package).")

(defface bandali-mode-line-indicator-small
  '((t :height 0.8))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-red
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#880000")
    (((class color) (min-colors 88) (background dark))
     :foreground "#ff9f9f")
    (t :foreground "red"))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-red-bg
  '((default :inherit (bold bandali-mode-line-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#aa1111" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#ff9090" :foreground "black")
    (t :background "red" :foreground "black"))
  "Face for modeline indicators with a background."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-green
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#005f00")
    (((class color) (min-colors 88) (background dark))
     :foreground "#73fa7f")
    (t :foreground "green"))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-green-bg
  '((default :inherit (bold bandali-mode-line-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#207b20" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#77d077" :foreground "black")
    (t :background "green" :foreground "black"))
  "Face for modeline indicators with a background."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-yellow
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#6f4000")
    (((class color) (min-colors 88) (background dark))
     :foreground "#f0c526")
    (t :foreground "yellow"))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-yellow-bg
  '((default :inherit (bold bandali-mode-line-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#805000" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#ffc800" :foreground "black")
    (t :background "yellow" :foreground "black"))
  "Face for modeline indicators with a background."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-blue
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#00228a")
    (((class color) (min-colors 88) (background dark))
     :foreground "#88bfff")
    (t :foreground "blue"))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-blue-bg
  '((default :inherit (bold bandali-mode-line-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#0000aa" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#77aaff" :foreground "black")
    (t :background "blue" :foreground "black"))
  "Face for modeline indicators with a background."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-magenta
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#6a1aaf")
    (((class color) (min-colors 88) (background dark))
     :foreground "#e0a0ff")
    (t :foreground "magenta"))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-magenta-bg
  '((default :inherit (bold bandali-mode-line-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#6f0f9f" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#e3a2ff" :foreground "black")
    (t :background "magenta" :foreground "black"))
  "Face for modeline indicators with a background."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-cyan
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#004060")
    (((class color) (min-colors 88) (background dark))
     :foreground "#30b7cc")
    (t :foreground "cyan"))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-cyan-bg
  '((default :inherit (bold bandali-mode-line-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#006080" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#40c0e0" :foreground "black")
    (t :background "cyan" :foreground "black"))
  "Face for modeline indicators with a background."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-gray
  '((t :inherit (bold shadow)))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'bandali-mode-line-faces)

(defface bandali-mode-line-indicator-gray-bg
  '((default :inherit (bold bandali-mode-line-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#808080" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#a0a0a0" :foreground "black")
    (t :inverse-video t))
  "Face for modeline indicatovrs with a background."
  :group 'bandali-mode-line-faces)

(require 'doric-themes)
(defun bandali-mode-line-set-faces ()
  (doric-themes-with-colors
    (custom-set-faces
     `(bandali-mode-line-indicator-red
       ((t :inherit bold :foreground ,fg-red)))
     `(bandali-mode-line-indicator-green
       ((t :inherit bold :foreground ,fg-green)))
     `(bandali-mode-line-indicator-yellow
       ((t :inherit bold :foreground ,fg-yellow)))
     `(bandali-mode-line-indicator-blue
       ((t :inherit bold :foreground ,fg-blue)))
     `(bandali-mode-line-indicator-magenta
       ((t :inherit bold :foreground ,fg-magenta)))
     `(bandali-mode-line-indicator-cyan
       ((t :inherit bold :foreground ,fg-cyan)))
     `(bandali-mode-line-indicator-red-bg
       ((t :inherit (bold bandali-mode-line-indicator-button)
           :background ,bg-red :foreground ,fg-main)))
     `(bandali-mode-line-indicator-green-bg
       ((t :inherit (bold bandali-mode-line-indicator-button)
           :background ,bg-green :foreground ,fg-main)))
     `(bandali-mode-line-indicator-yellow-bg
       ((t :inherit (bold bandali-mode-line-indicator-button)
           :background ,bg-yellow :foreground ,fg-main)))
     `(bandali-mode-line-indicator-blue-bg
       ((t :inherit (bold bandali-mode-line-indicator-button)
           :background ,bg-blue :foreground ,fg-main)))
     `(bandali-mode-line-indicator-magenta-bg
       ((t :inherit (bold bandali-mode-line-indicator-button)
           :background ,bg-magenta :foreground ,fg-main)))
     `(bandali-mode-line-indicator-cyan-bg
       ((t :inherit (bold bandali-mode-line-indicator-button)
           :background ,bg-cyan :foreground ,fg-main)))
     ;; "Padding" for mode lines
     `(mode-line
       ((t :box (:line-width 6 :color ,bg-shadow-intense))))
     `(mode-line-inactive
       ((t :box (:line-width 6 :color ,bg-shadow-subtle))))
     `(mode-line-highlight
       ((t :box (:color ,bg-shadow-intense)))))))
(add-hook
 'doric-themes-after-load-theme-hook #'bandali-mode-line-set-faces)
(bandali-mode-line-set-faces)

3.5.3. bandali-mode-line.el defining my custom mode line constructs

Here I define my custom mode line constructs, which I will use later (bandali-mode-line.el setting the mode-line-format).

(defvar-local bandali-mode-line-kbd-macro
  '(:eval
    (when (and (mode-line-window-selected-p) defining-kbd-macro)
      (propertize
       " K "
       'face 'bandali-mode-line-indicator-blue-bg
       'mouse-face 'mode-line-highlight)))
  "Mode line construct displaying `mode-line-defining-kbd-macro'.
Specific to the current window's mode line.")

(defvar-local bandali-mode-line-narrow
  '(:eval
    (when
        (and (mode-line-window-selected-p)
             (buffer-narrowed-p)
             (not (derived-mode-p
                   '(Info-mode help-mode special-mode message-mode))))
      (propertize
       " N "
       'face 'bandali-mode-line-indicator-cyan-bg
       'help-echo "mouse-2: Remove narrowing from buffer"
       'mouse-face 'mode-line-highlight
       'local-map (make-mode-line-mouse-map
                   'mouse-2 #'mode-line-widen))))
  "Mode line construct displaying narrowed state of the current buffer.")

(defun bandali-mode-line-window-control ()
  "Compute mode line construct for window dedicated state.
Value is used for `bandali-mode-line-window-dedicated', which see."
  (cond
   ((eq (window-dedicated-p) t)
    (propertize
     " D "
     'face 'bandali-mode-line-indicator-gray-bg
     'mouse-face 'mode-line-highlight))
   ((window-dedicated-p)
    (propertize
     " d "
     'face 'bandali-mode-line-indicator-gray-bg
     'mouse-face 'mode-line-highlight))
   (t "")))

(defvar-local bandali-mode-line-window-dedicated
  '(:eval (bandali-mode-line-window-control))
  "Mode line construct displaying window dedicated status.")

(defvar-local bandali-mode-line-remote
  '(:eval
    (when (file-remote-p default-directory)
      (propertize
       " @ "
       'face 'bandali-mode-line-indicator-red-bg
       'mouse-face 'mode-line-highlight)))
  "Mode line construct for indicating remote buffer.")

(defvar-local bandali-mode-line-input-method
  '(:eval
    (when current-input-method-title
      (propertize
       (format " %s " current-input-method-title)
       'face 'bandali-mode-line-indicator-green-bg
       'mouse-face 'mode-line-highlight)))
  "Mode line construct for indicating multilingual environment.")

;; MSDOS frames have window-system, but want the Fn identification.
(defun bandali-mode-line-frame-control ()
  "Compute mode line construct for frame identification.
Value is used for `bandali-mode-line-frame-identification', which see."
  (when (or (null window-system)
            (eq window-system 'pc))
      " %F  "))

(defvar-local bandali-mode-line-frame-identification
  '(:eval (bandali-mode-line-frame-control))
  "Mode line construct to describe the current frame.")

(defun bandali-mode-line-buffer-identification-face ()
  "Return appropriate face or face list for `bandali-mode-line-buffer-identification'."
  (cond
   ((and (mode-line-window-selected-p)
         (buffer-modified-p))
    '(italic mode-line-buffer-id))
   ((buffer-modified-p)
    'italic)
   ((mode-line-window-selected-p)
    'mode-line-buffer-id)))

(defun bandali-mode-line-buffer-name-help-echo ()
  "Return `help-echo' value for `bandali-mode-line-buffer-identification'."
  (concat
   (propertize (buffer-name) 'face 'mode-line-buffer-id)
   "\n"
   (propertize
    (or (buffer-file-name)
        default-directory)
    'face 'font-lock-doc-face)))

(defvar-local bandali-mode-line-buffer-identification
  '(:eval
    (let ((name (propertize
                 (buffer-name)
                 'face (bandali-mode-line-buffer-identification-face)
                 'mouse-face 'mode-line-highlight
                 'help-echo (bandali-mode-line-buffer-name-help-echo)))
          (read-only-icon (if (display-graphic-p) "" "%%")))
      (if buffer-read-only
          (format "%s %s" read-only-icon name)
        name)))
  "Mode line construct for identifying the buffer being displayed.
Propertize the current buffer with the `mode-line-buffer-id' face.
Let other buffers have no face.")

(defun bandali-mode-line-major-mode-help-echo ()
  "Return `help-echo' value for `bandali-mode-line-major-mode'."
  (if-let* ((parent (get major-mode 'derived-mode-parent)))
      (format "Symbol: `%s'\nDerived from: `%s'" major-mode parent)
    (format "Symbol: `%s'" major-mode)))

(defvar-local bandali-mode-line-major-mode
  (let ((recursive-edit-help-echo
         "Recursive edit, type C-M-c to get out"))
    (list (propertize
           "%["
           'face 'bandali-mode-line-indicator-red
           'help-echo recursive-edit-help-echo)
          `(:propertize ("" mode-name)
                        help-echo "Major mode\n\
mouse-1: Display major mode menu\n\
mouse-2: Show help for major mode\n\
mouse-3: Toggle minor modes"
                        mouse-face mode-line-highlight
                        local-map ,mode-line-major-mode-keymap)
          (propertize
           "%]"
           'face 'bandali-mode-line-indicator-red
           'help-echo recursive-edit-help-echo)))
  "Mode line construct for displaying major mode.")

(defvar-local bandali-mode-line-exwm-workspace
  '(:eval
    (when (and (mode-line-window-selected-p)
               (boundp 'exwm-workspace-current-index))
      (format
       "[%s]" (number-to-string exwm-workspace-current-index))))
  "Mode line construct displaying `exwm-workspace-current-index'.
Specific to the current window's mode line.")

(defvar-local bandali-mode-line-misc-info
  '(:eval
    (when (mode-line-window-selected-p)
      mode-line-misc-info))
  "Mode line construct displaying `mode-line-misc-info'.
Specific to the current window's mode line.")

(defvar-local bandali-mode-line-end-spaces
  '(:eval (if (display-graphic-p) " " "-%-"))
  "Mode line construct to put at the end of the mode line.")

Finally, we need to mark the above variables as risky-local-variable to have Emacs evaluate them.

(dolist (construct '(bandali-mode-line-kbd-macro
                     bandali-mode-line-narrow
                     bandali-mode-line-window-dedicated
                     bandali-mode-line-remote
                     bandali-mode-line-input-method
                     bandali-mode-line-frame-identification
                     bandali-mode-line-buffer-identification
                     bandali-mode-line-major-mode
                     bandali-mode-line-exwm-workspace
                     bandali-mode-line-misc-info
                     bandali-mode-line-end-spaces))
  (put construct 'risky-local-variable t))

3.5.4. bandali-mode-line.el setting the mode-line-format

(setq-default
 mode-line-format
 '("%e"
   mode-line-front-space
   bandali-mode-line-kbd-macro
   bandali-mode-line-narrow
   bandali-mode-line-window-dedicated
   bandali-mode-line-remote
   bandali-mode-line-input-method
   bandali-mode-line-frame-identification
   bandali-mode-line-buffer-identification
   "  "
   bandali-mode-line-major-mode
   mode-line-process
   " "
   (vc-mode vc-mode)

   mode-line-format-right-align
   bandali-mode-line-exwm-workspace
   " "
   bandali-mode-line-misc-info
   bandali-mode-line-end-spaces))

3.5.5. bandali-mode-line.el settings for keycast

Here I configure the keycast package helpful for showing key presses and their corresponding commands

(with-eval-after-load 'keycast
  (setopt
   keycast-mode-line-format "%2s%k%c%R"
   keycast-mode-line-window-predicate 'mode-line-window-selected-p
   keycast-mode-line-remove-tail-elements nil)

  (dolist (input '(self-insert-command
                   org-self-insert-command
                   isearch-printing-char))
    (add-to-list 'keycast-substitute-alist `(,input "." "Typing…")))

  (dolist (event '("<mouse-event>" "<mouse-movement>" "<mouse-2>"
                   "<drag-mouse-1>" "<wheel-up>" "<wheel-down>"
                   "<double-wheel-up>" "<double-wheel-down>"
                   "<triple-wheel-up>" "<triple-wheel-down>"
                   "<wheel-left>" "<wheel-right>"
                   handle-select-window mouse-set-point
                   mouse-drag-region))
    (add-to-list 'keycast-substitute-alist `(,event nil nil))))

3.5.6. bandali-mode-line.el call to provide

Finally, we provide the module. This is the mirror function of require.

(provide 'bandali-mode-line)
;;; bandali-mode-line.el ends here

3.6. bandali-completion.el

This module configures aspects of Emacs having to do with completion.

From the Emacs manual:

Completion is a feature that fills in the rest of a name starting from an abbreviation for it. Completion works by comparing the user’s input against a list of valid names and determining how much of the name is determined uniquely by what the user has typed. For example, when you type C-x b (switch-to-buffer) and then type the first few letters of the name of the buffer to which you wish to switch, and then type TAB (minibuffer-complete), Emacs extends the name as far as it can.

;;; bandali-completion.el --- completion configs  -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: convenience, matching, completion

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Code:

(require 'bandali-core)

3.6.1. bandali-completion.el settings to ignore letter casing

I find it far more convenient to have case-insensitive search and completion. Occasions that I may need case-sensitivity are the rare exception. During incremental search, case-sensitivity can be toggled using M-c.

(setq completion-ignore-case t)
(setopt read-buffer-completion-ignore-case t)
(with-eval-after-load 'minibuffer
  (setopt read-file-name-completion-ignore-case t))
(setq-default case-fold-search t)

3.6.2. bandali-completion.el do not allow cursor in minibuffer prompt

Don’t allow the cursor to move inside the minibuffer prompt, to avoid mistakes.

(setopt
 minibuffer-prompt-properties
 '(read-only t cursor-intangible t face minibuffer-prompt))
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)

3.6.3. bandali-completion.el settings for prompts that read multiple strings

Here we customize the prompts that read multiple strings in the minibuffer with completion (via completing-read-multiple), and also adds a simple prompt indicator for completing-read-multiple on older Emacsen.

(with-eval-after-load 'crm
  (setopt
   crm-prompt (format "%s %%p" (propertize "[%d]" 'face 'shadow)))

   ;; `completing-read-multiple' prompt indicator for older Emacsen
   ;; https://bugs.gnu.org/76028
   (when (< emacs-major-version 31)
     (advice-add
      #'completing-read-multiple :filter-args
      (lambda (args)
        (cons (format "[CRM%s] %s"
                      (string-replace "[ \t]*" "" crm-separator)
                      (car args))
              (cdr args))))))

3.6.4. bandali-completion.el settings for recursive minibuffers

Recursive minibuffers allow execution of commands that require minibuffer input even when a minibuffer is already open.

Enabling minibuffer-depth-indicate-mode shows a number next to the minibuffer prompt, indicating the level of depth in recursion, starting with 2.

(setopt enable-recursive-minibuffers t)
(run-with-idle-timer 0.5 nil #'require 'mb-depth)
(with-eval-after-load 'mb-depth
  (minibuffer-depth-indicate-mode 1))

3.6.5. bandali-completion.el settings for minibuffer default values

Minibuffer prompts can have a default value, to be selected when the user hits RET without typing anything. In such cases Emacs displays the default value in the prompt, formatted according to minibuffer-default-prompt-format.

Also, minibuffer-electric-default-mode can be used to display the default value only when hitting RET would result in the default value being used.

I currently don’t use either of the above customizations, so I leave the following code block as an example, which won’t be included in my configuration.

(with-eval-after-load 'minibuffer
  (setq minibuffer-default-prompt-format " [%s]"))
(minibuffer-electric-default-mode 1)

3.6.6. bandali-completion.el settings for vertico

(with-eval-after-load 'vertico
  (require 'prot-vertico)
  (setq vertico-multiform-commands
        `(("consult-\\(.*\\)?\\(find\\|grep\\|ripgrep\\)"
           ,@prot-vertico-multiform-maximal)))
  (setq vertico-multiform-categories
        `(;; Maximal
          (embark-keybinding ,@prot-vertico-multiform-maximal)
          (multi-category ,@prot-vertico-multiform-maximal)
          (consult-location ,@prot-vertico-multiform-maximal)
          (imenu ,@prot-vertico-multiform-maximal)
          (theme ,@prot-vertico-multiform-maximal)
          (unicode-name ,@prot-vertico-multiform-maximal)
          ;; Minimal
          (file
           ,@prot-vertico-multiform-minimal
           (vertico-sort-function . vertico-sort-directories-first))
          (t ,@prot-vertico-multiform-minimal)))

  (vertico-multiform-mode 1)

  (bandali-define-key vertico-map
    "C-M-n" #'vertico-next-group
    "C-M-p" #'vertico-previous-group
    "TAB" #'prot-vertico-private-complete
    "DEL" #'vertico-directory-delete-char
    "M-DEL" #'vertico-directory-delete-word)

  (bandali-define-key vertico-multiform-map
    "RET" #'prot-vertico-private-exit
    "C-n" #'prot-vertico-private-next
    "<down>" #'prot-vertico-private-next
    "C-p" #'prot-vertico-private-previous
    "<up>" #'prot-vertico-private-previous
    "C-l" #'vertico-multiform-vertical)

  (setopt
   vertico-cycle t
   vertico-scroll-margin 0)
  (require 'orderless)
  (setopt
   completion-styles
   '(orderless basic)
   completion-category-overrides
   '((file (styles basic partial-completion)))))
(when (fboundp #'vertico-mode)
  (add-hook 'after-init-hook #'vertico-mode))

3.6.7. bandali-completion.el settings for marginalia

(when (fboundp #'marginalia-mode)
  (add-hook 'after-init-hook #'marginalia-mode))

3.6.8. bandali-completion.el settings for corfu

(with-eval-after-load 'corfu
  (setopt
   corfu-min-width 20
   corfu-preview-current nil)
  (require 'corfu-popupinfo)
  (setopt corfu-popupinfo-delay '(1.25 . 0.5))
  (corfu-popupinfo-mode 1))
(when (fboundp #'global-corfu-mode)
  (add-hook 'after-init-hook #'global-corfu-mode))

3.6.9. bandali-completion.el settings for consult

(with-eval-after-load 'consult
  (setopt
   consult-find-args
   (concat "find . -not ( "
           "-path */.git* -prune "
           "-or -path */.cache* -prune )"))
  (add-to-list
   'consult-mode-histories
   '(vc-git-log-edit-mode . log-edit-comment-ring)))

(bandali-define-key global-map
  "M-g M-g" #'consult-goto-line
  "M-s M-b" #'consult-buffer
  "M-s M-f" #'consult-find
  "M-s M-g" #'consult-grep
  "M-s M-h" #'consult-history
  "M-s M-i" #'consult-imenu
  "M-s M-l" #'consult-line
  "M-s M-m" #'consult-mark
  "M-s M-y" #'consult-yank-pop
  "M-s M-o" #'consult-outline)

3.6.10. bandali-completion.el call to provide

Finally, we provide the module. This is the mirror function of require.

(provide 'bandali-completion)
;;; bandali-completion.el ends here

3.8. bandali-lang.el

;;; bandali-lang.el --- human or prog lang configs  -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: languages

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Code:

(require 'bandali-core)

(with-eval-after-load 'mule-cmds
  (setopt default-input-method "farsi-isiri-9147"))

;; (with-eval-after-load 'flyspell
;;   (setopt flyspell-mode-line-string " fly"))
(add-hook 'text-mode-hook #'flyspell-mode)
(add-hook 'tex-mode-hook #'flyspell-mode)

(with-eval-after-load 'files
  (add-to-list
   'auto-mode-alist '("\\(README.*\\|COMMIT_EDITMSG$\\)" . text-mode))
  (add-to-list 'auto-mode-alist '("\\.*rc$" . conf-mode))
  (add-to-list 'auto-mode-alist '("\\.bashrc$" . sh-mode)))

;; `elisp-mode'
;; (with-eval-after-load 'elisp-mode
;;   (setopt elisp-fontify-semantically t))
(bandali-define-key global-map "C-c e e" #'eval-last-sexp)

;; Display Lisp objects at point in the echo area.
(with-eval-after-load 'eldoc
  (setopt eldoc-minor-mode-string " eldoc")
  (global-eldoc-mode 1))

;; Highlight matching parens.
(run-with-idle-timer 0.2 nil #'require 'paren)
(with-eval-after-load 'paren
  (setopt
   show-paren-context-when-offscreen 'overlay
   ;; show-paren-style 'expression
   show-paren-when-point-in-periphery t
   show-paren-when-point-inside-paren t)
  (show-paren-mode 1))

(with-eval-after-load 'text-mode
  ;; Treat single-quote as punctuation
  (modify-syntax-entry ?' ".   " text-mode-syntax-table))
(when (version<= "31" emacs-version)
  (bandali-define-key global-map "C-c C" #'center-line-mode))

;; `pp'
(bandali-define-key global-map "C-c e m" #'pp-macroexpand-last-sexp)

;; `lisp-mode'
(add-hook
 'lisp-interaction-mode-hook
 (lambda () (setq-local indent-tabs-mode nil)))

(with-eval-after-load 'sgml-mode
  (setopt sgml-basic-offset 0))
(add-hook 'sgml-mode-hook (lambda () (electric-indent-local-mode -1)))

(with-eval-after-load 'css-mode
  (setopt css-indent-offset 2))

;; `reftex'
(add-hook 'latex-mode-hook #'reftex-mode)

;; `po-mode'
(defvar po-mode-map)
(declare-function po-mode "po-mode")
(declare-function View-exit "view")

(with-eval-after-load 'po-mode
  ;; Based on the `po-wrap' function from the GNUN manual:
  ;; https://www.gnu.org/s/trans-coord/manual/gnun/html_node/Wrapping-Long-Lines.html
  (defun bandali-po-wrap ()
    "Run the current `po-mode' buffer through `msgcat' to wrap all
lines."
    (interactive)
    (when (eq major-mode 'po-mode)
      (let ((tmp-file (make-temp-file "po-wrap."))
            (tmp-buffer (generate-new-buffer "*temp*")))
        (unwind-protect
            (progn
              (write-region (point-min) (point-max) tmp-file nil 1)
              (if (zerop
                   (call-process "msgcat" nil tmp-buffer t
                                 (shell-quote-argument tmp-file)))
                  (let ((saved (point))
                        (inhibit-read-only t))
                    (delete-region (point-min) (point-max))
                    (insert-buffer-substring tmp-buffer)
                    (goto-char (min saved (point-max))))
                (with-current-buffer tmp-buffer
                  (error (buffer-string)))))
          (kill-buffer tmp-buffer)
          (delete-file tmp-file)))))
  (declare-function bandali-po-wrap "bandali-lang")

  (add-hook
   'po-mode-hook (lambda () (run-with-timer 0.1 nil #'View-exit)))
  (bandali-define-key po-mode-map "M-q" #'bandali-po-wrap))

(autoload #'po-mode (locate-user-emacs-file "lisp/po-mode.el")
  "Major mode for translators when they edit PO files.

Special commands:
\\{po-mode-map}
Turning on PO mode calls the value of the variable 'po-mode-hook',
if that value is non-nil.  Behaviour may be adjusted through some variables,
all reachable through 'M-x customize', in group 'Emacs.Editing.I18n.Po'."
  t)
(add-to-list 'auto-mode-alist '("\\.po[tx]?\\'\\|\\.po\\." . po-mode))

(provide 'bandali-lang)
;;; bandali-lang.el ends here

3.9. bandali-dired.el

;;; bandali-dired.el --- bandali's dired setup -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: files

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; My dired setup and customizations.

;;; Code:

(require 'bandali-core)

(with-eval-after-load 'dired
  (setopt
   dired-dwim-target t
   ;; dired-listing-switches "-alh --group-directories-first"
   dired-listing-switches "-alh")

  (declare-function dired-dwim-target-directory "dired-aux")
  ;; easily diff 2 marked files
  ;; https://oremacs.com/2017/03/18/dired-ediff/
  (defun dired-ediff-files ()
    (interactive)
    (require 'dired-aux)
    (defvar ediff-after-quit-hook-internal)
    (let ((files (dired-get-marked-files))
          (wnd (current-window-configuration)))
      (if (<= (length files) 2)
          (let ((file1 (car files))
                (file2 (if (cdr files)
                           (cadr files)
                         (read-file-name
                          "file: "
                          (dired-dwim-target-directory)))))
            (if (file-newer-than-file-p file1 file2)
                (ediff-files file2 file1)
              (ediff-files file1 file2))
            (add-hook 'ediff-after-quit-hook-internal
                      (lambda ()
                        (setq ediff-after-quit-hook-internal nil)
                        (set-window-configuration wnd))))
        (error "no more than 2 files should be marked"))))

  ;; local key bindings
  (bandali-define-key dired-mode-map
    "b" #'dired-up-directory
    "E" #'dired-ediff-files
    "e" #'dired-toggle-read-only
    "\\" #'dired-hide-details-mode)

  (require 'dired-x)
  (setopt
   dired-guess-shell-alist-user
   '(("\\.pdf\\'"  "atril" "evince" "okular")
     ("\\.doc\\'"  "libreoffice")
     ("\\.docx\\'" "libreoffice")
     ("\\.ppt\\'"  "libreoffice")
     ("\\.pptx\\'" "libreoffice")
     ("\\.xls\\'"  "libreoffice")
     ("\\.xlsx\\'" "libreoffice")
     ("\\.flac\\'" "mpv"))))
(add-hook 'dired-mode-hook #'dired-hide-details-mode)
(add-hook 'dired-mode-hook #'hl-line-mode)

(provide 'bandali-dired)
;;; bandali-dired.el ends here

3.10. bandali-vc.el

;;; bandali-vc.el --- my VC-related configs  -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: convenience, tools, vc

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Code:

(require 'bandali-core)

(with-eval-after-load 'ediff
  (setopt
   ediff-window-setup-function #'ediff-setup-windows-plain
   ediff-split-window-function #'split-window-horizontally))

(with-eval-after-load 'project
  (setopt project-vc-merge-submodules nil))

(with-eval-after-load 'vc
  (setopt vc-allow-rewriting-published-history 'ask))
(bandali-define-key global-map "C-x v C-=" #'vc-ediff)

(with-eval-after-load 'vc-git
  (when (version< emacs-version "30")
    (setopt vc-git-print-log-follow t)))

(with-eval-after-load 'vc-hooks
  (setopt vc-use-incoming-outgoing-prefixes t))

(unless (fboundp #'global-diff-hl-mode)
  (autoload #'global-diff-hl-mode "diff-hl" nil t))
(declare-function global-diff-hl-mode "diff-hl")
(global-diff-hl-mode 1)

(provide 'bandali-vc)
;;; bandali-vc.el ends here

3.11. bandali-org

;;; bandali-org.el --- bandali's Org setup -*- lexical-binding: t; -*-

;; Copyright (c) 2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: calendar, data, docs, hypermedia, outlines

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; My set up for Org (org-mode) and all things Org.

;;; Code:

(require 'bandali-core)

(with-eval-after-load 'org
  (setopt
   org-src-content-indentation 0
   org-src-preserve-indentation t
   org-src-window-setup 'current-window))

(with-eval-after-load 'ox
  ;; (setopt org-export-timestamp-file nil)
  (require 'bandali-oxen))

(with-eval-after-load 'ox-ascii
  (setopt
   org-ascii-inner-margin 0
   org-ascii-text-width 70))

(with-eval-after-load 'ox-html
  (setopt
   org-html-doctype "xhtml5"
   org-html-html5-fancy t
   org-html-container-element "section"
   org-html-divs '((preamble  "header"  "preamble")
                   (content   "article" "content")
                   (postamble "footer"  "postamble"))))

(with-eval-after-load 'bandali-oxen
  (setopt
   ox-bhtml-default-site-title
   '(("en" "bandali")
     ("fa" "بندعلی"))
   ox-bhtml-home/up-format
   "<p><a accesskey=\"h\" href=\"%s\">&larr; home | خانه</a></p>"))

(provide 'bandali-org)
;;; bandali-org.el ends here

3.12. bandali-oxen

;;; bandali-oxen.el --- bandali's Org exporters -*- lexical-binding: t; -*-

;; Copyright (c) 2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: outlines, hypermedia, text

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; My custom Org exporters.

;;; Code:

(require 'ox)
(require 'ox-html)


;;; ox-bhtml


;;;; User options

(defcustom ox-bhtml-export-with-site-title t
  "Non-nil means print site title into the exported file.
This option can also be set with the OPTIONS keyword,
e.g. \"site-title:nil\"."
  :group 'org-export-html
  :type 'boolean
  :safe #'booleanp)

(defcustom ox-bhtml-default-site-title nil
  "Alist of languages and site titles for the HTML page title.
The first element of each list is the language code, as used for
the LANGUAGE keyword.  See `org-export-default-language'.

The second element of each list is the site title to use."
  :group 'org-export-html
  :type '(repeat
          (list (string :tag "Language")
                (string :tag "Site title"))))

(defcustom ox-bhtml-language-directions
  '(("fa" "rtl"))
  "Alist of languages and directions for HTML page direction.
The first element of each list is the language code, as used for
the LANGUAGE keyword.  See `org-export-default-language'.

The second element of each list is an HTML direction, one of `ltr',
`rtl', `auto'."
  :group 'org-export-html
  :type '(repeat
          (list (string :tag "Language")
                (string :tag "Direction"))))

(defcustom ox-bhtml-meta-tags #'ox-bhtml-meta-tags-default
  "Form that is used to produce meta tags in the HTML head.

Can be a list where each item is a list of arguments to be passed
to `org-html--build-meta-entry'.  Any nil items are ignored.

Also accept a function which gives such a list when called with a
single argument (INFO, a communication plist)."
  :group 'org-export-html
  :type '(choice
      (repeat
       (list (string :tag "Meta label")
         (string :tag "label value")
         (string :tag "Content value")))
      function))

(defcustom ox-bhtml-head-include-default-style nil
  "Non-nil means include the default style in exported HTML files.
Like `org-html-head-include-default-style' but for `ox-bhtml'."
  :group 'org-export-html
  :safe #'booleanp
  :type 'boolean)

(defcustom ox-bhtml-human-date-format "%-d %B %Y"
  "Format used for human-readable dates in preamble and postamble.
Conversion of the document `date' to the specified format will be
attempted using `format-time-string'.  See `format-time-string' for
more information on its components.

If conversion fails, one can manully add an `hdate' to the document
which will be used as-is when available."
  :group 'org-export-html
  :safe #'stringp
  :type 'string)

(defcustom ox-bhtml-metadata-timestamp-format "%Y-%m-%d %a %H:%M"
  "Format used for timestamps in preamble, postamble and metadata.
See `format-time-string' for more information on its components."
  :group 'org-export-html
  :type 'string)

(defcustom ox-bhtml-datetime-formats '("%F" . "%FT%T%z")
  "Formats used for the timestamp added as metadata to the time HTML element.
This only has an effect when `org-html-html5-fancy' is enabled, but
does not affect how the timestamp is displayed.  The format in CAR
represents the timestamp used for timestamps without a time component,
CDR the one for the full date and time.  Note that the HTML standard
restricts what timestamp formats are considered valid for the datetime
attribute.  See `format-time-string' for more information on its
components."
  :type '(cons string string)
  :group 'org-export-html
  :safe #'consp)

(defcustom ox-bhtml-preamble t
  "Non-nil means insert a preamble in HTML export.

When t, insert a string as defined by the formatting string in
`ox-bhtml-preamble-format'.  When set to a string, use this
formatting string instead (see `ox-bhtml-postamble-format' for an
example of such a formatting string).

When set to a function, apply this function and insert the
returned string.  The function takes the property list of export
options as its only argument.

Setting :html-preamble in publishing projects will take
precedence over this variable."
  :group 'org-export-html
  :type '(choice (const :tag "No preamble" nil)
                 (const :tag "Default preamble" t)
                 (string :tag "Custom formatting string")
                 (function :tag "Function (must return a string)")))

(defcustom ox-bhtml-postamble t
  "Non-nil means insert a postamble in HTML export.

When set to `auto', check against the
`org-export-with-author/email/creator/date' variables to set the
content of the postamble.  When t, insert a string as defined by the
formatting string in `ox-bhtml-postamble-format'.  When set to a
string, use this formatting string instead (see
`ox-bhtml-postamble-format' for an example of such a formatting
string).

When set to a function, apply this function and insert the
returned string.  The function takes the property list of export
options as its only argument.

Setting :html-postamble in publishing projects will take
precedence over this variable."
  :group 'org-export-html
  :type '(choice (const :tag "No postamble" nil)
                 (const :tag "Auto postamble" auto)
                 (const :tag "Default formatting string" t)
                 (string :tag "Custom formatting string")
                 (function :tag "Function (must return a string)")))

(defcustom ox-bhtml-preamble-format '(("en" ""))
  "Alist of languages and format strings for the HTML preamble.

The first element of each list is the language code, as used for
the LANGUAGE keyword.  See `org-export-default-language'.

The second element of each list is a format string to format the
preamble itself.  This format string can contain these elements,
as well as those from `org-html-format-spec':

  %D will be replaced by `hdate' (human-readable date).
  %i will be replaced by the input file name.
  %r will be replaced by `rights' (license/copyright information).

If you need to use a \"%\" character, you need to escape it
like that: \"%%\".

See the default value of `ox-bhtml-postamble-format' for an
example."
  :group 'org-export-html
  :type '(repeat
          (list (string :tag "Language")
                (string :tag "Format string"))))

(defcustom ox-bhtml-postamble-format
  '(("en" "<hr />
<p>%D -
<a href=\"%i\">Org source</a> -
%r</p>"))
  "Alist of languages and format strings for the HTML postamble.

The first element of each list is the language code, as used for
the LANGUAGE keyword.  See `org-export-default-language'.

The second element of each list is a format string to format the
postamble itself.  This format string can contain these elements,
as well as those from `org-html-format-spec':

  %D will be replaced by `hdate' (human-readable date).
  %i will be replaced by the input file name.
  %r will be replaced by `rights' (license/copyright information).

If you need to use a \"%\" character, you need to escape it
like that: \"%%\"."
  :group 'org-export-html
  :type '(repeat
          (list (string :tag "Language")
                (string :tag "Format string"))))

(defcustom ox-bhtml-home/up-format
  "<p><a accesskey=\"u\" href=\"%s\"> UP </a>
 |
 <a accesskey=\"h\" href=\"%s\"> HOME </a></p>"
  "Snippet used to insert the HOME and UP links.
This is a format string, the first %s will receive the UP link,
the second the HOME link.  If both `org-html-link-up' and
`org-html-link-home' are empty, the entire snippet will be
ignored."
  :group 'org-export-html
  :type 'string)


;;;; Template

(defun ox-bhtml-meta-tags-default (info)
  "A default value for `ox-bhtml-meta-tags'.

Generate a list items, each of which is a list of arguments that can
be passed to `org-html--build-meta-entry', to generate meta tags to be
included in the HTML head.

Use document's plist INFO to derive relevant information for the tags."
  (let ((author (and (plist-get info :with-author)
                     (let ((auth (plist-get info :author)))
                       ;; Return raw Org syntax.
                       (and auth (org-element-interpret-data auth))))))
    (list
     (list "name" "color-scheme" "light dark")
     (when (org-string-nw-p author)
       (list "name" "author" author))
     (when (org-string-nw-p (plist-get info :description))
       (list "name" "description"
             (plist-get info :description)))
     (when (org-string-nw-p (plist-get info :keywords))
       (list "name" "keywords" (plist-get info :keywords)))
     '("name" "generator" "GNU Emacs (Org mode)"))))

(defun ox-bhtml-format-spec (info)
  "Return format specification for preamble and postamble.
INFO is a plist used as a communication channel."
  (let* ((timestamp-format
          (plist-get info :html-metadata-timestamp-format))
         (date
          (org-export-data
           (org-export-get-date info timestamp-format)
           info))
         (datetime
          (condition-case nil
              (format-time-string
               (cdr ox-bhtml-datetime-formats) (date-to-time date))
            (error nil)))
         (hdate (plist-get info :hdate))
         (date-human
          (if (org-string-nw-p hdate)
              hdate
            (condition-case nil
                (format-time-string
                 ox-bhtml-human-date-format (date-to-time date))
              (error date)))))
    `(,@(org-html-format-spec info)
      (?D . ,(format
              "<time%s>%s</time>"
              (if datetime
                  (format
                   " datetime=\"%s\""
                   datetime)
                "")
              date-human))
      (?i . ,(file-name-nondirectory
              (plist-get info :input-file)))
      (?r . ,(org-export-data (plist-get info :rights) info)))))


;;;; Internal functions

(defun ox-bhtml--build-meta-info (info)
  "Return meta tags for exported document.
INFO is a plist used as a communication channel."
  (let* ((lang (plist-get info :language))
         (site-title
          (org-html-plain-text
           (org-element-interpret-data (plist-get info :site-title)) info))
         (site-title
          (if (org-string-nw-p site-title)
              site-title
            (or
             (cadr
              (assoc-string
               lang (plist-get info :default-site-title) t))
             "")))
         (title
          (org-html-plain-text
           (org-element-interpret-data (plist-get info :title)) info))
         ;; Set title to an invisible character instead of leaving it
         ;; empty, which is invalid.
         (title (if (org-string-nw-p title) title "&lrm;"))
         (charset (or (and org-html-coding-system
                           (symbol-name
                            (coding-system-get org-html-coding-system
                                               'mime-charset)))
                      "iso-8859-1")))
    (concat
     (when (plist-get info :time-stamp-file)
       (format-time-string
        (concat "<!-- "
                (plist-get info :html-metadata-timestamp-format)
                " -->\n")))

     (if (org-html-html5-p info)
         (org-html--build-meta-entry "charset" charset)
       (org-html--build-meta-entry
        "http-equiv" "Content-Type"
        (concat "text/html;charset=" charset)))

     (let ((viewport-options
            (cl-remove-if-not
             (lambda (cell) (org-string-nw-p (cadr cell)))
             (plist-get info :html-viewport))))
       (if viewport-options
           (org-html--build-meta-entry
            "name" "viewport"
            (mapconcat
             (lambda (elm)
               (format "%s=%s" (car elm) (cadr elm)))
             viewport-options ", "))))

     (format
      "<title>%s</title>\n"
      (if (and
           (plist-get info :with-site-title)
           (org-string-nw-p site-title))
          (format "%s - %s" title site-title)
        title))

     (mapconcat
      (lambda (args) (apply #'org-html--build-meta-entry args))
      (delq nil (if (functionp ox-bhtml-meta-tags)
                    (funcall ox-bhtml-meta-tags info)
                  ox-bhtml-meta-tags))
      ""))))

(defun ox-bhtml--build-pre/postamble (type info)
  "Return document preamble or postamble as a string, or nil.
TYPE is either `preamble' or `postamble', INFO is a plist used as a
communication channel."
  (let ((section (plist-get info (intern (format ":html-%s" type))))
        (spec (ox-bhtml-format-spec info)))
    (when section
      (let ((section-contents
             (if (functionp section) (funcall section info)
               (cond
                ((stringp section) (format-spec section spec))
                ((and (eq section 'auto) (eq type 'postamble))
                 (let ((date (cdr (assq ?d spec)))
                       (author (cdr (assq ?a spec)))
                       (email (cdr (assq ?e spec)))
                       (creator (cdr (assq ?c spec)))
                       (validation-link (cdr (assq ?v spec))))
                   (concat
                    (and (plist-get info :with-date)
                         (org-string-nw-p date)
                         (format "<p class=\"date\">%s: %s</p>\n"
                                 (org-html--translate "Date" info)
                                 date))
                    (and (plist-get info :with-author)
                         (org-string-nw-p author)
                         (format "<p class=\"author\">%s: %s</p>\n"
                                 (org-html--translate "Author" info)
                                 author))
                    (and (plist-get info :with-email)
                         (org-string-nw-p email)
                         (format "<p class=\"email\">%s: %s</p>\n"
                                 (org-html--translate "Email" info)
                                 email))
                    (and (plist-get info :time-stamp-file)
                         (format
                          "<p class=\"date\">%s: %s</p>\n"
                          (org-html--translate "Created" info)
                          (format-time-string
                           (plist-get info :html-metadata-timestamp-format))))
                    (and (plist-get info :with-creator)
                         (org-string-nw-p creator)
                         (format "<p class=\"creator\">%s</p>\n" creator))
                    (and (org-string-nw-p validation-link)
                         (format "<p class=\"validation\">%s</p>\n"
                                 validation-link)))))
                (t
                 (let ((formats (plist-get info (if (eq type 'preamble)
                                                    :html-preamble-format
                                                  :html-postamble-format)))
                       (language (plist-get info :language)))
                   (format-spec
                    (cadr (or (assoc-string language formats t)
                              (assoc-string "en" formats t)))
                    spec)))))))
        (let ((div (assq type (plist-get info :html-divs))))
          (when (org-string-nw-p section-contents)
            (concat
             (format "<%s id=\"%s\" class=\"%s\">\n"
                     (nth 1 div)
                     (nth 2 div)
                     org-html--pre/postamble-class)
             (org-element-normalize-string section-contents)
             (format "</%s>\n" (nth 1 div)))))))))


;;;; Transcode functions

(defun ox-bhtml-template (contents info)
  "Return complete document string after HTML conversion.
CONTENTS is the transcoded contents string.  INFO is a plist
holding export options."
  (concat
   (when (and (not (org-html-html5-p info)) (org-html-xhtml-p info))
     (let* ((xml-declaration (plist-get info :html-xml-declaration))
            (decl (or (and (stringp xml-declaration) xml-declaration)
                      (cdr (assoc (plist-get info :html-extension)
                                  xml-declaration))
                      (cdr (assoc "html" xml-declaration))
                      "")))
       (when (not (or (not decl) (string= "" decl)))
         (format
          "%s\n"
          (format decl
                  (or (and org-html-coding-system
                           (coding-system-get org-html-coding-system :mime-charset))
                      "iso-8859-1"))))))
   (org-html-doctype info)
   "\n"
   (let* ((lang (plist-get info :language))
          (dirp (plist-get info :direction))
          (dirp (if (org-string-nw-p dirp)
                    dirp
                  (cadr
                   (assoc-string
                    lang (plist-get info :language-directions) t))))
          (dir (if dirp (format " dir=\"%s\"" dirp) "")))
     (concat
      "<html"
      (cond
       ((org-html-xhtml-p info)
        (concat
         (format
          " xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\""
          lang lang)
         dir))
       ((org-html-html5-p info)
        (concat
         (format " lang=\"%s\"" lang)
         dir)))
      ">\n"))
    "<head>\n"
    (ox-bhtml--build-meta-info info)
    (org-html--build-head info)
    (org-html--build-mathjax-config info)
    "</head>\n"
    "<body>\n"
    ;; Preamble.
    (ox-bhtml--build-pre/postamble 'preamble info)
    ;; Document contents.
    (let ((div (assq 'content (plist-get info :html-divs))))
      (format "<%s id=\"%s\" class=\"%s\">\n"
              (nth 1 div)
              (nth 2 div)
              (plist-get info :html-content-class)))
    ;; Document title.
    (when (plist-get info :with-title)
      (let ((title (and (plist-get info :with-title)
                        (plist-get info :title)))
            (subtitle (plist-get info :subtitle))
            (html5-fancy (org-html--html5-fancy-p info)))
        (when title
          (format
           (if html5-fancy
               "<header>\n<h1 class=\"title\">%s</h1>\n%s</header>"
             "<h1 class=\"title\">%s%s</h1>\n")
           (org-export-data title info)
           (if subtitle
               (format
                (if html5-fancy
                    "<p class=\"subtitle\" role=\"doc-subtitle\">%s</p>\n"
                  (concat "\n" (org-html-close-tag "br" nil info) "\n"
                          "<span class=\"subtitle\">%s</span>\n"))
                (org-export-data subtitle info))
             "")))))
    contents
    (format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs))))
    ;; Postamble.
    (ox-bhtml--build-pre/postamble 'postamble info)
    (let ((link-up (org-trim (plist-get info :html-link-up)))
          (link-home (org-trim (plist-get info :html-link-home))))
      (unless (and (string= link-up "") (string= link-home ""))
        (format (plist-get info :html-home/up-format)
                (or (org-string-nw-p link-up) link-home)
                (or (org-string-nw-p link-home) link-up))))
    ;; Possibly use the Klipse library live code blocks.
    (when (plist-get info :html-klipsify-src)
      (concat "<script>" (plist-get info :html-klipse-selection-script)
              "</script><script src=\""
              org-html-klipse-js
              "\"></script><link rel=\"stylesheet\" type=\"text/css\" href=\""
              org-html-klipse-css "\"/>"))
    ;; Closing document.
    "</body>\n</html>"))


;;;; Export functions and backend definition

;;;###autoload
(defun ox-bhtml-export-as-html
    (&optional async subtreep visible-only body-only ext-plist)
  "Export current buffer to an HTML (bandali) buffer.

If narrowing is active in the current buffer, only export its
narrowed part.

If a region is active, export that region.

A non-nil optional argument ASYNC means the process should happen
asynchronously.  The resulting buffer should be accessible
through the `org-export-stack' interface.

When optional argument SUBTREEP is non-nil, export the sub-tree
at point, extracting information from the headline properties
first.

When optional argument VISIBLE-ONLY is non-nil, don't export
contents of hidden elements.

When optional argument BODY-ONLY is non-nil, only write code
between \"<body>\" and \"</body>\" tags.

EXT-PLIST, when provided, is a property list with external
parameters overriding Org default settings, but still inferior to
file-local settings.

Export is done in a buffer named \"*Org BHTML Export*\", which
will be displayed when `org-export-show-temporary-export-buffer'
is non-nil."
  (interactive)
  (org-export-to-buffer 'bhtml "*Org BHTML Export*"
    async subtreep visible-only body-only ext-plist
    (lambda () (set-auto-mode t))))

;;;###autoload
(defun ox-bhtml-export-to-html
    (&optional async subtreep visible-only body-only ext-plist)
  "Export current buffer to an HTML (bandali) file.

If narrowing is active in the current buffer, only export its
narrowed part.

If a region is active, export that region.

A non-nil optional argument ASYNC means the process should happen
asynchronously.  The resulting file should be accessible through
the `org-export-stack' interface.

When optional argument SUBTREEP is non-nil, export the sub-tree
at point, extracting information from the headline properties
first.

When optional argument VISIBLE-ONLY is non-nil, don't export
contents of hidden elements.

When optional argument BODY-ONLY is non-nil, only write code
between \"<body>\" and \"</body>\" tags.

EXT-PLIST, when provided, is a property list with external
parameters overriding Org default settings, but still inferior to
file-local settings.

Return output file's name."
  (interactive)
  (let* ((extension (concat
                     (when (> (length org-html-extension) 0) ".")
                     (or (plist-get ext-plist :html-extension)
                         org-html-extension
                         "html")))
         (file (org-export-output-file-name extension subtreep))
         (org-export-coding-system org-html-coding-system))
    (org-export-to-file 'bhtml file
      async subtreep visible-only body-only ext-plist)))

(org-export-define-derived-backend 'bhtml 'html
  :menu-entry
  '(?h 1
       ((?B "As HTML buffer (bandali)" ox-bhtml-export-as-html)
        (?b "As HTML file (bandali)" ox-bhtml-export-to-html)))
  :options-alist
  '((:hdate "HDATE" nil nil parse)
    (:rights "RIGHTS" nil nil parse)
    (:direction "DIRECTION" nil nil t)
    (:site-title "SITE_TITLE" nil nil parse)
    (:with-site-title
     nil "site-title" ox-bhtml-export-with-site-title)
    (:html-preamble nil "html-preamble" ox-bhtml-preamble)
    (:html-postamble nil "html-postamble" ox-bhtml-postamble)
    (:html-head-include-default-style
     nil "html-style" ox-bhtml-head-include-default-style)
    (:html-home/up-format nil nil ox-bhtml-home/up-format)
    (:default-site-title nil nil ox-bhtml-default-site-title)
    (:language-directions nil nil ox-bhtml-language-directions)
    (:html-metadata-timestamp-format
     nil nil ox-bhtml-metadata-timestamp-format)
    (:html-preamble-format nil nil ox-bhtml-preamble-format)
    (:html-postamble-format nil nil ox-bhtml-postamble-format))
  :translate-alist
  '((template . ox-bhtml-template)))

(provide 'bandali-oxen)
;;; bandali-oxen.el ends here

3.13. bandali-gnus.el

;;; bandali-gnus.el --- bandali's Gnus setup -*- lexical-binding: t; -*-

;; Copyright (C) 2018-2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: mail, news

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; My trusty email setup with Gnus and Message.

;;; Code:

(require 'bandali-core)
(require 'bandali-simple)

;; (defvar bandali-maildir
;;   (expand-file-name (convert-standard-filename "~/mail/")))

(eval-when-compile
  (progn
    (defvar nndraft-directory)
    (defvar gnus-read-newsrc-file)
    (defvar gnus-save-newsrc-file)
    (defvar gnus-gcc-mark-as-read)
    (defvar nnmail-split-abbrev-alist)))

(declare-function article-make-date-line "gnus-art" (date type))

(with-eval-after-load 'gnus
  (setopt
   gnus-select-method '(nnnil "")
   gnus-secondary-select-methods
   `((nnimap
      "kelar"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "bandali@kelar.local")
      ;; (nnmail-expiry-wait immediate)
      (nnmail-expiry-target nnmail-fancy-expiry-target)
      (nnmail-fancy-expiry-targets
       (("from" ".*" "nnimap+kelar:Archive.%Y"))))
     (nnimap
      "ia"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "bandali@archive.local")
      (nnmail-expiry-wait immediate)
      (nnmail-expiry-target nnmail-fancy-expiry-target)
      (nnmail-fancy-expiry-targets
       (("from" ".*" "nnimap+ia:Archive.%Y"))))
     (nnimap
      "shemshak"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "bandali@shemshak.local"))
     (nnimap
      "debian"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "bandali@debian.local")
      ;; (nnmail-expiry-wait immediate)
      (nnmail-expiry-target nnmail-fancy-expiry-target)
      (nnmail-fancy-expiry-targets
       (("from" ".*" "nnimap+debian:Archive.%Y"))))
     (nnimap
      "gnu"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "bandali@gnu.local")
      (nnimap-inbox "INBOX")
      (nnimap-split-methods 'nnimap-split-fancy)
      (nnimap-split-fancy
       (|
        ;; (: gnus-registry-split-fancy-with-parent)
        ;; (: gnus-group-split-fancy "INBOX" t "INBOX")
        ;; spam
        ("X-Spam_action" "reject" "Junk")
        ;; keep debbugs emails in INBOX
        (list ".*<\\(.*\\)\\.debbugs\\.gnu\\.org>.*" "INBOX")
        ;; list moderation emails
        (from ".+-\\(owner\\|bounces\\)@\\(non\\)?gnu\\.org" "listmod")
        ;; gnu
        (list ".*<\\(.*\\)\\.\\(non\\)?gnu\\.org>.*" "l.\\1")
        ("Envelope-To" "emacsconf-donations@gnu.org" "l.emacsconf-donations")
        ;; board-eval
        (|
         (list ".*<.*\\.board-eval\\.fsf\\.org>.*" "l.board-eval")
         (from ".*@board-eval\\.fsf\\.org" "l.board-eval"))
        ;; fsf
        (list ".*<\\(.*\\)\\.fsf\\.org>.*" "l.\\1")
        ;; cfarm
        (from "cfarm-.*@lists\\.tetaneutral\\.net" "l.cfarm")
        ;; debian
        (list ".*<\\(.*\\)\\.\\(lists\\|other\\)\\.debian\\.org>.*" "l.\\1")
        (list ".*<\\(.*\\)\\.alioth-lists\\.debian\\.net>.*" "l.\\1")
        ;; gnus
        (list ".*<\\(.*\\)\\.gnus\\.org>.*" "l.\\1")
        ;; libreplanet
        (list ".*<\\(.*\\)\\.libreplanet\\.org>.*" "l.\\1")
        ;; iana (e.g. tz-announce)
        (list ".*<\\(.*\\)\\.iana\\.org>.*" "l.\\1")
        ;; mailop
        (list ".*<\\(.*\\)\\.mailop\\.org>.*" "l.\\1")
        ;; sdlu
        (list ".*<\\(.*\\)\\.spammers\\.dontlike\\.us>.*" "l.sdlu")
        ;; bitfolk
        (from ".*@\\(.+\\)?bitfolk\\.com>.*" "bitfolk")
        ;; haskell
        (list ".*<\\(.*\\)\\.haskell\\.org>.*" "l.\\1")
        ;; webmasters
        (from "webmasters\\(-comment\\)?@gnu\\.org" "webmasters")
        ;; other
        (list ".*atreus.freelists.org" "l.atreus")
        (list ".*deepspec.lists.cs.princeton.edu" "l.deepspec")
        (list ".*haskell-art.we.lurk.org" "l.haskell-art")
        (list ".*dev.lists.parabola.nu" "l.parabola-dev")
        ;; otherwise, leave mail in INBOX
        "INBOX")))
     (nnimap
      "csc"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "abandali@csclub.uwaterloo.local")
      (nnimap-inbox "INBOX")
      (nnimap-split-methods 'nnimap-split-fancy)
      (nnimap-split-fancy
       (|
        ;; cron reports and other messages from root
        (from "root@\\(.*\\.\\)?csclub\\.uwaterloo\\.ca" "INBOX")
        ;; spam
        ("X-Spam-Flag" "YES" "Junk")
        ;; catch-all
        "INBOX"))))
   gnus-message-archive-group "nnimap+kelar:INBOX"
   gnus-parameters
   '(("l\\.fencepost-users"
      (to-address . "fencepost-users@gnu.org")
      (to-list    . "fencepost-users@gnu.org")
      (list-identifier . "\\[Fencepost-users\\]"))
     ("l\\.haskell-cafe"
      (to-address . "haskell-cafe@haskell.org")
      (to-list    . "haskell-cafe@haskell.org")
      (list-identifier . "\\[Haskell-cafe\\]")))
   ;; (gnus-large-newsgroup  50)
   gnus-process-mark-toggle t
   gnus-home-directory (locate-user-emacs-file "gnus/")
   gnus-directory
   (expand-file-name
    (convert-standard-filename "news/") gnus-home-directory)
   gnus-interactive-exit nil
   gnus-user-agent '(emacs gnus type))

  (with-eval-after-load 'message
    (setopt
     message-directory
     (expand-file-name
      (convert-standard-filename "mail/") gnus-home-directory)))

  (with-eval-after-load 'nndraft
    (setopt
     nndraft-directory
     (expand-file-name
      (convert-standard-filename "drafts/") gnus-home-directory)))

  (with-eval-after-load 'nnimap
    (setq nnimap-record-commands init-file-debug))

  (with-eval-after-load 'gnus-agent
    (setopt gnus-agent-synchronize-flags 'ask))

  (with-eval-after-load 'gnus-art       ; article
    (setopt
     gnus-buttonized-mime-types
     '("multipart/\\(signed\\|encrypted\\)")
     gnus-sorted-header-list
     '("^From:"
       "^X-RT-Originator"
       "^Newsgroups:"
       "^Subject:"
       "^Date:"
       "^Envelope-To:"
       "^Followup-To:"
       "^Reply-To:"
       "^Organization:"
       "^Summary:"
       "^Abstract:"
       "^Keywords:"
       "^To:"
       "^[BGF]?Cc:"
       "^Posted-To:"
       "^Mail-Copies-To:"
       "^Mail-Followup-To:"
       "^Apparently-To:"
       "^Resent-From:"
       "^User-Agent:"
       "^X-detected-operating-system:"
       "^X-Spam_action:"
       "^X-Spam_bar:"
       "^Message-ID:"
       ;; "^References:"
       "^List-Id:"
       "^Gnus-Warning:")
     gnus-visible-headers
     (mapconcat #'identity gnus-sorted-header-list "\\|")))

  ;; `gnus-dired'
  (with-eval-after-load 'dired
    (require 'gnus-dired)
    (add-hook 'dired-mode-hook #'gnus-dired-mode))

  (with-eval-after-load 'gnus-group
    (setopt
     gnus-permanently-visible-groups "\\(:INBOX$\\|:gnu$\\)")
    (add-hook 'gnus-group-mode-hook #'gnus-topic-mode)
    (add-hook 'gnus-group-mode-hook #'gnus-agent-mode))

  (with-eval-after-load 'gnus-msg
    (let ((bandali "Amin Bandali%s - https://kelar.org/~bandali"))
      (defvar bandali-csc-signature
        (mapconcat
         #'identity
         `(,(format bandali ", MMath")
           "Systems Committee <syscom@csclub.uwaterloo.ca>"
           "Computer Science Club of the University of Waterloo")
         "\n")))
    (setopt
     gnus-gcc-mark-as-read t
     gnus-message-replysign t
     gnus-posting-styles
     '(("nnimap\\+kelar:.*"
        (address "bandali@kelar.org")
        ("X-Message-SMTP-Method" "smtp mail.kelar.org 587")
        (gcc "nnimap+kelar:INBOX"))
       ("nnimap\\+ia:.*"
        (address "bandali@archive.org")
        ("X-Message-SMTP-Method" "smtp smtp.gmail.com 587")
        (gcc "nnimap+ia:INBOX"))
       ("nnimap\\+shemshak:.*"
        (address "amin@shemshak.org")
        ("X-Message-SMTP-Method" "smtp mail.shemshak.org 587")
        (gcc "nnimap+shemshak:Sent"))
       ("nnimap\\+debian:.*"
        (address "bandali@debian.org")
        ("X-Message-SMTP-Method" "smtp mail-submit.debian.org 587")
        (gcc "nnimap+debian:INBOX"))
       ("nnimap\\+gnu:.*"
        (address "bandali@gnu.org")
        ("X-Message-SMTP-Method" "smtp fencepost.gnu.org 587")
        (gcc "nnimap+gnu:INBOX"))
       ("nnimap\\+.*:l\\.ubuntu-.*"
        (address "bandali@ubuntu.com")
        ("X-Message-SMTP-Method" "smtp mail.kelar.org 587"))
       ((header "list-id" ".*\\.lists.ubuntu.com")
        (address "bandali@ubuntu.com")
        ("X-Message-SMTP-Method" "smtp mail.kelar.org 587"))
       ("nnimap\\+csc:.*"
        (address "bandali@csclub.uwaterloo.ca")
        ("X-Message-SMTP-Method" "smtp mail.csclub.uwaterloo.ca 587")
        (signature bandali-csc-signature)
        (gcc "nnimap+csc:Sent"))))

    ;; `gnus-registry'
    ;; (setopt
    ;;  gnus-registry-max-entries 2500
    ;;  gnus-registry-ignored-groups
    ;;  (append gnus-registry-ignored-groups
    ;;          '(("^nnimap:gnu\\.l" t) ("webmasters$" t))))
    ;; (require 'gnus-registry)
    ;; (gnus-registry-initialize)

    (with-eval-after-load 'gnus-search
      (setopt
       gnus-search-use-parsed-queries t))

    (with-eval-after-load 'gnus-start
      (setopt
       gnus-save-newsrc-file nil
       gnus-read-newsrc-file nil)
      (unless (fboundp 'gnus-notifications)
        (autoload #'gnus-notifications "gnus-start" nil t))
      (add-hook
       'gnus-after-getting-new-news-hook #'gnus-notifications))

    (with-eval-after-load 'gnus-sum       ; summary
      (setopt
       gnus-thread-sort-functions
       '(gnus-thread-sort-by-number
         gnus-thread-sort-by-subject
         gnus-thread-sort-by-date))

      (with-eval-after-load 'message
        (setopt
         gnus-ignored-from-addresses message-dont-reply-to-names))

      (defun bandali-gnus-junk-article (&optional n)
        (interactive "P" gnus-summary-mode)
        (gnus-summary-move-article
         n
         (gnus-group-prefixed-name
          "Junk"
          (gnus-find-method-for-group gnus-newsgroup-name))))

      (defvar bandali-gnus-summary-prefix-map)
      (define-prefix-command 'bandali-gnus-summary-prefix-map)
      (bandali-define-key gnus-summary-mode-map
        "v" 'bandali-gnus-summary-prefix-map)
      (bandali-define-key bandali-gnus-summary-prefix-map
        "r r" #'gnus-summary-very-wide-reply
        "r q" #'gnus-summary-very-wide-reply-with-original
        "R r" #'gnus-summary-reply
        "R q" #'gnus-summary-reply-with-original
        "r a w" #'gnus-summary-show-raw-article
        "s" #'bandali-gnus-junk-article))

    (with-eval-after-load 'gnus-topic
      (setopt
       ;; gnus-topic-line-format "%i[ %A: %(%{%n%}%) ]%v\n"
       gnus-topic-line-format "%i[ %(%{%n%}%) (%A) ]%v\n")
      (setq
       gnus-topic-topology
       `(("Gnus" visible nil nil)
         (("misc" visible nil nil))
         (("csc" visible nil nil))
         (("ia" visible nil nil))
         (("kelar" visible nil nil))
         (("shemshak" visible nil nil))
         (("debian" visible nil nil))
         (("gnu" visible nil nil))
         ;; (("old-gnu" visible nil nil))
         )))

    ;; (with-eval-after-load 'gnus-win
    ;;   (setopt gnus-use-full-window nil))

    (with-eval-after-load 'mm-archive
      (add-to-list
       'mm-archive-decoders
       '("application/gzip" nil "gunzip" "-S" ".zip" "-kd" "%f" "-r")))

    (with-eval-after-load 'mm-decode
      (setopt
       ;; mm-attachment-override-types `("text/x-diff" "text/x-patch"
       ;;                                ,@mm-attachment-override-types)
       mm-discouraged-alternatives '("text/html" "text/richtext")
       mm-decrypt-option 'known
       mm-verify-option 'known)
      (add-to-list
       'mm-inline-media-tests
       `("application/gzip" mm-archive-dissect-and-inline identity))
      (add-to-list 'mm-inlined-types "application/gzip" 'append))

    (with-eval-after-load 'mm-uu
      (when (version< "27" emacs-version)
        (set-face-attribute 'mm-uu-extract nil :extend t)))

    (with-eval-after-load 'mml
      (setopt
       mml-attach-file-at-the-end t
       mml-content-disposition-alist
       '((text
          (markdown . "attachment")
          (rtf . "attachment")
          (t . "inline"))
         (t . "attachment"))))

    (with-eval-after-load 'mml-sec
      (setopt
       mml-secure-openpgp-encrypt-to-self t
       mml-secure-openpgp-sign-with-sender t))))
(bandali-define-key global-map
  "C-c g" #'gnus-plugged
  "C-c G" #'gnus-unplugged
  "C-c m" (lambda () (interactive) (async-shell-command "m")))
(with-eval-after-load 'message
  ;; Redefine for a simplified In-Reply-To header
  ;; (https://todo.sr.ht/~sircmpwn/lists.sr.ht/67)
  (defun message-make-in-reply-to ()
    "Return the In-Reply-To header for this message."
    (when message-reply-headers
      (let ((from (mail-header-from message-reply-headers))
            (msg-id (mail-header-id message-reply-headers)))
        (when from
          msg-id))))

  (setopt
   message-elide-ellipsis "> [... %l lines elided]\n"
   message-citation-line-format "%N wrote:\n"
   message-citation-line-function
   #'message-insert-formatted-citation-line
   message-confirm-send t
   message-fill-column 70
   message-forward-as-mime t
   ;; message-kill-buffer-on-exit t
   message-send-mail-function #'smtpmail-send-it
   message-subscribed-address-functions
   '(gnus-find-subscribed-addresses)
   message-dont-reply-to-names
   (mapconcat
    #'identity
    '("bandali@kelar\\.org"
      "amin@shemshak\\.org"
      "\\(bandali\\|mab\\|aminb?\\)@gnu\\.org"
      "a?bandali@\\(csclub\\.\\)?uwaterloo\\.ca"
      "bandali@gnu\\.ca"
      "bandali@ubuntu\\.com"
      "bandali@debian\\.org"
      "bandali@archive\\.org")
    "\\|"))

  (defun bandali-newlines-or-asterism (arg)
    "Create newlines per my liking, or insert asterism if ARG is
non-nil."
    (interactive "P")
    (if arg
        (bandali-insert-asterism)
      (progn
        (delete-region (line-beginning-position) (line-end-position))
        (newline)
        (open-line 1))))
  (bandali-define-key message-mode-map
    "M-RET" #'bandali-newlines-or-asterism
    "C-c C-s" nil
    ;; breaks C-S-n selection
    ;; "<remap> <next-line>" #'mail-abbrev-next-line
    "<remap> <end-of-buffer>" #'mail-abbrev-end-of-buffer))
(add-hook 'message-mode-hook #'flyspell-mode)
;; (with-eval-after-load 'sendmail
;;   (setopt mail-header-separator ""))
;; (with-eval-after-load 'smtpmail
;;   (setopt smtpmail-queue-mail t
;;           smtpmail-queue-dir (concat bandali-maildir "queue/")))
(declare-function debbugs-gnu "debbugs-gnu")
(declare-function debbugs-gnu-bugs "debbugs-gnu")
(bandali-define-key global-map
  "C-c D d" #'debbugs-gnu
  "C-c D b" #'debbugs-gnu-bugs
  "C-c D e"          ; bug-gnu-emacs
  (lambda ()
    (interactive)
    (setq debbugs-gnu-current-suppress t)
    (debbugs-gnu debbugs-gnu-default-severities
                 '("emacs")))
  "C-c D g"          ; bug-gnuzilla
  (lambda ()
    (interactive)
    (setq debbugs-gnu-current-suppress t)
    (debbugs-gnu debbugs-gnu-default-severities
                 '("gnuzilla"))))
(provide 'bandali-gnus)
;;; bandali-gnus.el ends here

3.14. bandali-erc.el

;;; bandali-erc.el --- bandali's ERC setup -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: IRC, chat, client, Internet

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; My ERC setup for IRC in GNU Emacs.

;;; Code:

(require 'bandali-core)

(with-eval-after-load 'erc
  (setopt
   erc-auto-query 'bury
   erc-autojoin-domain-only nil
   erc-dcc-get-default-directory (locate-user-emacs-file "erc-dcc")
   erc-email-userid "bandali"
   ;; erc-lurker-hide-list '("JOIN" "PART" "QUIT")
   erc-nick "bandali"
   erc-prompt "erc>"
   erc-prompt-for-password nil
   erc-query-display 'buffer
   ;; erc-server-reconnect-attempts 5
   erc-server-reconnect-timeout 3
   erc-show-speaker-membership-status t)

  (add-to-list 'erc-modules 'keep-place)
  (when (display-graphic-p)
    (add-to-list 'erc-modules 'notifications)
    (add-to-list 'erc-modules 'smiley))
  (add-to-list 'erc-modules 'spelling)

  (declare-function erc-update-modules "erc")
  (erc-update-modules)

  (with-eval-after-load 'erc-match
    (setopt
     erc-pal-highlight-type 'nick
     erc-pals
     '("corwin" "sachac" "^rwp" "^iank" "thomzane"
       "neverwas" "^gopar" "technomancy"))
    (set-face-attribute
     'erc-pal-face nil
     :foreground 'unspecified
     :weight 'unspecified
     :inherit 'erc-nick-default-face
     :background "#ffffdf"))

  (with-eval-after-load 'erc-pcomplete
    (setopt erc-pcomplete-nick-postfix ","))

  (with-eval-after-load 'erc-stamp
    (setopt
     erc-timestamp-only-if-changed-flag nil
     erc-timestamp-format "%T "
     erc-insert-timestamp-function #'erc-insert-timestamp-left)
    (set-face-attribute
     'erc-timestamp-face nil
     :foreground "#aaaaaa"
     :weight 'unspecified
     :background 'unspecified))

  (with-eval-after-load 'erc-track
    (setopt
     erc-track-enable-keybindings nil
     erc-track-exclude-types
     '("JOIN" "MODE" "NICK" "PART" "QUIT"
       "324" "329" "332" "333" "353" "477")
     erc-track-position-in-mode-line t
     erc-track-priority-faces-only 'all
     erc-track-shorten-function nil
     erc-track-showcount t))

  (bandali-define-key global-map
    "C-c w e" #'erc-switch-to-buffer-other-window)
  (bandali-define-key erc-mode-map
    "M-a" #'erc-track-switch-buffer))
(bandali-define-key global-map
  "C-c e l"
  (lambda ()
    (interactive)
    (erc-tls
     :id "bnc-libera"
     :server "bnc.kelar.org"
     :port 6697
     :user "bandali/irc.libera.chat"))
  "C-c e o"
  (lambda ()
    (interactive)
    (erc-tls
     :id "bnc-oftc"
     :server "bnc.kelar.org"
     :port 6697
     :user "bandali/irc.oftc.net")))

(provide 'bandali-erc)
;;; bandali-erc.el ends here

3.15. bandali-web.el

;;; bandali-web.el --- web/net-related configs -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: html, gopher, gemini

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Code:

(with-eval-after-load 'shr
  (setopt shr-max-width 70))

(with-eval-after-load 'eww
  (setopt
   eww-download-directory
   (file-name-as-directory (getenv "XDG_DOWNLOAD_DIR"))))

(with-eval-after-load 'elpher
  (setopt elpher-gemini-max-fill-width 70))

(provide 'bandali-web)
;;; bandali-web.el ends here

3.16. bandali-simple.el

;;; bandali-simple.el --- bandali's common commands  -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2026 Amin Bandali

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: tools

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; A grab-bag of my commands for GNU Emacs.

;;; Code:

(require 'bandali-core)

(defun bandali-insert-asterism ()
  "Insert a centred asterism."
  (interactive)
  (let ((asterism "* * *"))
    (insert
     (concat
      "\n"
      (make-string
       (floor (/ (- fill-column (length asterism)) 2))
       ?\s)
      asterism
      "\n"))))

(defun bandali-join-line-top ()
  "Like `join-line', but join next line to the current line."
  (interactive)
  (join-line 1))

(defun bandali-*scratch* ()
  "Switch to `*scratch*' buffer, creating it if it does not exist."
  (interactive)
    (switch-to-buffer (get-scratch-buffer-create)))

(defun bandali-duplicate-line-or-region (&optional n)
  "Duplicate the current line, or region (if active).
Make N (default: 1) copies of the current line or region."
  (interactive "*p")
  (let ((u-r-p (use-region-p))          ; if region is active
        (n1 (or n 1)))
    (save-excursion
      (let ((text
             (if u-r-p
                 (buffer-substring (region-beginning) (region-end))
               (prog1 (thing-at-point 'line)
                 (end-of-line)
                 (if (eobp)
                     (newline)
                   (forward-line 1))))))
        (dotimes (_ (abs n1))
          (insert text))))))

(defun bandali-invert-default-face (arg)
  "Invert the `default' and `mode-line' faces for the current frame.
Swap the background and foreground for the two `default' and
`mode-line' faces, effectively acting like a simple light/dark
theme toggle.  If prefix argument ARG is given, invert the faces
for all frames."
  (interactive "P")
  (let ((frame (unless arg
                 (selected-frame))))
    (invert-face 'default frame)
    (invert-face 'mode-line frame)
    (when (fboundp 'exwm-systemtray--refresh-background-color)
      (exwm-systemtray--refresh-background-color 'remap))))

(defun bandali-unfill-paragraph-or-region (&optional beg end)
  "Unfill paragraph, or region (if active)."
  (interactive "r")
  (let ((fill-column most-positive-fixnum))
    (if (use-region-p)
        (fill-region beg end)
      (fill-paragraph))))

(defun bandali-pactl-get-volume (&optional type name)
  "Returns current Pulse volume from `pactl' if possible, else `nil'.
By default, it will return the volume of the default sink.

Optional TYPE must be either \"sink\" or \"source\".

Optional NAME should be the name of the sink/source whose volume should
be retrieved.  If not provided, the default sink/source will be used."
  (interactive
   (list
    (completing-read "Type (default sink): "
                     '("sink" "source")
                     nil t nil nil "sink")
    (let ((n (read-string "Sink/source name: ")))
      (unless (string-empty-p n) n))))
  (let* ((s_t (or type "sink"))
         (s_n (or name (format "@DEFAULT_%s@" (upcase s_t))))
         (out (condition-case err
                  (process-lines
                   "pactl" (format "get-%s-volume" s_t) s_n)
                (error
                 (message "Error invoking pactl: %s"
                          (error-message-string err))
                 nil))))
    (catch 'vol_str
      (dolist (line out)
        (when (string-match "\\([[:digit:]]+\\)%" line)
          (throw 'vol_str (match-string 1 line)))))))

(defun bandali-pactl-set-default-sink-volume (volume)
  "Try to set VOLUME of the default sink using `pactl'."
  (interactive
   ;; read `volume' as string so we can differentiate 5 from +5
   (list
    (read-string
     (format-prompt "sink volume" (bandali-pactl-get-volume "sink")))))
  (start-process
   "" nil
   "pactl" "set-sink-volume" "@DEFAULT_SINK@" (format "%s%%" volume)))

(defun bandali-pactl-set-default-source-volume (volume)
  "Try to set VOLUME of the default source using `pactl'."
  (interactive
   ;; read `volume' as string so we can differentiate 5 from +5
   (list
    (read-string
     (format-prompt "source volume" (bandali-pactl-get-volume "source")))))
  (start-process
   "" nil
   "pactl" "set-sink-volume" "@DEFAULT_SOURCE@" (format "%s%%" volume)))

(defun bandali-brightnessctl-get ()
  "Returns current brightness using `brightnessctl'."
  (interactive)
  (let ((out (condition-case err
                 (process-lines "brightnessctl" "-m")
               (error
                (message "Error invoking brightnessctl: %s"
                         (error-message-string err))
                nil))))
    (catch 'brightness_str
      (dolist (line out)
        (when (string-match "\\([[:digit:]]+\\)%" line)
          (throw 'brightness_str (match-string 1 line)))))))

(defun bandali-brightnessctl-set (level)
  "Try to set brightness to LEVEL percent using `brightnessctl'."
  (interactive
   (list
    (read-string
     (format-prompt "brightness" (bandali-brightnessctl-get)))))
  (start-process "" nil "brightnessctl" "s" (format "%s%%" level)))

(bandali-define-key global-map
  "C-c s c" #'bandali-*scratch*
  "C-c d" #'bandali-duplicate-line-or-region
  "C-c j" #'bandali-join-line-top
  "C-c v" #'bandali-invert-default-face
  "C-c q" #'bandali-unfill-paragraph-or-region)

(provide 'bandali-simple)
;;; bandali-simple.el ends here

4. The attic

The attic is where I stash the pieces of configuration I no longer use. These were known to work when I last used them, but may have fully or partially stopped working since and need some love to get them back in shape.

4.1. bandali-ibuffer.el

My Ibuffer configuration which I no longer use these days, since the combination of vertico and marginalia tends to cover the use-case.

;;; bandali-ibuffer.el --- bandali's Ibuffer setup -*- lexical-binding: t; -*-

;; Copyright (c) 2018-2026 Amin Bandali <bandali@gnu.org>

;; Author: Amin Bandali <bandali@gnu.org>
;; Keywords: tools

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; My Ibuffer setup.

;;; Code:

(require 'bandali-core)

(with-eval-after-load 'ibuffer
  (setopt
   ibuffer-saved-filter-groups
   '(("default"
      ("dired" (mode . dired-mode))
      ("erc" (mode . erc-mode))
      ("gnus"
       (or
        (mode . gnus-group-mode)
        (mode . gnus-server-mode)
        (mode . gnus-summary-mode)
        (mode . gnus-article-mode)
        (mode . message-mode)))
      ("shell"
       (or
        (mode . eshell-mode)
        (mode . shell-mode)
        (mode . term-mode)))
      ("slack"
       (or
        (mode . slack-mode)
        (mode . slack-buffer-mode)
          
        (name . "\\*slack.+")))
      ("tex"
       (or
        (mode . tex-mode)
        (mode . bibtex-mode)
        (mode . latex-mode)))))
   ibuffer-formats
   `((mark modified read-only locked
           " " (name 18 18 :left :elide)
           " " (size-h 9 -1 :right)     ; human-readable size
           " " (mode 16 16 :left :elide) " " filename-and-process)
     ,@ibuffer-formats))
  ;; Use human readable Size column instead of original one.
  (define-ibuffer-column size-h
    (:name "Size" :inline t)
    (cond
     ((> (buffer-size) (* 1024 1024))
      (format "%7.1fM" (/ (buffer-size) (* 1024.0 1024.0))))
     ((> (buffer-size) (* 100 1024))
      (format "%7.0fK" (/ (buffer-size) 1024.0)))
     ((> (buffer-size) 1024)
      (format "%7.1fK" (/ (buffer-size) 1024.0)))
     (t (format "%8d" (buffer-size)))))

  (bandali-define-key ibuffer-mode-map
    "P" #'ibuffer-backward-filter-group
    "N" #'ibuffer-forward-filter-group
    "M-p" #'ibuffer-do-print
    "M-n" #'ibuffer-do-shell-command-pipe-replace))

(bandali-define-key global-map "C-x C-b" #'ibuffer)
(declare-function
 ibuffer-switch-to-saved-filter-groups "ibuf-ext" (name))
(add-hook
 'ibuffer-hook
 (lambda () (ibuffer-switch-to-saved-filter-groups "default")))

(provide 'bandali-ibuffer)
;;; bandali-ibuffer.el ends here

← home | خانه