;;; init.el --- bandali's emacs configuration -*- lexical-binding: t -*- ;; Copyright (c) 2018-2024 Amin Bandali ;; 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 . ;;; Commentary: ;; bandali's opinionated GNU Emacs configs. I tend to use the latest ;; development trunk of emacs.git, but I try to maintain backward ;; compatibility with a few of the recent older GNU Emacs releases ;; so I could easily reuse it on machines stuck with older Emacsen. ;; When initially putting this together, I took inspiration from ;; configurations of many great people. Some that I could remember ;; off the top of my head are: ;; ;; - https://github.com/dieggsy/dotfiles ;; - https://github.com/dakra/dmacs ;; - https://pages.sachachua.com/.emacs.d/Sacha.html ;; - https://github.com/dakrone/eos ;; - https://cce.whatthefuck.computer/ ;; - https://github.com/jwiegley/dot-emacs ;; - https://github.com/wasamasa/dotemacs ;; - https://github.com/hlissner/doom-emacs ;;; Code: ;;; Emacs initialization ;; Temporarily increase `gc-cons-threshhold' and `gc-cons-percentage' ;; during startup to reduce garbage collection frequency. Clearing ;; `file-name-handler-alist' seems to help reduce startup time too. (defconst b/gc-cons-threshold gc-cons-threshold) (defconst b/gc-cons-percentage gc-cons-percentage) (defvar b/file-name-handler-alist file-name-handler-alist) (setq gc-cons-threshold (* 30 1024 1024) ; 30 MiB gc-cons-percentage 0.6 file-name-handler-alist nil) ;; Set them back to their defaults once we're done initializing. (defun b/post-init () "My post-initialize function, run after loading `user-init-file'." (setq b/emacs-initialized t gc-cons-threshold b/gc-cons-threshold gc-cons-percentage b/gc-cons-percentage file-name-handler-alist b/file-name-handler-alist) (require 'package) (package-initialize)) (add-hook 'after-init-hook #'b/post-init) ;; whoami (setq user-full-name "Amin Bandali" user-mail-address "bandali@kelar.org") ;;; Initial setup (eval-and-compile (defsubst b/emacs.d (path) "Expand path PATH relative to `user-emacs-directory'." (expand-file-name (convert-standard-filename path) user-emacs-directory)) ;; Wrappers around the new keybinding functions, with fallback to ;; the corresponding older lower level function on older Emacsen. (defsubst b/keymap-set (keymap key definition) (if (version< emacs-version "29") (define-key keymap (kbd key) definition) (keymap-set keymap key definition))) (defsubst b/keymap-global-set (key command) (if (version< emacs-version "29") (global-set-key (kbd key) command) (keymap-global-set key command))) (defsubst b/keymap-local-set (key command) (if (version< emacs-version "29") (local-set-key (kbd key) command) (keymap-local-set key command))) (defsubst b/keymap-global-unset (key) (if (version< emacs-version "29") (global-unset-key (kbd key)) (keymap-global-unset key 'remove))) (defsubst b/keymap-local-unset (key) (if (version< emacs-version "29") (local-unset-key (kbd key)) (keymap-local-unset key 'remove))) (when (version< emacs-version "29") ;; Emacs 29 introduced the handy `setopt' macro for setting user ;; options (defined with `defcustom') with a syntax similar to ;; `setq'. So, we define it on older Emacsen that don't have it. (defmacro setopt (&rest pairs) "Set VARIABLE/VALUE pairs, and return the final VALUE. This is like `setq', but is meant for user options instead of plain variables. This means that `setopt' will execute any `custom-set' form associated with VARIABLE. \(fn [VARIABLE VALUE]...)" (declare (debug setq)) (unless (zerop (mod (length pairs) 2)) (error "PAIRS must have an even number of variable/value members")) (let ((expr nil)) (while pairs (unless (symbolp (car pairs)) (error "Attempting to set a non-symbol: %s" (car pairs))) (push `(setopt--set ',(car pairs) ,(cadr pairs)) expr) (setq pairs (cddr pairs))) (macroexp-progn (nreverse expr)))) (defun setopt--set (variable value) (custom-load-symbol variable) ;; Check that the type is correct. (when-let ((type (get variable 'custom-type))) (unless (widget-apply (widget-convert type) :match value) (warn "Value `%S' does not match type %s" value type))) (put variable 'custom-check-value (list value)) (funcall (or (get variable 'custom-set) #'set-default) variable value)))) ;; Separate custom file (don't want it mixing with init.el). (setopt custom-file (b/emacs.d "custom.el")) (with-eval-after-load 'custom (when (file-exists-p custom-file) (load custom-file))) ;; Start Emacs server ;; (https://www.gnu.org/software/emacs/manual/html_node/emacs/Emacs-Server.html) (run-with-idle-timer 0.5 nil #'require 'server) (with-eval-after-load 'server (declare-function server-edit "server") (b/keymap-global-set "C-c F D" #'server-edit) (declare-function server-running-p "server") (or (server-running-p) (server-mode))) ;;; Defaults ;;;; C source code (setq-default ;; Case-sensitive search (and `dabbrev-expand'). ;; case-fold-search nil indent-tabs-mode nil ; always use space for indentation ;; tab-width 4 indicate-buffer-boundaries 'left) (setq ;; line-spacing 3 completion-ignore-case t read-buffer-completion-ignore-case t enable-recursive-minibuffers t resize-mini-windows t message-log-max 20000 mode-line-compact t ;; mouse-autoselect-window t scroll-conservatively 15 scroll-preserve-screen-position 1 ;; I don't feel like randomly jumping out of my chair. ring-bell-function 'ignore) ;;;; elisp source code (with-eval-after-load 'minibuffer (setopt read-file-name-completion-ignore-case t)) (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) (add-to-list 'auto-mode-alist '("\\README.*" . text-mode)) (add-to-list 'auto-mode-alist '("\\.*rc$" . conf-mode)) (add-to-list 'auto-mode-alist '("\\.bashrc$" . sh-mode))) (setq disabled-command-function nil) (run-with-idle-timer 0.1 nil #'require 'autorevert) (with-eval-after-load 'autorevert (setopt ;; auto-revert-verbose nil global-auto-revert-non-file-buffers nil) (global-auto-revert-mode 1)) (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 %Y-%m-%d %-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 `(,@zoneinfo-style-world-list ("Etc/UTC" "UTC") ("Asia/Tehran" "Tehran") ("Australia/Melbourne" "Melbourne"))) (unless (display-graphic-p) (display-time-mode))) (run-with-idle-timer 0.1 nil #'require 'battery) (with-eval-after-load 'battery (setopt battery-mode-line-format " [%b%p%% %t]") (display-battery-mode)) (run-with-idle-timer 0.5 nil #'require 'winner) (with-eval-after-load 'winner (winner-mode 1)) (run-with-idle-timer 0.5 nil #'require 'windmove) (with-eval-after-load 'windmove (setopt windmove-wrap-around t) (b/keymap-global-set "M-H" #'windmove-left) (b/keymap-global-set "M-L" #'windmove-right) (b/keymap-global-set "M-K" #'windmove-up) (b/keymap-global-set "M-J" #'windmove-down)) (with-eval-after-load 'isearch (setopt isearch-allow-scroll t isearch-lazy-count t ;; Match non-ASCII variants during search search-default-mode #'char-fold-to-regexp)) (b/keymap-global-set "C-x v C-=" #'vc-ediff) (with-eval-after-load 'vc-git (setopt ;; vc-git-show-stash 0 vc-git-print-log-follow t)) (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 'face-remap ;; (setopt ;; ;; Gentler font resizing. ;; text-scale-mode-step 1.05)) (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)) (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 'auth-source ;; (setopt ;; auth-sources '("~/.authinfo.gpg") ;; authinfo-hidden ;; (regexp-opt '("password" "client-secret" "token")))) (with-eval-after-load 'info (setq Info-directory-list `(,@Info-directory-list ,(expand-file-name (convert-standard-filename "info/") source-directory) "/usr/share/info/"))) (when (display-graphic-p) (set-fontset-font t 'arabic "Sahel WOL") (with-eval-after-load 'faces (let ((grey "#e7e7e7")) (set-face-attribute 'fixed-pitch nil :font "Source Code Pro" :weight 'medium) (set-face-attribute 'mode-line nil :background grey :inherit 'fixed-pitch)))) (when (and (version< emacs-version "28") mode-line-compact) ;; Manually make some `mode-line' spaces smaller. ;; Emacs 28 and above do a terrific job at this out of the box ;; when `mode-line-compact' is set to t (see above)." (setq-default mode-line-format (mapcar (lambda (x) (if (and (stringp x) (or (string= x " ") (string= x " "))) " " x)) mode-line-format) mode-line-buffer-identification (propertized-buffer-identification "%10b"))) ;;; Useful utilities (defun b/insert-asterism () "Insert a centred asterism." (interactive) (let ((asterism "* * *")) (insert (concat "\n" (make-string (floor (/ (- fill-column (length asterism)) 2)) ?\s) asterism "\n")))) (defun b/join-line-top () "Like `join-line', but join next line to the current line." (interactive) (join-line 1)) (defun b/*scratch* () "Switch to `*scratch*' buffer, creating it if it does not exist." (interactive) (let ((fun (if (functionp #'get-scratch-buffer-create) #'get-scratch-buffer-create ; (version<= "29" emacs-version) #'startup--get-buffer-create-scratch))) ; (version< emacs-version "29") (switch-to-buffer (funcall fun)))) (defun b/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 b/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))) ;;; General key bindings (let ((kfs '(("C-c i" . ielm) ("C-c d" . b/duplicate-line-or-region) ("C-c j" . b/join-line-top) ("C-S-j" . b/join-line-top) ("C-c s c" . b/*scratch*) ("C-c v" . b/invert-default-face) ;; evaling and macro-expanding ("C-c e b" . eval-buffer) ("C-c e e" . eval-last-sexp) ("C-c e m" . pp-macroexpand-last-sexp) ("C-c e r" . eval-region) ;; emacs things ("C-c e i" . emacs-init-time) ("C-c e u" . emacs-uptime) ("C-c e v" . emacs-version) ;; finding ("C-c f ." . find-file) ("C-c f l" . find-library) ("C-c f p" . find-file-at-point) ;; frames ("C-c F m" . make-frame-command) ("C-c F d" . delete-frame) ;; help/describe ("C-c h F" . describe-face)))) (dolist (kf kfs) (let ((key (car kf)) (fun (cdr kf))) (b/keymap-global-set key fun)))) (when (display-graphic-p) ;; Too easy to accidentally suspend (freeze) Emacs GUI. (b/keymap-global-unset "C-z")) ;;; Essential packages (add-to-list 'load-path (b/emacs.d "lisp")) ;; recently opened files (run-with-idle-timer 0.2 nil #'require 'recentf) (with-eval-after-load 'recentf (setopt recentf-max-saved-items 2000) (recentf-mode) (defun b/recentf-open () "Use `completing-read' to \\[find-file] a recent file." (interactive) (find-file (completing-read "Find recent file: " recentf-list))) (b/keymap-global-set "C-c f r" #'b/recentf-open)) (with-eval-after-load 'eshell (setopt eshell-hist-ignoredups t eshell-input-filter #'eshell-input-filter-initial-space eshell-prompt-regexp "^[^#$\n]* [#$] ; " eshell-prompt-function (lambda () (let ((uid (if (functionp #'file-user-uid) #'file-user-uid ; (version<= "30" emacs-version) #'user-uid))) ; (version< emacs-version "30") (concat ": " (system-name) ":" (abbreviate-file-name (eshell/pwd)) (unless (eshell-exit-success-p) (format " [%d]" eshell-last-command-status)) (if (= (funcall uid) 0) " # " " $ ") "; ")))) (eval-when-compile (defvar eshell-prompt-regexp) (declare-function eshell-life-is-too-much "esh-mode") (declare-function eshell-send-input "esh-mode" (&optional use-region queue-p no-newline))) (defun b/eshell-quit-or-delete-char (arg) (interactive "p") (if (and (eolp) (looking-back eshell-prompt-regexp nil)) (eshell-life-is-too-much) (delete-char arg))) (defun b/eshell-clear () (interactive) (let ((inhibit-read-only t)) (erase-buffer)) (eshell-send-input)) (defun b/eshell-history () (interactive) (completing-read "Eshell history: " (ring-elements eshell-history-ring))) (with-eval-after-load 'esh-mode (let ((m eshell-mode-map)) (b/keymap-set m "C-d" #'b/eshell-quit-or-delete-char) (b/keymap-set m "C-S-l" #'b/eshell-clear))) (with-eval-after-load 'esh-hist (let ((m eshell-hist-mode-map)) (b/keymap-set m "M-r" #'b/eshell-history)))) (b/keymap-global-set "C-c s e" #'eshell) (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))) ("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) " " (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))))) (let ((m ibuffer-mode-map)) (b/keymap-set m "P" #'ibuffer-backward-filter-group) (b/keymap-set m "N" #'ibuffer-forward-filter-group) (b/keymap-set m "M-p" #'ibuffer-do-print) (b/keymap-set m "M-n" #'ibuffer-do-shell-command-pipe-replace))) (b/keymap-global-set "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"))) (with-eval-after-load 'dired ;; (require 'ls-lisp) (setopt dired-dwim-target t ;; dired-listing-switches "-alh --group-directories-first" dired-listing-switches "-alh" ;; ls-lisp-dirs-first t ls-lisp-use-insert-directory-program nil) (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 (let ((m dired-mode-map)) (b/keymap-set m "b" #'dired-up-directory) (b/keymap-set m "E" #'dired-ediff-files) (b/keymap-set m "e" #'dired-toggle-read-only) (b/keymap-set m "\\" #'dired-hide-details-mode)) (require 'dired-x) (setopt dired-guess-shell-alist-user '(("\\.pdf\\'" "atril" "evince" "zathura" "okular") ("\\.doc\\'" "libreoffice") ("\\.docx\\'" "libreoffice") ("\\.ppt\\'" "libreoffice") ("\\.pptx\\'" "libreoffice") ("\\.xls\\'" "libreoffice") ("\\.xlsx\\'" "libreoffice") ("\\.flac\\'" "mpv")))) (add-hook 'dired-mode-hook #'dired-hide-details-mode) (with-eval-after-load 'help (temp-buffer-resize-mode) (setopt help-window-select t)) (with-eval-after-load 'help-mode (let ((m help-mode-map)) (b/keymap-set m "p" #'backward-button) (b/keymap-set m "n" #'forward-button) (b/keymap-set m "b" #'help-go-back) (b/keymap-set m "f" #'help-go-forward))) (with-eval-after-load 'doc-view (b/keymap-set doc-view-mode-map "M-RET" #'image-previous-line)) (with-eval-after-load 'shr (setopt shr-max-width 80)) ;;; Email (defvar b/maildir (expand-file-name (convert-standard-filename "~/mail/"))) (with-eval-after-load 'recentf (add-to-list 'recentf-exclude b/maildir)) (setopt mail-user-agent 'gnus-user-agent read-mail-command #'gnus) (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 (with-eval-after-load 'nnimap (setq nnimap-record-commands init-file-debug)) (setopt gnus-select-method '(nnnil "") gnus-secondary-select-methods `(,@(when (member (system-name) '("darya" "nostalgia")) '((nnimap "canonical" (nnimap-stream plain) (nnimap-address "127.0.0.1") (nnimap-server-port 143) (nnimap-authenticator plain) (nnimap-user "bandali@canonical.local")))) (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 "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"))) (nnimap "sfl" (nnimap-stream plain) (nnimap-address "127.0.0.1") (nnimap-server-port 143) (nnimap-authenticator plain) (nnimap-user "amin.bandali@savoirfairelinux.local"))) 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 (b/emacs.d "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))) (when (version< emacs-version "27") (with-eval-after-load 'nnmail (add-to-list 'nnmail-split-abbrev-alist '(list . "list-id\\|list-post\\|x-mailing-list\\|x-beenthere\\|x-loop") 'append))) (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 "\\|"))) (with-eval-after-load 'gnus-dired (with-eval-after-load '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 b/csc-signature (mapconcat #'identity `(,(format bandali ", MMath") "Systems Committee " "Computer Science Club of the University of Waterloo") "\n")) (defvar b/sfl-signature (mapconcat #'identity `(,(format bandali "") "Volunteer, Savoir-faire Linux" "jami:bandali") "\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\\+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\\+canonical:.*" (address "bandali@canonical.com") ("X-Message-SMTP-Method" "smtp smtp.canonical.com 587") (signature nil) (gcc "nnimap+canonical:Sent")) ((header "to" "amin\\.bandali@canonical\\.com") (address "amin.bandali@canonical.com")) ((header "cc" "amin\\.bandali@canonical\\.com") (address "amin.bandali@canonical.com")) ;; ("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 b/csc-signature) (gcc "nnimap+csc:Sent")) ("nnimap\\+sfl:.*" (address "amin.bandali@savoirfairelinux.com") ("X-Message-SMTP-Method" "smtp mail.savoirfairelinux.com 587") (signature b/sfl-signature) (gcc "nnimap+sfl:Sent"))))) ;; (require 'gnus-registry) ;; (with-eval-after-load 'gnus-registry ;; (setopt ;; gnus-registry-max-entries 2500 ;; gnus-registry-ignored-groups ;; (append gnus-registry-ignored-groups ;; '(("^nnimap:gnu\\.l" t) ("webmasters$" t)))) ;; (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) (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 b/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 b/gnus-summary-prefix-map) (define-prefix-command 'b/gnus-summary-prefix-map) (b/keymap-set gnus-summary-mode-map "v" 'b/gnus-summary-prefix-map) (let ((m b/gnus-summary-prefix-map)) (b/keymap-set m "r r" #'gnus-summary-very-wide-reply) (b/keymap-set m "r q" #'gnus-summary-very-wide-reply-with-original) (b/keymap-set m "R r" #'gnus-summary-reply) (b/keymap-set m "R q" #'gnus-summary-reply-with-original) (b/keymap-set m "r a w" #'gnus-summary-show-raw-article) (b/keymap-set m "s" #'b/gnus-junk-article))) (with-eval-after-load 'gnus-topic ;; (setopt gnus-topic-line-format "%i[ %A: %(%{%n%}%) ]%v\n") (setopt gnus-topic-line-format "%i[ %(%{%n%}%) (%A) ]%v\n") (setq gnus-topic-topology `(("Gnus" visible nil nil) (("misc" visible nil nil)) ,@(when (member (system-name) '("darya" "nostalgia")) '((("canonical" visible nil nil)))) (("csc" visible nil nil)) (("kelar" visible nil nil)) (("shemshak" visible nil nil)) (("debian" visible nil nil)) (("gnu" visible nil nil)) ;; (("old-gnu" visible nil nil)) (("sfl" 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)) (when (version< emacs-version "27") (setopt mm-uu-diff-groups-regexp "."))) (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)) (with-eval-after-load 'recentf (add-to-list 'recentf-exclude gnus-home-directory))) (b/keymap-global-set "C-c g" #'gnus-plugged) (b/keymap-global-set "C-c G" #'gnus-unplugged) (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 "[...]\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" "amin\\.bandali@savoirfairelinux\\.com" "\\(amin\\.\\)?bandali@canonical\\.com" "bandali@ubuntu\\.com" "bandali@debian\\.org") "\\|")) (defun b/newlines-or-asterism (arg) "Create newlines per my liking, or insert asterism if ARG is non-nil." (interactive "P") (if arg (b/insert-asterism) (progn (beginning-of-line) (delete-region (point) (line-end-position)) (newline) (open-line 1)))) (b/keymap-set message-mode-map "M-RET" #'b/newlines-or-asterism) (add-hook 'message-mode-hook #'flyspell-mode) (add-hook 'message-mode-hook (lambda () (b/keymap-local-unset "C-c C-s")))) ;; (with-eval-after-load 'sendmail ;; (setopt mail-header-separator "")) ;; (with-eval-after-load 'smtpmail ;; (setopt smtpmail-queue-mail t ;; smtpmail-queue-dir (concat b/maildir "queue/"))) ;;; IRC (with-eval-after-load 'erc (setopt erc-auto-query 'bury erc-autojoin-domain-only nil erc-dcc-get-default-directory (b/emacs.d "erc-dcc") erc-email-userid "bandali" ;; erc-join-buffer 'bury ;; 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) (if (version< erc-version "5.6-git") (setopt erc-format-nick-function #'erc-format-@nick) (setopt erc-show-speaker-membership-status t)) (unless (version< erc-version "5.5") (setopt erc-rename-buffers t)) (unless (version< erc-version "5.4") (declare-function erc-message "erc-backend" (message-command line &optional force)) (declare-function erc-default-target "erc") (declare-function erc-current-nick "erc") (defun erc-cmd-OPME () "Ask chanserv to op me in the current channel." (erc-message "PRIVMSG" (format "chanserv op %s %s" (erc-default-target) (erc-current-nick)) nil)) (declare-function erc-cmd-DEOP "erc" (&rest people)) (defun erc-cmd-DEOPME () "Deop myself in the current channel." (erc-cmd-DEOP (format "%s" (erc-current-nick))))) (add-to-list 'erc-modules 'keep-place) (add-to-list 'erc-modules 'log) (when (display-graphic-p) (add-to-list 'erc-modules 'notifications) (add-to-list 'erc-modules 'smiley)) (add-to-list 'erc-modules 'spelling) (setopt ;; erc-enable-logging 'erc-log-all-but-server-buffers erc-log-file-coding-system 'utf-8 erc-log-write-after-insert t erc-log-write-after-send t erc-save-buffer-on-part nil erc-save-queries-on-quit nil) (with-eval-after-load 'erc-match (setopt erc-pal-highlight-type 'nick erc-pals '("corwin" "^gopar" "^iank" "^rwp" "technomancy" "thomzane")) (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 ",") ;; for matterircd nick (username) completions ;; (advice-add ;; #'pcomplete-erc-nicks ;; :around ;; (lambda (orig-fun &rest args) ;; (let ((nicks (apply orig-fun args))) ;; (if (string-match-p "matterircd" (symbol-name (erc-network))) ;; (mapcar (lambda (nick) (concat "@" nick)) nicks) ;; nicks)))) ) (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)) (declare-function erc-update-modules "erc") (erc-update-modules) (b/keymap-global-set "C-c w e" #'erc-switch-to-buffer-other-window) (b/keymap-set erc-mode-map "M-a" #'erc-track-switch-buffer)) (b/keymap-global-set "C-c e l" (lambda () (interactive) (erc-tls :server "irc.libera.chat" :port 6697 :client-certificate t))) (b/keymap-global-set "C-c e o" (lambda () (interactive) (erc-tls :server "irc.oftc.net" :port 6697 :client-certificate t))) (b/keymap-global-set "C-c e t" (lambda () (interactive) (erc-tls :server "na.tilde.chat" :port 6697 :client-certificate t))) ;; (b/keymap-global-set ;; "C-c e c" ;; (lambda () ;; (interactive) ;; (erc :server "localhost" :port 6667 ;; :id 'matterircd-canonical))) ;;; Editing ;; 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 (show-paren-mode 1)) (with-eval-after-load 'simple (setopt ;; 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)) (run-with-idle-timer 0.2 nil #'require 'savehist) (with-eval-after-load 'savehist ;; Save minibuffer history. (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)) (with-eval-after-load 'flyspell (setopt flyspell-mode-line-string " fly")) (with-eval-after-load 'text-mode (add-hook 'text-mode-hook #'flyspell-mode) (b/keymap-set text-mode-map "M-RET" #'b/insert-asterism)) (with-eval-after-load 'abbrev (add-hook 'text-mode-hook #'abbrev-mode)) ;;; Programming modes (with-eval-after-load 'lisp-mode (add-hook 'lisp-interaction-mode-hook (lambda () (setq indent-tabs-mode nil)))) ;; (add-to-list 'load-path (b/lisp "alloy-mode")) ;; (autoload 'alloy-mode "alloy-mode" nil t) ;; (with-eval-after-load 'alloy-mode ;; (setq alloy-basic-offset 2) ;; ;; (defun b/alloy-simple-indent (start end) ;; ;; (interactive "r") ;; ;; ;; (if (region-active-p) ;; ;; ;; (indent-rigidly start end alloy-basic-offset) ;; ;; ;; (if (bolp) ;; ;; ;; (indent-rigidly (line-beginning-position) ;; ;; ;; (line-end-position) ;; ;; ;; alloy-basic-offset))) ;; ;; (indent-to (+ (current-column) alloy-basic-offset))) ;; (define-key alloy-mode-map (kbd "RET") #'electric-newline-and-maybe-indent) ;; ;; (define-key alloy-mode-map (kbd "TAB") #'b/alloy-simple-indent) ;; (define-key alloy-mode-map (kbd "TAB") #'indent-for-tab-command)) ;; (add-to-list 'auto-mode-alist '("\\.\\(als\\|dsh\\)\\'" . alloy-mode)) ;; (add-hook 'alloy-mode-hook (lambda nil (setq-local indent-tabs-mode nil))) ;; (eval-when-compile (defvar lean-mode-map)) ;; (run-with-idle-timer 0.4 nil #'require 'lean-mode) ;; (with-eval-after-load 'lean-mode ;; (require 'lean-input) ;; (setq default-input-method "Lean" ;; lean-input-tweak-all '(lean-input-compose ;; (lean-input-prepend "/") ;; (lean-input-nonempty)) ;; lean-input-user-translations '(("/" "/"))) ;; (lean-input-setup) ;; ;; local key bindings ;; (define-key lean-mode-map (kbd "S-SPC") #'company-complete)) (with-eval-after-load 'sgml-mode (setopt sgml-basic-offset 0)) (with-eval-after-load 'css-mode (setopt css-indent-offset 2)) (add-hook 'tex-mode-hook #'auto-fill-mode) (add-hook 'tex-mode-hook #'flyspell-mode) (autoload 'cmake-mode "cmake-mode" nil t) (add-to-list 'auto-mode-alist '("CMakeLists\\.txt\\'" . cmake-mode)) (add-to-list 'auto-mode-alist '("\\.cmake\\'" . cmake-mode)) (with-eval-after-load 'cmake-mode (add-to-list 'load-path (b/emacs.d "lisp/cmake-font-lock")) (require 'cmake-font-lock)) ;;; Emacs enhancements & auxiliary packages (with-eval-after-load 'man (setopt Man-width 80)) ;; `debbugs' (b/keymap-global-set "C-c D d" #'debbugs-gnu) (b/keymap-global-set "C-c D b" #'debbugs-gnu-bugs) (b/keymap-global-set "C-c D e" ; bug-gnu-emacs (lambda () (interactive) (setq debbugs-gnu-current-suppress t) (debbugs-gnu debbugs-gnu-default-severities '("emacs")))) (b/keymap-global-set "C-c D g" ; bug-gnuzilla (lambda () (interactive) (setq debbugs-gnu-current-suppress t) (debbugs-gnu debbugs-gnu-default-severities '("gnuzilla")))) (with-eval-after-load 'eww (setopt eww-download-directory (file-name-as-directory (getenv "XDG_DOWNLOAD_DIR")))) (b/keymap-global-set "C-c e w" #'eww) (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)) (with-eval-after-load 'window (setopt split-width-threshold 150)) (add-hook 'latex-mode-hook #'reftex-mode) (run-with-idle-timer 0.5 nil #'require 'delight) (with-eval-after-load 'delight (delight 'auto-fill-function " f" "simple") (delight 'abbrev-mode "" "abbrev") (delight 'mml-mode " mml" "mml")) (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 b/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))))) (add-hook 'po-mode-hook (lambda () (run-with-timer 0.1 nil #'View-exit))) (b/keymap-set po-mode-map "M-q" #'b/po-wrap)) (autoload #'po-mode "po-mode" "Major mode for editing PO translation files" t) (add-to-list 'auto-mode-alist '("\\.po\\'\\|\\.po\\." . po-mode)) (add-to-list 'load-path (b/emacs.d "lisp/ffs")) (run-with-idle-timer 0.5 nil #'require 'ffs) (with-eval-after-load 'ffs (setopt ffs-default-face-height 250) (global-set-key (kbd "C-c f s") #'ffs)) (add-hook 'ffs-start-hook (lambda () (mapc (lambda (mode) (funcall mode 1)) ; enable '(ffs--no-mode-line-minor-mode ffs--no-cursor-minor-mode)) (mapc (lambda (mode) (funcall mode -1)) ; disable '(show-paren-local-mode display-battery-mode display-fill-column-indicator-mode flyspell-mode tool-bar-mode menu-bar-mode scroll-bar-mode)) (fringe-mode 0))) (add-hook 'ffs-quit-hook (lambda () (mapc (lambda (mode) (funcall mode -1)) ; disable '(ffs--no-mode-line-minor-mode ffs--no-cursor-minor-mode)) (mapc (lambda (mode) (funcall mode 1)) ; enable '(show-paren-local-mode display-battery-mode display-fill-column-indicator-mode flyspell-mode tool-bar-mode menu-bar-mode scroll-bar-mode)) (fringe-mode nil))) (add-to-list 'load-path (b/emacs.d "lisp/debian-el")) (run-with-idle-timer 0.5 nil #'require 'debian-el) (with-eval-after-load 'debian-el (require 'apt-sources) (require 'apt-utils) (require 'debian-bug) (require 'deb-view) (require 'gnus-BTS) (require 'preseed)) (add-to-list 'load-path (b/emacs.d "lisp/dpkg-dev-el")) (run-with-idle-timer 0.5 nil #'require 'dpkg-dev-el) (with-eval-after-load 'dpkg-dev-el (require 'debian-changelog-mode) (require 'debian-bts-control) (require 'debian-changelog-mode) (require 'debian-control-mode) (require 'debian-copyright) (require 'readme-debian)) ;;; init.el ends here