;;; .emacs --- bandali's emacs configuration -*- lexical-binding:t -*- ;; Copyright (c) 2018-2026 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 . ;;;; Initialization (setq ; invoking emacs with --debug-init sets `init-file-debug' to t debug-on-error init-file-debug debug-on-quit init-file-debug) (setq ; prefer newest version of a file load-prefer-newer t) (setq ; whoami user-full-name "Amin Bandali" user-mail-address "bandali@kelar.org") (setq ; make custom-file disposable custom-file (make-temp-file "emacs-custom-")) (setq ; don't resize the frame when font size is adjusted frame-resize-pixelwise t frame-inhibit-implied-resize 'force) (setq ; i don't like getting jump-scared out of my chair ring-bell-function #'ignore) (progn ; disable some distractions (*) ;; (*) if you're new to emacs, i suggest keeping the menu bar and ;; tool bar enabled for a while - they can be very helpful ;; in helping you (re)discover context-specific or general ;; functionality as you get more acquainted with emacs :) (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)) (progn ; startup and gc tweaks for faster initialization (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) (defun bandali--post-init () "My post-initialization function." (setq ; restore the defaults 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)) ;;;; Core (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))) ;; 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) ;;;; Simple commands and utilities (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) ;;;; Theme & appearance (when (or (display-graphic-p) (string= (getenv "COLORTERM") "truecolor")) (require 'doric-themes) (with-eval-after-load 'doric-themes (setopt doric-themes-to-toggle '(doric-oak doric-pine)) (doric-themes-select 'doric-oak))) (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) ))) (with-eval-after-load 'face-remap (setopt text-scale-mode-step 1.05 text-scale-remap-header-line t)) ;; Emacs 29 introduced global-text-scale-adjust (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) (run-with-idle-timer 0.2 nil #'require 'display-fill-column-indicator nil 'noerror) (with-eval-after-load 'display-fill-column-indicator ;; (add-hook 'prog-mode-hook #'display-fill-column-indicator-mode) ;; (add-hook 'message-mode-hook #'display-fill-column-indicator-mode) (global-display-fill-column-indicator-mode 1)) ;;;; Essentials (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 "\\\\[eval-print-last-sexp]"))) (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))) (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))) (setopt tab-always-indent 'complete) (setq-default indent-tabs-mode nil tab-width 4) (setq-default indicate-buffer-boundaries 'left) (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)) (with-eval-after-load 'package (setopt ;; Explicitly set `package-archives', in part to ensure https ones ;; are used, and also to have NonGNU ELPA on older Emacsen as well. package-archives '(("gnu" . "https://elpa.gnu.org/packages/") ("gnu-devel" . "https://elpa.gnu.org/devel/") ("nongnu" . "https://elpa.nongnu.org/nongnu/")) package-archive-priorities '(("gnu" . 3) ("gnu-devel" . 2) ("nongnu" . 1)) package-pinned-packages '((doric-themes . "gnu-devel")) package-review-policy t)) (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/"))) (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))) (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) (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)) (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)) (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"))) (unless (display-graphic-p) (display-time-mode 1))) (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)) (unless (display-graphic-p) (display-battery-mode 1))) (setopt trusted-content `(,(file-name-as-directory (locate-user-emacs-file "lisp/ffs")))) (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 ;; " " #'eat-mouse-yank-primary ;; " " #'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) (with-eval-after-load 'select (setopt select-enable-clipboard t ;; select-enable-primary t )) (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 (setopt remote-file-name-inhibit-locks t remote-file-name-inhibit-auto-save-visited t tramp-default-method "rsync" tramp-copy-size-limit (* 1024 1024) ;; 1MB tramp-verbose 2) (connection-local-set-profile-variables 'remote-direct-async-process '((tramp-direct-async-process . t))) (connection-local-set-profiles '(:application tramp :protocol "scp") 'remote-direct-async-process) (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")))) (with-eval-after-load 'appt (setopt appt-display-diary nil appt-display-format nil appt-display-mode-line t appt-display-interval 5 appt-message-warning-time 20)) (add-hook 'after-init-hook (lambda () (require 'appt) (appt-activate 1))) ;; `ffs' (add-hook 'ffs-present-mode-hook (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)))) (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") (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) ;;;; Window (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")) "f" (lambda () (interactive) (start-process "" nil "b-browser" "-P" "farangis")) "F" (lambda () (interactive) (start-process "" nil "b-browser" "-P" "farangis" "-private-window")) "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-layout (setopt exwm-layout-fullscreen-release-keyboard nil)) (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 " " #'bandali-exwm-ws-prev " " #'bandali-exwm-ws-next " " #'bandali-exwm-ws-prev " " #'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 display-buffer-alist '(("\\`\\*Group\\*\\'" (display-buffer-reuse-mode-window display-buffer-in-tab) (mode . gnus-group-mode) (tab-name . "Gnus") (inhibit-switch-frame . t)) ("\\`\\*vc-dir\\*" (display-buffer-reuse-mode-window display-buffer-in-tab) (mode . vc-dir-mode) (tab-name . (lambda (buffer _alist) (buffer-name buffer)))) ("\\`*vc-git" (display-buffer-reuse-window display-buffer-pop-up-window) (body-function . select-window)) ((derived-mode . calendar-mode) (display-buffer-reuse-mode-window display-buffer-below-selected) (mode . calendar-mode) (inhibit-switch-frame . t) (dedicated . t) (window-height . fit-window-to-buffer)) ((derived-mode . diary-mode) (display-buffer-in-side-window) (dedicated . t) (side . bottom) (slot . 0) (window-height . 0.3) (window-parameters . ((mode-line-format . none))))) split-width-threshold 140)) (defun bandali-delete-window-dwim () "Do What I Mean to delete the current THING. THING is determined in the following order: When there is more than one window, THING is a window. When there is more than one tab, THING is a tab. When there is more than one frame, THING is a frame. Based on Protesilaos's `prot-simple-delete-window-dwim'." (declare (interactive-only t)) (interactive) (cond ((length> (window-list) 1) (delete-window)) ((length> (tab-bar-tabs) 1) (tab-close)) ((length> (frame-list) 1) (delete-frame)) (t (user-error "Nothing to delete")))) (bandali-define-key global-map "C-x 0" #'bandali-delete-window-dwim) (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)) ;;;; Mode line and tab bar (setopt mode-line-compact nil ; Emacs 28 mode-line-right-align-edge 'right-margin) ; Emacs 30 (with-eval-after-load 'doric-themes (defun bandali-mode-line-set-faces () (doric-themes-with-colors (custom-set-faces ;; "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)) (with-eval-after-load 'keycast (setopt keycast-mode-line-format "%2s%k%c%R" keycast-mode-line-insert-after 'mode-line-misc-info 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 '("" "" "" "" "" "" "" "" "" "" "" "" handle-select-window mouse-set-point mouse-drag-region)) (add-to-list 'keycast-substitute-alist `(,event nil nil)))) (with-eval-after-load 'tab-bar (setopt tab-bar-close-button-show nil tab-bar-new-button-show nil tab-bar-show 1)) ;;;; Completion (setopt completion-ignore-case t read-buffer-completion-ignore-case t) (with-eval-after-load 'minibuffer (setopt completion-show-help nil completion-show-inline-help nil completion-auto-help t completion-eager-display 'auto completion-eager-update t completion-category-overrides '((file . ((eager-display . nil) (styles basic partial-completion substring emacs22 flex))) (buffer . ((styles basic partial-completion substring emacs22 flex)))) completion-styles '(basic partial-completion substring emacs22) completions-detailed t completions-format 'one-column completions-max-height 11 completions-sort 'historical minibuffer-visible-completions t read-file-name-completion-ignore-case t)) (setq-default case-fold-search t) (setopt minibuffer-prompt-properties '(read-only t cursor-intangible t face minibuffer-prompt)) (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode) (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)))))) (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)) ;;;; Search (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)) (with-eval-after-load 'replace (setopt query-replace-show-preview 'both)) ;;;; Languages (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\\|TAG\\)_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)) ;;;; Dired (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) ;;;; VC (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-extra-root-markers '("Makefile") 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-dir (setopt vc-dir-show-key-binding-hints nil)) (with-eval-after-load 'vc-git (setopt vc-git-log-switches "--format=fuller") (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)) (when (and (boundp 'global-diff-hl-mode) (functionp #'global-diff-hl-mode)) (global-diff-hl-mode 1)) ;;;; Org (with-eval-after-load 'org (setopt org-directory "~/usr/doc/org" org-agenda-files (list org-directory) org-src-content-indentation 0 org-src-preserve-indentation t org-src-window-setup 'current-window)) (with-eval-after-load 'org-agenda (require 'appt) (appt-activate 1) ;; Create reminders for tasks with a due date when agenda is read. (org-agenda-to-appt)) (with-eval-after-load 'ox ;; (setopt org-export-timestamp-file nil) (require 'ox-texinfo)) (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")))) ;;;; Gnus ;; (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 ((to-from "bandali@debian\\.org" "nnimap+debian:Archive.%Y") ("envelope-to" "bandali\\+debian@kelar\\.org" "nnimap+debian:Archive.%Y") ("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")) (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 (to "gnumaint-reply@gnu\\.org" "l.gnumaint-reply") (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) (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$\\|:debian$\\)") (add-hook 'gnus-exit-gnus-hook #'quit-window) (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://www.kelar.org/~bandali")) (defvar bandali-csc-signature (mapconcat #'identity `(,(format bandali ", MMath") "Systems Committee " "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\\+kelar:debian.*" (address "bandali@debian.org") ("X-Message-SMTP-Method" "smtp mail-submit.debian.org 587") (gcc "nnimap+kelar:debian")) ("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"))) (define-advice gnus (:around (orig &rest args) bandali) "So we can match on Gnus's *Group* buffer in `display-buffer-alist'." (let ((switch-to-buffer-obey-display-actions t)) (apply orig args))) (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 ;; " " #'mail-abbrev-next-line " " #'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")))) ;;;; ERC (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"))) ;;;; Web (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))