init.el (34621B)
1 ;;; -*- lexical-binding: t -*- 2 3 ;;; "Custom"ization & theming. 4 5 ;; Trying to migrate to use-package instead of Custom's serialized 6 ;; forms. It's a long-term project; until that's done, start by 7 ;; setting and loading the `custom-file'. 8 (setq custom-file (file-name-concat user-emacs-directory "custom.el")) 9 (load custom-file) 10 11 ;; Compatibility shim for setopt. 12 (if (fboundp 'setopt) 13 (defalias 'my/setopt 'setopt) 14 (defmacro my/setopt (&rest pairs) 15 `(let ((pairs (quote ,pairs))) 16 (while pairs 17 (customize-set-variable (pop pairs) (pop pairs)))))) 18 19 ;; Helper for customizing list options. 20 ;; 21 ;; None of Emacs's customization tools (Custom, setopt, use-package) 22 ;; can be told "add this element, take those two away": I need to "set 23 ;; in stone" an exhaustive list that will make me (1) scratch my head 24 ;; a few months later when I try to remember which of those items I 25 ;; deliberately added vs which were part of the default list (2) miss 26 ;; out on additions to the default list, unless I cautiously audit 27 ;; every release of every package. 28 ;; 29 ;; Examples: erc-modules, git-commit-setup-hook, package-archives. 30 (defmacro my/setopt-update-list (l to-add &optional to-remove) 31 `(my/setopt ,l (thread-first 32 ,l (seq-union ,to-add) (seq-difference ,to-remove)))) 33 34 (load-theme 'eighters t) 35 36 ;;; Key bindings. 37 38 ;; Rebinding C-h, the gateway to the Self-Documenting Editor's 39 ;; defining feature, proved tricky; this is the most reliable way I 40 ;; found to consistently get C-h to do what DEL does. 41 ;; 42 ;; Likewise, C-M-h is re-bound by some major modes (CC, Python, Perl), 43 ;; so this is the simplest way I know of to make sure C-M-h sticks as 44 ;; "backward-kill-word". 45 ;; 46 ;; Same story with M-h (mark-paragraph) which gets re-bound by eg 47 ;; markdown-mode and nxml-mode. 48 (define-key input-decode-map (kbd "C-h") (kbd "DEL")) 49 (define-key input-decode-map (kbd "C-M-h") (kbd "M-DEL")) 50 51 (global-set-key (kbd "C-x C-b") 'ibuffer) 52 53 (defun my/find-user-file () 54 (interactive) 55 (let ((default-directory user-emacs-directory)) 56 (call-interactively 'find-file))) 57 58 ;; Hopefully these will be easier to remember than TeX commands: 59 60 (quail-define-package 61 "my/symbols" "UTF-8" "π°" t 62 "Input arbitrary Unicode symbols with other arbitrary symbols.") 63 64 (pcase-dolist 65 (`(,key ,translation) 66 '(;; Punctuation 67 ("..." ?β¦) 68 ("--" ?β) ("---" ?β) 69 ;; Math symbols 70 ("~~" ?β) ("~~=" ?β) ("~==" ?β ) ("~=" ?β) 71 ("==" ?β‘) ("^=" ?β) (":=" ?β) 72 ("<=" ?β€) (">=" ?β₯) 73 ("-->" ?β) ("-/>" ?β) ("==>" ?β) ("=/>" ?β) 74 ("<--" ?β) ("</-" ?β) ("<==" ?β) ("</=" ?β) 75 ("<->" ?β) ("<=>" ?β) 76 ;; Emojis 77 ("/!\\" ["β οΈ"]) ("\\o/" ?π) ("\\m/" ?π€))) 78 (quail-defrule key translation "my/symbols")) 79 80 (defmacro my/make-input-toggle (input-method) 81 (let ((fsym (intern (format "my/toggle-input-%s" input-method))) 82 ;; Unfortunately, by default `help-make-xrefs' does not try to 83 ;; cross-reference input methods, as `help-xref-mule-regexp' 84 ;; is nil. This can be worked around by setting this variable 85 ;; to `help-xref-mule-regexp-template'. 86 (doc (format "Toggle `%s' input method." input-method))) 87 `(defun ,fsym () 88 ,doc 89 (interactive) 90 ;; `current-input-method' is a string; if INPUT-METHOD is a 91 ;; symbol, neither eq, eql nor equal would return t. 92 (if (string= current-input-method ',input-method) 93 (deactivate-input-method) 94 (set-input-method ',input-method t))))) 95 96 (defun my/set-tab-width (&optional arg) 97 (interactive "P") 98 (let ((new-width (cond (arg (prefix-numeric-value arg)) 99 ((= tab-width 4) 8) 100 (4))) 101 (old-width tab-width)) 102 ;; TODO: for some reason, set-variable takes effect immediately, 103 ;; but setq(-local)? do not: I need to move the cursor before tabs 104 ;; are re-drawn. 105 (set-variable 'tab-width new-width) 106 (message "changed from %s to %s" old-width new-width))) 107 108 (defun my/auto-hscroll-toggle () 109 (interactive) 110 ;; Use indices to avoid confusing `if-let*' with nil. 111 (if-let* ((values '(nil t current-line)) 112 (prev (seq-position values auto-hscroll-mode)) 113 (next (% (1+ prev) (length values)))) 114 (progn 115 (message (concat 116 (propertize (format "%s" (nth prev values)) 'face 'shadow) 117 " β " 118 (propertize (format "%s" (nth next values)) 'face 'bold))) 119 (setopt auto-hscroll-mode (nth next values))) 120 (error "Unknown value for auto-hscroll-mode: %s" auto-hscroll-mode))) 121 122 (defun my/kill (stuff) 123 (kill-new stuff) 124 (message "%s" stuff)) 125 126 ;; TODO: my/kill-where 127 ;; * filename 128 ;; * absolute, project-relative (w/o project), namespace-relative, base 129 ;; * function 130 ;; * line number 131 ;; * public URL 132 133 ;; TODO: my/kill-cite 134 ;; * prefix: nil, >, | 135 ;; * indent 136 ;; * attribution: see my/kill-where 137 ;; * concise: "(manual) Node", "manual(7)" 138 ;; * executable: (info "(manual) Node"), "man 7 manual" 139 ;; * <https://somewhe.re/manual.html#node> 140 141 (defun my/read (prompt default) 142 (read-string (format-prompt prompt default) nil nil default)) 143 144 (defvar my/run-strip-newline t 145 "Whether `my/run' will remove a trailing newline from a command's output.") 146 147 (defun my/run (program &rest args) 148 "Return output from 'PROGRAM [ARGSβ¦]'. 149 Raise a user error if the command fails. Heed `my/run-strip-newline'." 150 (with-temp-buffer 151 (let* ((status (apply 'call-process program nil t nil args)) 152 (output (buffer-string))) 153 (if (eq status 0) 154 (if my/run-strip-newline 155 (string-remove-suffix "\n" output) 156 output) 157 (user-error "%s returned %d:\n%s" program status output))))) 158 159 (defun my/kill-command (program &rest args) 160 "Send output from PROGRAM to kill-ring. 161 See `my/run' for details, e.g. status handling and output massaging." 162 (my/kill (apply 'my/run program args))) 163 164 (defun my/kill-char (c) 165 (interactive (list (read-char-by-name "Codepoint? " t))) 166 (my/kill (string c))) 167 168 (defun my/kill-date (date format) 169 (interactive 170 (if current-prefix-arg 171 (list (my/read "Date spec?" "today") 172 (my/read "Format?" "%F")) 173 (list "today" "%F"))) 174 (my/kill-command "date" (concat "-d" date) (concat "+" format))) 175 176 (defun my/kill-filename () 177 (interactive) 178 (my/kill (or (buffer-file-name) default-directory))) 179 180 (defun my/kill-pipe-region (command) 181 (interactive (list (read-shell-command "Shell command on region: "))) 182 (let ((input (funcall region-extract-function nil))) 183 (with-temp-buffer 184 (insert input) 185 (call-process-region (point-min) (point-max) shell-file-name 186 t t nil shell-command-switch command) 187 (my/kill (buffer-string))))) 188 189 (defun my/kill-shell (command) 190 "Send output from COMMAND to kill-ring. 191 Meant for interactive prompting for full commands passed to a shell. 192 For Lisp use, prefer `my/kill-command', where arguments are passed via a 193 list and require no escaping." 194 (interactive (list (read-shell-command "Shell command: "))) 195 (with-temp-buffer 196 (call-process-shell-command command nil t) 197 (my/kill (buffer-string)))) 198 199 (defun my/shell-command-help (command) 200 (interactive 201 (list (read-shell-command "Show --help for: "))) 202 (let* ((command--help (concat command " --help")) 203 (help-buf (get-buffer-create (format "*%s*" command--help)))) 204 (shell-command (concat command--help) help-buf) 205 (display-buffer help-buf))) 206 207 (defun my/magit-project () 208 (interactive) 209 (require 'project) 210 (magit-status (project-prompt-project-dir))) 211 212 (defun my/magit-toggle-margin-date () 213 (interactive) 214 (let ((do-message 215 (lambda (old new) 216 (message 217 "%s β %s" 218 (propertize old 'face 'shadow) 219 (propertize new 'face 'bold))))) 220 (apply do-message (if magit-log-margin-show-committer-date 221 '("commit" "author") '("author" "commit"))) 222 (setq magit-log-margin-show-committer-date 223 (not magit-log-margin-show-committer-date)) 224 (revert-buffer))) 225 226 (defmacro my/define-prefix-command (name doc bindings) 227 (declare (indent defun)) 228 `(defvar ,name 229 (let ((map (define-prefix-command ',name))) 230 (pcase-dolist (`(,key ,fun) ,bindings) 231 (define-key map key fun)) 232 map) 233 ,doc)) 234 235 (my/define-prefix-command my/buffer-map 236 "Keymap for buffer manipulation commands." 237 '(("b" bury-buffer) 238 ("g" revert-buffer) 239 ("r" rename-buffer))) 240 241 (my/define-prefix-command my/display-map 242 "Keymap for display-related commands." 243 '(("h" my/auto-hscroll-toggle) 244 ("l" hl-line-mode) 245 ("n" display-line-numbers-mode) 246 ("t" toggle-truncate-lines) 247 ("v" visual-line-mode))) 248 249 (my/define-prefix-command my/editing-map 250 "Keymap for toggling editing features." 251 '(("f" auto-fill-mode))) 252 253 (my/define-prefix-command my/find-map 254 "Keymap for finding things." 255 '(("u" my/find-user-file))) 256 257 (my/define-prefix-command my/magit-map 258 "Keymap for Magit commands." 259 '(("d" my/magit-toggle-margin-date) 260 ("f" magit-file-dispatch) 261 ("g" magit-status) 262 ("p" my/magit-project) 263 ("x" magit-dispatch) 264 ("\C-f" magit-find-file))) 265 266 (my/define-prefix-command my/input-map 267 "Keymap for input methods shortcuts." 268 `(("e" ,(my/make-input-toggle emoji)) 269 ("t" ,(my/make-input-toggle TeX)) 270 ("u" ,(my/make-input-toggle my/symbols)))) 271 272 (my/define-prefix-command my/kill-map 273 "Keymap for adding things to the kill ring (or system clipboard)." 274 '(("C" my/kill-char) 275 ("d" my/kill-date) 276 ("f" my/kill-filename) 277 ("W" my/kill-as-html) 278 ("|" my/kill-pipe-region) 279 ("!" my/kill-shell))) 280 281 (my/define-prefix-command my/manual-map 282 "Keymap for reading manuals." 283 '(("h" my/shell-command-help) 284 ("i" info-display-manual) 285 ("m" man) 286 ("s" shortdoc-display-group))) 287 288 (my/define-prefix-command my/whitespace-map 289 "Keymap for whitespace-related commands." 290 '(("c" whitespace-cleanup) 291 ("m" whitespace-mode) 292 ("t" my/set-tab-width))) 293 294 ;; C-c [[:alpha:]] is reserved for users - let's make good use of it. 295 296 (global-set-key (kbd "C-c b") 'my/buffer-map) 297 (global-set-key (kbd "C-c c") 'compile) 298 (global-set-key (kbd "C-c d") 'my/display-map) 299 (global-set-key (kbd "C-c e") 'my/editing-map) 300 (global-set-key (kbd "C-c f") 'my/find-map) 301 (global-set-key (kbd "C-c g") 'my/magit-map) 302 (global-set-key (kbd "C-c i") 'my/input-map) 303 (global-set-key (kbd "C-c k") 'my/kill-map) 304 (global-set-key (kbd "C-c m") 'my/manual-map) 305 (global-set-key (kbd "C-c w") 'my/whitespace-map) 306 307 (rg-enable-default-bindings) ; Uses the C-c s prefix. 308 309 ;; What's life without a little risk? 310 (setq disabled-command-function nil) 311 312 ;;; Window management. 313 314 ;; Bindings ala Terminator 315 (when window-system 316 (global-set-key (kbd "C-S-o") 'split-window-below) 317 (global-set-key (kbd "C-S-e") 'split-window-right) 318 (global-set-key (kbd "C-+") 'text-scale-adjust) 319 (global-set-key (kbd "C--") 'text-scale-adjust) 320 (global-set-key (kbd "C-0") 'text-scale-adjust) 321 (global-set-key (kbd "C-S-<up>") 'enlarge-window) 322 (global-set-key (kbd "C-S-<down>") 'shrink-window) 323 (global-set-key (kbd "C-S-<right>") 'enlarge-window-horizontally) 324 (global-set-key (kbd "C-S-<left>") 'shrink-window-horizontally)) 325 326 ;;; Lighters. 327 328 ;; So long, Will Mengarini. 329 (delight 'abbrev-mode nil 'abbrev) 330 (delight 'auto-fill-function "β" t) 331 (delight 'auto-revert-mode "β³" 'autorevert) 332 (delight 'auto-revert-tail-mode "β€" 'autorevert) 333 (delight 334 'flyspell-mode (propertize "ποΈ" 'face 'flyspell-incorrect) 'flyspell) 335 (delight 'hi-lock-mode nil 'hi-lock) 336 (delight 'hs-minor-mode "β¦" 'hideshow) 337 (delight 'mml-mode "π§" 'mml) 338 (delight 'scroll-lock-mode "π" 'scroll-lock) 339 (delight 'text-scale-mode 340 '(:eval (if (>= text-scale-mode-amount 0) "π" "π")) 341 'face-remap) 342 (delight 'visual-line-mode nil t) 343 (delight 'with-editor-mode "βΈ" 'with-editor) 344 ;; TODO: Narrow (β, βΆ) 345 346 (let* ((indicator (alist-get 'compilation-in-progress mode-line-modes)) 347 (old-props (text-properties-at 0 (car indicator))) 348 (face '(:inverse-video t :inherit compilation-mode-line-run)) 349 (new-props (append `(face ,face) old-props)) 350 (icon "βοΈ")) 351 (setcar indicator (concat (apply #'propertize icon new-props) " "))) 352 353 (setq eglot-menu-string "π¦»") 354 355 (with-eval-after-load 'flymake 356 (let ((indicator (propertize "βοΈ" 'face 'flymake-error))) 357 ;; Prefer customizing the string instead delight'ing, as flymake 358 ;; slaps a bunch of helpful properties on top of the lighter, 359 ;; which delight would strip. 360 (if (boundp 'flymake-mode-line-lighter) 361 (setq flymake-mode-line-lighter indicator) 362 (delight 'flymake-mode indicator 'flymake)))) 363 364 ;;; Version control. 365 366 (defvar my/git-commit-fill-columns 367 '((my/emacs-repo-p . 63))) 368 369 (defun my/git-upstreams () 370 ;; TODO: memoize, perhaps? 371 (seq-uniq 372 (seq-keep 373 (lambda (remote-desc) 374 (and (string-match "\\`.*\t\\(.*\\) (fetch)\\'" remote-desc) 375 (match-string 1 remote-desc))) 376 (process-lines "git" "remote" "-v")))) 377 378 (cl-defun my/git-commit-maybe-set-fill-column () 379 (let ((remotes (my/git-upstreams))) 380 (pcase-dolist (`(,pred . ,column) my/git-commit-fill-columns) 381 (when (funcall pred remotes) 382 (cl-return-from my/git-commit-maybe-set-fill-column 383 (setq fill-column column)))))) 384 385 (defvar my/git-commit-use-changelog 386 (list 'my/emacs-repo-p)) 387 388 (defun my/git-commit-maybe-set-changelog-support () 389 (let ((remotes (my/git-upstreams))) 390 (when (run-hook-with-args-until-success 391 'my/git-commit-use-changelog remotes) 392 (git-commit-setup-changelog-support)))) 393 394 (defun my/revision-at-point () 395 (cond 396 ((derived-mode-p 'magit-mode) 397 (magit-branch-or-commit-at-point)) 398 ((derived-mode-p 'vc-git-log-view-mode) 399 (log-view-current-tag)) 400 ((derived-mode-p 'vc-annotate-mode) 401 (car (vc-annotate-extract-revision-at-line))))) 402 403 (defun my/describe-revision (rev) 404 "Format a Git revision in a format suitable for changelogs." 405 (interactive 406 (list (my/read "Revision" (my/revision-at-point)))) 407 (my/kill-command 408 "git" "show" "--no-patch" "--date=short" "--format=%cd \"%s\" (%h)" rev)) 409 410 ;;; Major modes configuration. 411 412 (defun my/c-modes-hook () 413 (c-set-style "bsd") 414 (c-set-offset 'arglist-close 0)) 415 416 (add-hook 'c-mode-common-hook 'my/c-modes-hook) 417 418 (defun my/calendar-iso-week (year month day) 419 ;; NIH version of `calendar-intermonth-text''s serving suggestion. 420 (propertize 421 (format-time-string "%V" (encode-time (list 0 0 0 day month year))) 422 'font-lock-face 'eighters-date)) 423 424 (defun my/compilation-notify (buffer results) 425 (require 'notifications) 426 (notifications-notify 427 :title (buffer-name buffer) 428 :body results 429 :app-icon (if (equal results "finished\n") 430 'compilation-success 'compilation-failure) 431 :timeout 3000)) 432 433 (add-to-list 'compilation-finish-functions 'my/compilation-notify) 434 435 (defun my/make-tabless (f) 436 "Make a function which will run F with `indent-tabs-mode' disabled." 437 (lambda () 438 (:documentation (format "Run `%s' with `indent-tabs-mode' set to nil." f)) 439 (interactive) 440 (let ((indent-tabs-mode nil)) 441 (call-interactively f)))) 442 443 (defun my/makefile-hook () 444 ;; I would rather align backslashes with spaces rather than tabs; 445 ;; however, I would also like indent-tabs-mode to remain non-nil. 446 (local-set-key (kbd "C-c C-\\") (my/make-tabless 'makefile-backslash-region)) 447 (local-set-key (kbd "M-q") (my/make-tabless 'fill-paragraph))) 448 449 (add-hook 'makefile-mode-hook 'my/makefile-hook) 450 451 (defun my/shell-hook () 452 (setq truncate-lines nil) 453 (setq-local recenter-positions '(top middle bottom))) 454 455 (add-to-list 'ibuffer-saved-filter-groups 456 '("my/ibuffer-groups" 457 ("REPL" 458 (or (derived-mode . comint-mode) 459 (mode . lisp-interaction-mode))) 460 ("Programming" (derived-mode . prog-mode)) 461 ("Folders" (mode . dired-mode)) 462 ("Messaging" 463 (or (mode . erc-mode) 464 (mode . message-mode) 465 (derived-mode . gnus-mode))) 466 ("Documentation" 467 (or (mode . Info-mode) 468 (mode . Man-mode) 469 (mode . help-mode))) 470 ("Version control" 471 (or (derived-mode . magit-mode) 472 (name . "\\`\\*vc"))))) 473 474 (add-hook 'ibuffer-mode-hook 475 (lambda () 476 (ibuffer-switch-to-saved-filter-groups "my/ibuffer-groups"))) 477 478 ;;; Development helpers. 479 (defun my/emacs-repo-p (upstreams) 480 "Guess whether we are working in the Emacs repository. 481 UPSTREAMS is a list of fetch URLs." 482 (member "https://git.savannah.gnu.org/git/emacs.git" upstreams)) 483 484 (defun my/emacs-run-testcase () 485 (interactive) 486 (require 'which-func) 487 (let* ((emacs-root (project-root (project-current))) 488 (testfile (file-name-sans-extension 489 (file-relative-name 490 buffer-file-name (file-name-concat 491 emacs-root "test")))) 492 (cores (num-processors 'all)) 493 (options 494 `(("SELECTOR" . ,(which-function)) 495 ("TEST_BACKTRACE_LINE_LENGTH" . nil))) 496 (options-list 497 (seq-map 498 (lambda (opt) (format "%s=%s" (car opt) (cdr opt))) 499 options)) 500 (compile-command 501 (format "make -j%s && make -C test %s %s" 502 cores testfile (string-join options-list " ")))) 503 (call-interactively 'project-compile))) 504 505 ;;; Helper functions and miscellaneous settings. 506 507 ;;;; French quick toggle. 508 (defun my/froggify () 509 (ispell-change-dictionary "fr") 510 (setq-local 511 colon-double-space nil 512 sentence-end-double-space nil 513 fill-nobreak-predicate (cons 'fill-french-nobreak-p fill-nobreak-predicate))) 514 515 (defun my/unfroggify () 516 (ispell-change-dictionary "default") 517 (setq-local 518 colon-double-space t 519 sentence-end-double-space t 520 fill-nobreak-predicate (remq 'fill-french-nobreak-p fill-nobreak-predicate))) 521 522 (define-minor-mode my/frog-mode 523 "Croak like a froggy." 524 :init-value nil 525 :lighter "πΈ" 526 (if my/frog-mode 527 (my/froggify) 528 (my/unfroggify))) 529 530 (defalias 'my/croak 'my/frog-mode) 531 532 ;;;; Mailing lists utilities. 533 (defun my/kill-message-id () 534 (interactive) 535 (my/kill (mail-header-message-id (gnus-summary-article-header)))) 536 537 (defun my/describe-message (id url) 538 (my/kill (format "%s\n%s\n" 539 (if (string-prefix-p "<" id) 540 id 541 (format "<%s>" id)) 542 url))) 543 544 (defun my/describe-message-id (list id) 545 "Format references from the Message-ID of a gnu.org list." 546 (interactive 547 (list 548 (read-string "List: ") ; TODO: default to current list. 549 (let ((default-id (mail-header-message-id 550 (gnus-summary-article-header)))) 551 (my/read "Message-ID" default-id)))) 552 (with-current-buffer 553 (url-retrieve-synchronously 554 (concat 555 ;; For some reason, literal "+" chars cause the search to fail. 556 ;; Escape them. 557 "https://lists.gnu.org/archive/cgi-bin/namazu.cgi" 558 "?query=%2Bmessage-id:" 559 (replace-regexp-in-string "\\+" "%2B" id) 560 "&submit=Search!" 561 "&idxname=" list)) 562 (search-forward-regexp 563 (rx "<a href=\"" 564 (group "/archive/html/" (literal list) "/" 565 (+ (any "0-9-")) "/msg" (+ (any "0-9")) ".html") 566 "\">")) 567 (let ((url (concat "https://lists.gnu.org" (match-string 1)))) 568 (my/describe-message id url)))) 569 570 (defun my/describe-message-url (url) 571 "Format references from an article archived on MHonArc." 572 (interactive 573 (list 574 (let ((default (or (thing-at-point 'url) 575 (and (derived-mode-p 'eww-mode) 576 (shr-url-at-point nil))))) 577 (read-string (format-prompt "URL" default) nil nil default)))) 578 (with-current-buffer (url-retrieve-synchronously url) 579 (search-forward-regexp "^<!--X-Message-Id: \\(.+\\) -->$") 580 (let ((id (xml-substitute-numeric-entities (match-string 1)))) 581 (my/describe-message id url)))) 582 583 ;;;; Frame title. 584 585 (defun my/project-root () 586 (and-let* ((project (project-current))) 587 (project-root project))) 588 589 (defun my/project-name () 590 (and-let* ((root (my/project-root)) 591 ;; Home is under VC to track dotfile changes. Not a 592 ;; "project" I want shown in the UI though. 593 ((not (file-equal-p root "~")))) 594 (file-name-nondirectory (directory-file-name root)))) 595 596 (defun my/connection-name () 597 (let ((method (file-remote-p default-directory 'method))) 598 (pcase method 599 ;; No method: nil. 600 ('nil method) 601 ;; sudo(edit): just "METHOD". 602 ((pred (string-match-p "sudo")) method) 603 ;; Default: "METHOD:HOST". 604 (_ (format "%s:%s" method (file-remote-p default-directory 'host)))))) 605 606 (defun my/frame-title-format () 607 (let ((prefix 608 ;; Messing with match data during redisplay is dangerous 609 ;; (cf. bug#33697). 610 (save-match-data 611 ;; For some reason, calling filename-parsing functions 612 ;; while TRAMP is busy opens the gates to Infinite 613 ;; Minibuffer Recursion Hell. Cautiously side-step that. 614 (or 615 (my/connection-name) 616 (my/project-name))))) 617 (concat (when prefix (format "[%s] " prefix)) 618 "%b"))) 619 620 (setq frame-title-format '(:eval (my/frame-title-format))) 621 622 ;;;; Clipboard interaction. 623 (defun my/kill-as-html (text markup) 624 (interactive 625 (list (buffer-substring (region-beginning) (region-end)) 626 (or (alist-get major-mode '((markdown-mode . "markdown") 627 (org-mode . "org") 628 (rst-mode . "rst"))) 629 (let ((default "plain")) 630 (read-string (format-prompt "Convert from:" default) 631 nil nil default))))) 632 ;; TODO: make this a transient to easily (un)set pandoc extensions. 633 (with-temp-buffer 634 (call-process-region text nil "pandoc" nil t nil 635 "--from" markup "--to" "html") 636 ;; TODO: could `gui-set-selection' help here? The docstring makes 637 ;; it sound like passing a value with a 'text/html property set to 638 ;; the HTML string should work, but empirically it doesn't. 639 ;; Maybe look into `selection-converter-alist'. 640 (call-process-region 641 nil nil "wl-copy" nil 0 nil "--type" "text/html") 642 (message "Copied:\n\n%s" (buffer-string)))) 643 644 (defun my/yank-from-html (html markup) 645 (interactive 646 (list 647 (gui-get-selection 'CLIPBOARD 'text/html) 648 (or (alist-get major-mode '((markdown-mode . "markdown") 649 (org-mode . "org") 650 (rst-mode . "rst"))) 651 (let ((default "plain")) 652 (read-string (format-prompt "Convert to:" default) 653 nil nil default))))) 654 ;; TODO: make this a transient to easily (un)set 655 ;; * extensions 656 ;; * switches (--wrap) 657 ;; * filters (remove all attributes) 658 (let* ((disabled-html-extensions (list 659 "native_divs" 660 "native_spans" 661 )) 662 (disabled-markup-extensions (list 663 ;; "smart" 664 )) 665 (html-spec 666 (funcall 'string-join `("html" ,@disabled-html-extensions) "-")) 667 (markup-spec 668 (funcall 'string-join `(,markup ,@disabled-markup-extensions) "-"))) 669 (call-process-region html nil "pandoc" nil t t 670 "--wrap=none" 671 "--from" html-spec "--to" markup-spec))) 672 673 ;;;; Miscellany. 674 (setq-default paragraph-start (concat "[ ]*- \\|" paragraph-start)) 675 676 (defun my/screenshot (output) 677 (interactive 678 (list 679 (let ((default (format-time-string "/tmp/Emacs-Screenshot-%F-%T.pdf"))) 680 (read-file-name (format-prompt "Output?" default) nil default)))) 681 (let ((data (x-export-frames)) 682 (buf (find-file output))) 683 (insert data) 684 (save-buffer) 685 (kill-buffer buf))) 686 687 ;; Trying out use-package. 688 689 (use-package use-package 690 :custom 691 (use-package-always-defer t)) 692 693 (use-package package 694 :custom 695 (package-selected-packages 696 (append '(auctex 697 debbugs 698 delight 699 diff-hl 700 elisp-benchmarks 701 forge 702 gnus-mock 703 magit 704 markdown-mode 705 rg 706 rust-mode 707 wgrep) 708 (and (< emacs-major-version 29) 709 '(eglot use-package)) 710 (and (< emacs-major-version 30) 711 '(which-key)))) 712 :config 713 (my/setopt-update-list 714 package-archives '(("melpa" . "https://melpa.org/packages/")))) 715 716 (use-package emacs 717 :custom 718 (auto-hscroll-mode 'current-line) 719 ;; Disabled until bug#56662 is solved. Prefer `hl-line-mode'. 720 ;; (highlight-nonselected-windows t) 721 ) 722 723 (use-package calendar 724 :custom 725 (calendar-intermonth-text '(my/calendar-iso-week year month day)) 726 (calendar-today-visible-hook '(calendar-mark-today)) 727 (calendar-week-start-day 1)) 728 729 (use-package diff-hl 730 :custom 731 (diff-hl-flydiff-mode t) 732 (global-diff-hl-mode t) 733 734 ;; FIXME: Adding to these hooks _here_ clobbers them, i.e. they end 735 ;; up containing (a) the diff-hl functions (b) whatever functions 736 ;; their libraries add dynamically (c) *none* of the functions 737 ;; included in the defcustom's default value. 738 ;; 739 ;; Therefore, set these hooks up in the :config form _for the 740 ;; libraries that define these hooks_, so that (presumably) the 741 ;; default values for these hooks are loaded *before* adding the 742 ;; diff-hl functions. 743 ;; 744 ;; :hook 745 ;; ((dired-mode . diff-hl-dired-mode-unless-remote) 746 ;; (magit-pre-refresh . diff-hl-magit-pre-refresh) 747 ;; (magit-post-refresh . diff-hl-magit-post-refresh)) 748 ) 749 750 (use-package dired 751 :custom 752 (dired-kill-when-opening-new-dired-buffer t) 753 (dired-listing-switches "-al -Fhv --group-directories-first") 754 :config 755 (add-hook 'dired-mode-hook 'diff-hl-dired-mode-unless-remote)) 756 757 (use-package dired-aux 758 :custom 759 (dired-vc-rename-file t)) 760 761 (use-package ediff 762 :custom 763 (ediff-merge-split-window-function 'split-window-vertically) 764 (ediff-split-window-function 'split-window-horizontally) 765 (ediff-window-setup-function 'ediff-setup-windows-plain)) 766 767 (use-package eldoc 768 :delight "π") 769 770 (use-package erc 771 :custom 772 (erc-log-channels-directory 773 (concat user-emacs-directory "erc/logs")) 774 (erc-log-write-after-insert t) 775 (erc-log-write-after-send t) 776 (erc-notifications-icon 777 (concat data-directory "images/icons/hicolor/scalable/apps/emacs.svg")) 778 (erc-prompt-for-nickserv-password nil) 779 (erc-prompt-for-password nil) 780 (erc-use-auth-source-for-nickserv-password t) 781 (erc-user-full-name 'user-full-name) 782 ;; Timestamps are a mess. 783 ;; 784 ;; The default `left-and-right' tries to keep timestamps flush right 785 ;; either with hard-spacing or with :align-to; both cause jank when 786 ;; splitting windows or rescaling faces. The default `left' does 787 ;; not do the separate-date-and-time thing. 788 ;; 789 ;; It may be possible to define my own function to do the 790 ;; date-if-changed-then-time-if-changed thing, but that would 791 ;; require a lot of cargo-culting of erc-stamp.el which, as of 792 ;; 30.0.50, makes this look more complex than I have patience for: 793 ;; an obsolete variable (`erc-stamp-prepend-date-stamps-p'), an 794 ;; internal minor mode (`erc-stamp--date-mode'), lots of text 795 ;; properties ('field, 'invisible)β¦ 796 ;; 797 ;; The options below seem like the least bad compromise, even though 798 ;; they yield a huge left margin interrupted by continuation lines; 799 ;; `erc-fill-wrap' _should_ help with those, except it causes 800 ;; impromptu recentering. `visual-wrap' could help here. 801 (erc-insert-timestamp-function 'erc-insert-timestamp-left) 802 (erc-timestamp-format "[%F %H:%M] ") 803 :config 804 (my/setopt-update-list erc-modules '(log notifications stamp track) '(fill)) 805 (my/setopt-update-list erc-track-exclude-types '("JOIN" "PART" "QUIT"))) 806 807 (use-package find-dired 808 :custom 809 (find-ls-option 810 ;; The default value (-default-ls) relies on 'find -ls', which 811 ;; escapes non-ASCII chars. 812 ;; The suggested alternatives (-default-exec, -default-xargs) 813 ;; _display_ non-ASCII chars correctly, but ls quotes filenames 814 ;; that contain spaces: `dired-get-filename' fails to strip them, 815 ;; so they cannot be visited. 816 ;; So go for -default-exec, *plus* -N to disable quoting 817 ;; altogether. That takes care of all the edge cases I tested 818 ;; (non-ASCII chars, spaces). 819 '("-exec ls -Nld {} +" . "-Nld"))) 820 821 (use-package forge 822 ;; Auto-load after Magit, to ensure `f n' works. 823 :after magit 824 ;; We have `use-package-always-defer' set, so `:after' does nothing 825 ;; unless we also set `:demand' (xref GH#572): 826 :demand t) 827 828 (use-package generic-x 829 :demand t 830 :custom 831 (generic-extras-enable-list 832 '(etc-fstab-generic-mode 833 etc-modules-conf-generic-mode 834 etc-passwd-generic-mode 835 etc-services-generic-mode 836 etc-sudoers-generic-mode 837 hosts-generic-mode 838 pkginfo-generic-mode 839 resolve-conf-generic-mode 840 x-resource-generic-mode))) 841 842 (use-package git-commit 843 :config 844 (my/setopt-update-list 845 git-commit-setup-hook 846 '(git-commit-turn-on-flyspell 847 my/git-commit-maybe-set-fill-column 848 my/git-commit-maybe-set-changelog-support) 849 '(git-commit-setup-changelog-support))) 850 851 (use-package gnus 852 :custom 853 ;; Only set file locations here; let gnus-init-file do the heavy 854 ;; lifting. 855 (gnus-home-directory (file-name-concat user-emacs-directory "gnus")) 856 (gnus-init-file (file-name-concat user-emacs-directory "gnus" "init.el"))) 857 858 (use-package isearch 859 :delight "π" 860 :custom 861 (isearch-allow-scroll t) 862 (isearch-lazy-count t) 863 (search-default-mode 'char-fold-to-regexp)) 864 865 (use-package magit 866 :init 867 (setq magit-bind-magit-project-status nil) 868 :custom 869 (magit-define-global-key-bindings nil) 870 (magit-diff-refine-hunk t) 871 (magit-ediff-dwim-show-on-hunks t) 872 (magit-process-apply-ansi-colors t) 873 (magit-revision-show-gravatars t) 874 :config 875 ;; See `diff-hl' form for rationale. 876 (add-hook 'magit-pre-refresh-hook 'diff-hl-magit-pre-refresh) 877 (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh)) 878 879 (use-package magit-blame 880 :delight "π") 881 882 (use-package markdown-mode 883 :custom 884 (markdown-asymmetric-header t) 885 (markdown-command "pandoc -s") 886 (markdown-enable-math t) 887 (markdown-header-scaling t) 888 (markdown-indent-on-enter 'indent-and-new-item)) 889 890 (use-package message 891 :custom 892 (message-confirm-send t)) 893 894 ;; TODO: completion-eager-*? 895 ;; TODO: completion-styles bindings, e.g. completion-ignore-case? 896 ;; TODO: completion-category-overrides: motivation for multiple behaviors? 897 ;; TODO: mct? 898 ;; 899 ;; Gripes: 900 ;; - underused keys: C-M-i, C-j 901 ;; - (minibuffer-)choose-completion ignore completion-no-auto-exit 902 ;; when the candidate is a directory: the candidate is inserted in 903 ;; the minibuffer and the user does *not* exit the minibuffer. 904 ;; 905 ;; In minibuffer: 906 ;; - TAB complete, or show/update completions 907 ;; - TABΒ² jump to completions 908 ;; - C-M-n, C-M-p highlight candidate (without changing minibuffer) 909 ;; - RET, C-j accept minibuffer input 910 ;; - M-RET accept highlighted candidate 911 ;; - C-u M-RET insert highlighted candidate (without accepting) 912 ;; 913 ;; In completions: 914 ;; - n, TAB, p highlight candidate (without changing minibuffer) 915 ;; - RET accept highlighted candidate 916 ;; - C-u RET insert highlighted candidate in minibuffer (without accepting) 917 ;; - C-g, q back to minibuffer 918 (use-package minibuffer 919 :config 920 (setq completion-ignore-case t) 921 ;; TODO: tuck all "compat keys" in one spot; as a minor mode? 922 ;; (i.e. alternatives to <left|right|up|down> for 923 ;; minibuffer-*-completion, *-buffer, *window*-map) 924 (define-key completion-in-region-mode-map (kbd "C-M-n") 'minibuffer-next-completion) 925 (define-key completion-in-region-mode-map (kbd "C-M-p") 'minibuffer-previous-completion) 926 (define-key minibuffer-mode-map (kbd "C-M-n") 'minibuffer-next-completion) 927 (define-key minibuffer-mode-map (kbd "C-M-p") 'minibuffer-previous-completion) 928 :custom 929 (completion-auto-help 'visible) 930 (completion-auto-select 'second-tab) 931 (completion-pcm-leading-wildcard t) 932 (completion-show-help nil) 933 (completions-detailed t) 934 (completions-format 'one-column) 935 (completions-group t) 936 (completions-max-height 10) 937 (minibuffer-completion-auto-choose nil) 938 (read-buffer-completion-ignore-case t) 939 (read-file-name-completion-ignore-case t)) 940 941 (use-package org 942 :config 943 (when (version< org-version "9.4") 944 (define-key org-mode-map (kbd "C-j") 'org-return) 945 (define-key org-mode-map (kbd "RET") 'org-return-indent)) 946 :custom 947 (org-edit-src-content-indentation 0) 948 (org-ellipsis "β¦") 949 (org-fontify-done-headline nil) 950 (org-fontify-quote-and-verse-blocks t) 951 (org-goto-interface 'outline-path-completion) 952 (org-startup-indented t) 953 (org-use-extra-keys t) 954 (org-use-speed-commands t) 955 ;; Make org-refile a bit more eager. 956 (org-outline-path-complete-in-steps nil) 957 (org-refile-targets '((nil . (:maxlevel . 10)))) 958 (org-refile-use-outline-path t)) 959 960 (use-package org-indent 961 :delight "Β»") 962 963 (use-package paren 964 :custom 965 (show-paren-mode t) 966 (show-paren-predicate t)) 967 968 (use-package python 969 :custom 970 (python-fill-docstring-style 'pep-257-nn) 971 (python-forward-sexp-function nil) 972 (python-indent-def-block-scale 1)) 973 974 (use-package shell 975 :config 976 (setq shell-font-lock-keywords nil) 977 (add-hook 'shell-mode-hook 'my/shell-hook)) 978 979 (use-package shr 980 :custom 981 ;; Prefer visual-line-mode, which refills text automatically when 982 ;; the window width changes. 983 (shr-fill-text nil)) 984 985 (use-package which-key 986 :custom 987 (which-key-dont-use-unicode nil) 988 (which-key-idle-delay 0.5) 989 (which-key-mode t) 990 :delight) 991 992 (use-package whitespace 993 :config 994 (my/setopt-update-list whitespace-style nil '(lines missing-newline-at-eof)) 995 :delight 996 ;; FIXME: without :demand t, enabling whitespace-mode in a diff 997 ;; buffer first causes diff-mode's settings to be applied globally. 998 :demand t) 999 1000 ;;; TODO: 1001 ;; * decruftify mode-line (e.g. remove superflous parens). 1002 ;; * teach some modes to give better names to their buffers to reduce 1003 ;; clobbering: info, occur