/ mrb.org
mrb.org
1 #+SETUPFILE: "org-config.org" 2 #+TITLE: My org-babel based emacs configuration 3 #+LANGUAGE: en 4 #+OPTIONS: H:5 toc:nil creator:nil email:nil author:t timestamp:t tags:nil tex:verbatim 5 #+PROPERTY: header-args :results silent :noweb no-export 6 7 Last update: call_last_modified() 8 9 This is my emacs configuration file that is loaded with =org-babel-load-file= in the Emacs init file. The intent is to have as much of my Emacs configuration in here as possible. The system works as a literal programming system where with a tangle the elisp code that actually makes up my configuration is extracted automatically and loaded. 10 11 This document is irregularly published and lives in different locations: 12 13 - [[https://codeberg.org/mrb/emacs-config][config sources are available on codeberg]] 14 - [[https://qua.name/mrb/an-org-babel-based-emacs-configuration][published on my writefreely instance qua.name]] 15 - [[rad:z4Hkna53NPf7wFn6QJ8BcajVvG9do][published as a radicle repository]] 16 17 #+TOC: headlines 4 18 * Files for configuration 19 Next to this main configuration file (=mrb.org=), two other files are involved in the configuration: 20 21 1. =early-init.el= emacs' early initialization file; 22 #+INCLUDE: early-init.el src emacs-lisp 23 24 2. =init.el= the main init file, which is used to generate =mrb.el= from =mrb.org=. 25 #+INCLUDE: init.el src emacs-lisp 26 27 Ideally these files should be produced by org-babel as well, just like the main configuration, but that generates a dependency because these two files need to be available from the beginning. 28 29 Small snippet to have last modified timestamp inserted when exporting this file (see line above which has the call to =last_modified=) 30 31 #+NAME: last_modified 32 #+BEGIN_SRC sh :var emacs_dir=(expand-file-name user-emacs-directory) 33 git log -1 --pretty="format:%ci" "$emacs_dir/mrb.org" 34 #+END_SRC 35 36 * Preparing for lift-off 37 The first thing I want to take care of is to make customization possible and stored in a place to my liking. I want to load this first so anything in the configuration I define explicitly overrides it. The custom file is really only for =safe-local-variables= support; variables that get set automtically by reading a =.dir-locals.el= file for example. All actual persistent configuration goes in this file only. 38 39 #+begin_src emacs-lisp 40 (defun mrb/org-file (relative-path) 41 "Expand RELATIVE-PATH relative to org-directory." 42 (expand-file-name relative-path org-directory)) 43 44 (defun mrb/emacs-file (relative-path) 45 "Expand RELATIVE-PATH relative to user-emacs-directory." 46 (expand-file-name relative-path user-emacs-directory)) 47 48 (defun mrb/home-file (relative-path) 49 "Expand RELATIVE-PATH relative to the user's home directory." 50 (expand-file-name relative-path (getenv "HOME"))) 51 52 (setq custom-file (mrb/emacs-file "custom.el")) 53 (load custom-file) 54 55 (defun mrb/clean-custom-file (custom-file-path) 56 "Clean CUSTOM-FILE-PATH by removing auto-generated variables, keeping safe-local-variable-values. 57 Parses the file as Lisp, extracts the safe-local-variable-values entry, 58 and writes it back (or a fallback if not found). Skips cleanup on parse error." 59 (when (file-exists-p custom-file-path) 60 (let ((safe-entry nil) 61 (parse-ok t)) 62 ;; Parse custom.el as Lisp and extract safe-local-variable-values 63 (with-temp-buffer 64 (insert-file-contents custom-file-path) 65 (goto-char (point-min)) 66 (let (forms) 67 (condition-case err 68 (while t (push (read (current-buffer)) forms)) 69 (end-of-file nil) 70 (error 71 (lwarn 'mrb :warning "custom.el parse error: %S — skipping cleanup" err) 72 (setq parse-ok nil))) 73 (when parse-ok 74 (let* ((cvars-form (cl-find 'custom-set-variables forms :key #'car)) 75 (entries (cdr cvars-form))) 76 (setq safe-entry 77 (cl-find 'safe-local-variable-values entries 78 :key (lambda (e) (caadr e)))))))) 79 ;; Write back only safe-local-variable-values 80 (when parse-ok 81 (with-temp-file custom-file-path 82 (insert ";;;; Custom file for safe-local-variable approvals only\n") 83 (insert ";; Auto-generated variables removed by mrb/clean-custom-file.\n") 84 (insert ";; All face and configuration customizations are in mrb.org.\n\n") 85 (insert "(custom-set-variables\n ") 86 (insert (if safe-entry 87 (pp-to-string safe-entry) 88 "'(safe-local-variable-values\n '())")) 89 (insert ")\n")))))) 90 91 (add-hook 'after-init-hook (lambda () (mrb/clean-custom-file custom-file))) 92 #+end_src 93 94 * X Resources 95 X resource settings for Emacs are generated and loaded here. This keeps display configuration 96 in sync with the elisp configuration (fonts, colors, widget styling). 97 98 #+begin_src fundamental :tangle ~/.Xdefaults.d/emacs :exports code 99 ! Emacs X Resources 100 ! Generated from mrb.org - do not edit manually 101 ! Reload with: xrdb -merge ~/.Xdefaults.d/emacs 102 103 ! Font configuration (synced with base16-adjustments default face) 104 ! Height 112 at 120 DPI ≈ pixelsize=19 105 Emacs.font: Hack-11:pixelsize=19 106 107 ! Menu bar and tool bar 108 Emacs.menuBar: off 109 Emacs.toolBar: off 110 111 ! Menu and dialog styling (Nord theme colors) 112 Emacs.menu*.background: #5E81AC 113 Emacs.menu*.foreground: #D8DEE9 114 Emacs.dialog*.background: #5E81AC 115 Emacs.dialog*.foreground: #D8DEE9 116 Emacs.pane.menubar.background: #5E81AC 117 Emacs.pane.menubar.foreground: #D8DEE9 118 119 ! Widget appearance 120 Emacs*shadowThickness: 1 121 #+end_src 122 123 After tangling, load the generated file: 124 125 #+begin_src emacs-lisp 126 (when (display-graphic-p) 127 (call-process "xrdb" nil nil nil "-merge" 128 (mrb/home-file ".Xdefaults.d/emacs"))) 129 #+end_src 130 131 The config file =mrb.org= gets opened *a lot* because I tend to fiddle with this config at least once a day. So, it warrants it's own keybinding, but it will have to wait until [[Key bindings]] before we can use the =bind-key= package. 132 133 #+begin_src emacs-lisp 134 (defun mrb/open-config () 135 (interactive) 136 (find-file config-file)) 137 #+end_src 138 139 Also, it helps to have a config-reload option sometimes. 140 141 #+begin_src emacs-lisp 142 (defun mrb/reload-config () 143 "Reload init file, which will effectively reload everything" 144 (interactive) 145 (load-file (mrb/emacs-file "init.el"))) 146 147 (defun mrb/check-emacs-version-support (required-version message) 148 "Warn if current Emacs version is below REQUIRED-VERSION (e.g., \"29.1\"). 149 MESSAGE describes the unsupported feature. Supports formats: x, x.y, x.y.z. 150 REQUIRED-VERSION can be a string or integer; integers are converted to strings." 151 (let ((version-str (if (stringp required-version) 152 required-version 153 (number-to-string required-version)))) 154 (when (version< emacs-version version-str) 155 (warn "Emacs %s: %s (requires Emacs %s+)" 156 emacs-version message version-str)))) 157 #+end_src 158 159 Most of the time the editing process starts at the command line. Because I use emacs in server mode, this needs a little script. The script does two things: 160 1. check if we already have the emacs server, if not, start it; 161 2. treat input from stdin a little bit special. 162 163 While we are here, define a snippet which should go in shell scripts. We will repeat this per language at some point. 164 165 #+name: tangle-header 166 #+begin_src sh :tangle no 167 [ Org-mode generated this file from a code block, changes will be overwritten ] 168 #+end_src 169 170 And edit the file obviously. 171 172 #+begin_src sh :exports code :tangle ~/bin/edit :shebang #!/bin/bash 173 # <<tangle-header>> 174 # Wrapper for emacs-client usage to handle specific cases: 175 # - no filename given 176 # - stdin editing 177 # - force tty 178 # - adjustment to systemd invocation 179 180 # Set to false when not debugging 181 debug() { false $@; } 182 183 ME=$(basename $0) 184 SNAME=server 185 SFILE=$XDG_RUNTIME_DIR/emacs/$SNAME 186 187 # Default argument needs our socket in any case 188 ARGS="--socket-name=$SNAME --suppress-output" 189 190 # Ensure availability of commands 191 ensure() { 192 if ! command -v $1 >/dev/null 2>&1; then 193 echo "ERROR: command '$1' is needed" 194 exit 1 195 fi 196 } 197 198 ensure notify-send 199 ensure cat 200 ensure rm 201 ensure systemctl 202 ensure emacsclient 203 ensure emacs 204 205 # Get full paths to emacs commands via command lookup 206 EC=$(command -v emacsclient) 207 EM=$(command -v emacs) 208 209 # Do we already have an emacs? 210 # I could let emacsclient do this automatically, but it 211 # is hard-coded to exec emacs --daemon (without socket name) 212 213 # This goes along an emacs.service file obviously 214 215 # Diagnostic: if systemctl says emacs is active, socket MUST exist 216 if systemctl -q --user is-active emacs && [ ! -S $SFILE ]; then 217 echo "ERROR: Emacs service is active but socket not found at $SFILE" 218 echo "This indicates a problem with the Emacs service configuration" 219 exit 1 220 fi 221 222 # If socket missing, start the service 223 if [ ! -S $SFILE ]; then 224 debug "Socket file not found, starting Emacs service..." 225 notify-send "Starting Emacs server, this might take a while...."; 226 #$EM --bg-daemon=$SFILE 227 systemctl --user start emacs 228 fi 229 230 # Final assertion: socket must exist now 231 [ -S $SFILE ] || { echo "ERROR: Socket file not found at $SFILE"; exit 1; } 232 233 # Handle special 'stdin' case 234 if [ $# -ge 1 ] && [ "$1" == "-" ]; then 235 TMPSTDIN="$(mktemp /tmp/emacsstdinXXX)"; 236 cat >"$TMPSTDIN"; 237 # Recurse or call with flags below 238 $ME $TMPSTDIN; 239 rm $TMPSTDIN; 240 exit 0; 241 fi 242 243 ## If we explicitly get asked to start terminal, do so 244 if [[ $@ =~ (^|[[:space:]])(-nw|-tty|-t|--no-window-system)($|[[:space:]]) ]]; then 245 # We were forcing terminal, skip the whole wait, frame, no-frame stuff 246 debug "Forced tty startup" 247 # FIXME this may create twice the -tty argument to start terminal version, but it's harmless 248 ARGS=$ARGS' -tty' 249 else 250 # We're basically expecting X windows, but if not, we good 251 # force tty: call this like 'DISPLAY="" edit ....' see my aliases 252 if [[ $DISPLAY ]]; then 253 debug "X-display value seen, using GUI" 254 case $ME in 255 "edit") 256 ARGS=$ARGS' --no-wait' 257 ;; 258 "edit-wait") 259 ARGS=$ARGS' ' 260 ;; 261 "edit-frame") 262 ARGS=$ARGS' --no-wait --create-frame' 263 ;; 264 "edit-wait-frame") 265 ARGS=$ARGS' --create-frame' 266 ;; 267 ,*) 268 echo "Wrong calling name..." 269 ;; 270 esac 271 else 272 # tty always waits, as it is in the terminal, create-frame has no meaning 273 debug "Starting tty emacs" 274 ARGS=$ARGS' -tty' 275 fi 276 fi 277 278 # Go with the original arguments 279 debug $EC ${ARGS} "$@" 280 exec $EC ${ARGS} "$@" 281 #+end_src 282 283 The above script is the result of trying to get the server based setup of Emacs a little better. The thoughts leading to the above script are included below. 284 285 Emacs can run as a /server/ meaning that its process is always active and a specific /client/ program called =emacsclient= can be used to connect to it to do the actual user interaction and let me edit and use the lisp interpreter. 286 287 It's far from trivial, for me at least, on how to properly configure this, assuming you have some special wishes. I've certainly spent my time on trying to get it right. 288 289 *** Things that I'm sure of I want 290 Here's what I know for sure on what I want: 291 292 1. I want to have at least one server process for 'default' editing so emacs does not have to start a process for every file; the goal here is to have fast opening of files to edit. This is critical. I'm assuming that for this a server-like-setup is needed. 293 2. Given that I tweak emacs a lot, it must be very easy to either disable the server temporarily or have it restart easily so I can test new changes without shutting me out of emacs completely. 294 3. The previous item also implies that I want to run multiple independent emacs-en next to each other, regardless if they are run as a daemon or not. 295 4. Emacs must work on a non X-windows screen, but extra effort to get that is OK. 296 5. Be able to use '-' as placeholder for =stdin= to be able to pipe information directly into editor 297 6. allow specific handlers, like =mailto:= 298 299 *** Things I see potential for, but not sure of 300 Having a server/servers opens a couple of options which I may like, but not sure on how to do or even if I understand them properly 301 302 - given that the socket can be a TCP socket, does that mean I can use emacsclient -some-arg <the IP of the socket> on one machine to use emacs on another machine? How well would that work? This would be awesome to have a small computer which doesn't need an emacs config to use my emacs on another machine, but I suspect I misunderstand how this works 303 304 - having multiple emacs servers could help with specific scenario's for mu4e for example. I'd love to have the mu-server in its own process and just use that from other emacs servers Similarly for long running language servers and or other processes which shouldn't be affected by me clawing words into my keyboard. 305 306 * Package Management 307 Package handling, do this early so emacs knows where to find things. Package handling basically means two things: 308 309 1. getting and installing packages, by default handled by package.el; 310 2. configuring packages, just elisp, but I use the =use-package= macro typically. 311 312 As I want to have version controlled package installation that is easily reproducible, =straight.el= seem a logical choice, so let's start by bootstrapping that. 313 314 #+begin_src emacs-lisp 315 (defvar bootstrap-version) 316 (setq straight-repository-branch "develop") ; Need this for new org-contrib location 317 (let ((bootstrap-file 318 (mrb/emacs-file "straight/repos/straight.el/bootstrap.el")) 319 (bootstrap-version 5)) 320 (unless (file-exists-p bootstrap-file) 321 (with-current-buffer 322 (url-retrieve-synchronously 323 "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el" 324 'silent 'inhibit-cookies) 325 (goto-char (point-max)) 326 (eval-print-last-sexp))) 327 (load bootstrap-file nil 'nomessage)) 328 #+end_src 329 330 Now that we have =straight= we will use it from the =use-package= macro. 331 332 #+begin_src emacs-lisp 333 (setq use-package-always-ensure nil ; Make sure this is nil, so we do not use package.el 334 use-package-verbose nil ; disabled to reduce startup/reload overhead 335 ) 336 ;; From this point on we should be able to use `use-package 337 (use-package straight 338 :custom 339 ;; straight-host-usernames is set in personal.el 340 (straight-use-package-by-default t) 341 ;; Make sure packages do not pull in internal org, we pregister org from straight.el 342 (straight-register-package 'org) 343 (straight-register-package 'org-contrib)) 344 345 ;; FROM THIS POINT use-package should work as intended, i.e. using straight. 346 347 ;; Need to install dependencies of use-package manually, why?? 348 (use-package diminish) 349 (use-package buttercup) 350 #+end_src 351 352 I want to delegate garbage collection to someone who knows more than I do and made a package for it. The idea is still similar to what I had for years, i.e. large threshold on an idle-timer to do garbage collection. The package just adds some convenience, so I dont have to think about it other than making the threshold large on startup. 353 354 #+begin_src emacs-lisp 355 (use-package gcmh 356 :custom 357 (gcmh-verbose 1) 358 (gcmh-high-cons-threshold (* 16 1024 1024 1024)) 359 :config 360 (diminish 'gcmh-mode) 361 (gcmh-mode 1)) 362 #+end_src 363 364 365 As emacs lives inside the systemd environment it is not automatically the case that my shell environment is inherited. In many cases, this is not a problem, but for those cases that expect the same environment, I'm making explicit that the enviroment needs to be from the shell. We should do this a soon as =use-package= is useable. 366 367 #+begin_src emacs-lisp 368 (use-package exec-path-from-shell 369 :demand t 370 :custom 371 (exec-path-from-shell-arguments '("-l")) ; Login shell only (no interactive flag) 372 :config 373 (exec-path-from-shell-initialize)) 374 #+end_src 375 376 This makes sure that things like TeX and git and other programs run in the same context as they would in my shell. 377 378 * Personal information 379 I'd like to arrange 3 things related to personal information: 380 1. register the proper identification to use; 381 2. Make sure the proper authentication information is stored; 382 3. Store this information privately. 383 384 So, identification, authorization and encryption. 385 386 Seems like a good idea to start configuration with some details about me. The idea being that anything personal goes here and nowhere else. For one, this means my name only appears in this section in this document. Most of the variables I just made up, but some are used in other places too. 387 388 Personal variables are loaded from personal.el (see personal.example.el for template). 389 390 ** Authorization 391 Several things I use within Emacs need authorization, such as tramp, jabber, erc etc. The authorization can come from several sources; ideally as few as possible. Many packages in Emacs have support for a =.netrc= like mechanism, others want to use the key ring in GNOME. The variables =auth-sources= defines the sources available. 392 393 I want to use systems which are desktop independent, so things like the gnome key ring are out because they depend on the gnome environment being present, which I can not guarantee, nor want to related to authentication. The situation which I want to prevent is that if gnome is broken, I can't authenticate to services I need. 394 395 I have a gpg-agent daemon configured which manages gpg and ssh keys, protected by a hardware key. Let's make the system as simple as we can for now and just store passwords in the gpg protected store only, i.e. the password-store program. 396 397 #+begin_src emacs-lisp 398 ;; Use only password-store 399 (use-package password-store) 400 (use-package auth-source-pass 401 :straight (:type built-in) 402 :init 403 (auth-source-pass-enable) 404 :after password-store 405 :custom 406 ;; Make sure it's the only mechanism 407 (auth-sources '(password-store)) 408 (auth-source-gpg-encrypt-to (list user-mail-address))) 409 410 ;; I like the pass interface, so install that too 411 (use-package pass 412 :after password-store) 413 #+end_src 414 415 ** Encrypting information 416 I need a way to store some sensitive information without that being published, should I decide some day to push this config somewhere. 417 418 When needed, the gpg key is used to encrypt information. 419 420 #+begin_src emacs-lisp 421 ;; Use my email-address for encryption 422 (setq-default epa-file-encrypt-to user-mail-address) 423 ;; Make sure we always use this 424 (setq-default epa-file-select-keys nil) 425 #+end_src 426 427 For org-mode, there is a way to encrypt sections separately. See [[Encrypting information in org-mode]] for the details on the settings for this. 428 429 Next to inline content in org that needs encryption, there is also content that needs encrypting which is more suitable to store in a separate file for several reasons. 430 431 * Global Generic settings 432 Section contains global settings which are either generic or I haven't figured out how to classify it under a better section. 433 434 Let's begin with the setup of some fundamentals like if we want tabs or spaces, how to select stuff and what system facilities to use. 435 436 UI and display settings: 437 #+begin_src emacs-lisp 438 (setq inhibit-startup-message t ; suppress startup screen 439 frame-resize-pixelwise t) ; pixel-precise frame resizing 440 #+end_src 441 442 Native compilation settings: 443 #+begin_src emacs-lisp 444 (setq package-native-compile t ; compile packages natively 445 native-comp-async-report-warnings-errors 'silent ; suppress native-comp warnings 446 comp-deferred-compilation t) ; defer compilation 447 #+end_src 448 449 I'm of the /spaces-no-tabs religion/: 450 #+begin_src emacs-lisp 451 (setq-default indent-tabs-mode nil) ; do not insert tabs when indenting by default 452 (setq tab-width 4) ; 4 spaces by default 453 #+end_src 454 455 456 I want to prevent producing files which have trailing spaces, because they serve no purpose at all. Removing these spaces is less trivial than it seems and I am not pretending to know what the best strategy is. The [[https://github.com/jamescherti/stripspace.el][stripspace]] package seems to be exactly made for this. 457 458 #+begin_src emacs-lisp 459 (use-package stripspace 460 :diminish stripspace-local-mode 461 :hook ((prog-mode . stripspace-local-mode) 462 (text-mode . stripspace-local-mode) 463 (conf-mode . stripspace-local-mode)) 464 :custom 465 ;; We do now want diffs that are to verbose on unclean files 466 (stripspace-only-if-initially-clean t) 467 ;; make sure we keep in the right position after stripping 468 (stripspace-restore-column t)) 469 470 ;; FIXME these settings do not belong here 471 (setq 472 delete-by-moving-to-trash t ; move files to the trash instead of rm 473 select-enable-clipboard t ; use the clipboard in addition to kill-ring 474 475 warning-minimum-level 'error 476 large-file-warning-threshold nil 477 find-file-use-truenames nil 478 find-file-compare-truenames t 479 480 minibuffer-max-depth nil 481 minibuffer-confirm-incomplete t 482 complex-buffers-menu-p t 483 next-line-add-newlines nil 484 kill-whole-line t 485 486 auto-window-vscroll nil 487 488 compilation-scroll-output t 489 custom-safe-themes t 490 disabled-command-function #'ignore ; don't prompt or write to init.el for disabled functions 491 explicit-shell-file-name "/bin/bash" 492 font-lock-maximum-size nil 493 gud-gdb-command-name "gdb --annotate=1" 494 reb-re-syntax 'string 495 sgml-xml-mode t 496 warning-suppress-types '((comp) (undo)) 497 x-select-enable-clipboard-manager nil) 498 499 ;; Map backspace to DEL and delete to C-d 500 (when (display-graphic-p) 501 (normal-erase-is-backspace-mode t)) 502 503 ;; Let marks be set when shift arrowing, everybody does this 504 (setq shift-select-mode t) 505 (delete-selection-mode 1) 506 507 ;; Only require to type 'y' or 'n' instead of 'yes' or 'no' when prompted 508 (setq use-short-answers t) 509 510 ;; Use auto revert mode globally 511 ;; This is safe because emacs tracks if the file is saved in the editing buffer 512 ;; and if so, it will not revert to the saved file. 513 (use-package autorevert 514 :straight (:type built-in) 515 :diminish auto-revert-mode 516 517 :custom 518 (auto-revert-interval 10) ; increased from 5 to reduce filesystem checks during typing 519 (global-auto-revert-non-file-buffers t) ; things like dired buffers 520 521 :config 522 (global-auto-revert-mode t) 523 524 (defun mrb/org-restore-context-after-revert () 525 "Reveal org-mode heading context around point after buffer revert." 526 (when (derived-mode-p 'org-mode) 527 (org-show-context 'default))) 528 529 (add-hook 'after-revert-hook #'mrb/org-restore-context-after-revert)) 530 531 ;; Should this be here? 532 ;; Try to have urls and mailto links clickable everywhere 533 (setq goto-address-url-mouse-face 'default) 534 (define-global-minor-mode global-goto-address-mode 535 goto-address-mode 536 (lambda () 537 (goto-address-mode 1))) 538 (global-goto-address-mode t) 539 540 ;; Shorten urls for mode specific syntax 541 (defun mrb/shortenurl-at-point () 542 (interactive) 543 (let ((url (thing-at-point 'url)) 544 (bounds (bounds-of-thing-at-point 'url))) 545 (kill-region (car bounds) (cdr bounds)) 546 547 ; Leave url as is, unless mode has specific link syntax 548 (insert (format (cond ((eq major-mode 'org-mode) "[[%1s][%2s]]") 549 ((eq major-mode 'markdown-ts-mode) "[[%2s][%1s]]") 550 (t "%1s")) 551 url 552 (truncate-string-to-width url 40 nil nil "…"))))) 553 (bind-key "C-c s" 'mrb/shortenurl-at-point) 554 #+end_src 555 556 Make life easier if we have sudo, so we can just edit the files and be done with them if possible 557 558 #+begin_src emacs-lisp 559 (use-package sudo-save) 560 #+end_src 561 562 Using pdf-tools for PDF handling. This is a lot better than docview. I'm a bit annoyed that they are not one package, they are very similar. 563 564 #+begin_src emacs-lisp 565 (use-package pdf-tools 566 :demand t ; prevent configuring while opening pdf files 567 :after (fullframe) 568 :magic ("%PDF" . pdf-view-mode) 569 :hook ((pdf-view-mode . (lambda () (cua-mode 0)))) 570 :commands (pdf-info-getannots mrb/pdf-extract-info) 571 :bind ( :map pdf-view-mode-map 572 ("h" . 'pdf-annot-add-highlight-markup-annotation) 573 ("t" . 'pdf-annot-add-text-annotation) 574 ("D" . 'pdf-annot-delete) 575 ("C-s" . 'isearch-forward) 576 ("s-c". 'pdf-view-kill-ring-save) 577 ("m" . 'mrb/mailfile) 578 ("q" . 'kill-current-buffer) 579 ([remap pdf-misc-print-document] . 'mrb/pdf-misc-print-pages) 580 ) 581 582 :custom 583 (pdf-annot-activate-created-annotations t) ; automatically annotate highlights 584 (pdf-view-resize-factor 1.1) ; more fine-grained zooming 585 (pdf-view-midnight-colors '("#DCDCCC" . "#383838")) ; Not sure what this is 586 (pdf-misc-print-program-executable "/usr/bin/lp") 587 (pdf-view-display-size 'fit-page) ; scale to fit page by default 588 589 :config 590 (add-to-list 'pdf-view-incompatible-modes 'display-line-numbers-mode) ; disable line numbers in PDF view 591 (setq pdf-info-epdfinfo-program (mrb/emacs-file "straight/repos/pdf-tools/server/epdfinfo")) 592 ;;(pdf-tools-install) ; autobuild should not happen now, so enable the question if it does 593 (require 'pdf-annot) 594 (define-key pdf-annot-edit-contents-minor-mode-map (kbd "<return>") #'pdf-annot-edit-contents-commit) 595 (define-key pdf-annot-edit-contents-minor-mode-map (kbd "<S-return>") #'newline) 596 597 ;; Some settings from http://pragmaticemacs.com/emacs/even-more-pdf-tools-tweaks/ 598 (fullframe pdf-view-mode kill-this-buffer) 599 600 (add-to-list 'revert-without-query ".+\\.pdf") ; pdf is always a reflection of disk file 601 602 (defun mrb/pdf-misc-print-pages(filename pages &optional interactive-p) 603 "Wrapper for `pdf-misc-print-document` to add page selection support" 604 (interactive (list (pdf-view-buffer-file-name) 605 (read-string "Page range (empty for all pages): " 606 (number-to-string (pdf-view-current-page))) 607 t) pdf-view-mode) 608 (let ((pdf-misc-print-program-args 609 (if (not (string-blank-p pages)) 610 (cons (concat "-P " pages) pdf-misc-print-program-args) 611 pdf-misc-print-program-args))) 612 (pdf-misc-print-document filename)))) 613 #+end_src 614 615 Integrate this with orgmode as well. =org-pdftools= provides a link type, so we can link /into/ pdf documents. =pdf-tools-org= provides utility functions to help export pdf annotations into org mode and vice versa 616 617 #+begin_src emacs-lisp 618 (use-package org-pdftools ; this brings in org-noter as well 619 :after (org pdf-tools) 620 :hook (org-mode . org-pdftools-setup-link)) 621 622 (use-package pdf-tools-org 623 :demand t ; when should this be loaded? goes on pdftools load now 624 :after (org pdf-tools) 625 :straight (:host github :repo "machc/pdf-tools-org") 626 ) 627 #+end_src 628 629 I tend to compile Emacs with ImageMagick support, let's make sure we use it too. 630 631 #+begin_src emacs-lisp 632 (when (fboundp 'imagemagick-register-types) 633 (imagemagick-register-types)) 634 #+end_src 635 636 Quick install of package that exports =qrencode-region= function. I use this mainly to send links or package tracking codes to my mobile. 637 638 #+begin_src emacs-lisp 639 (use-package qrencode) 640 #+end_src 641 642 * Internationalization and multi-language features 643 If anything multi-language should work, UTF-8 encoding is a must, so let's make sure we try to use that everywhere 644 645 #+begin_src emacs-lisp 646 (use-package mule 647 :straight (:type built-in) 648 :init 649 (setq default-input-method 'TeX) 650 (prefer-coding-system 'utf-8) 651 :config 652 (set-terminal-coding-system 'utf-8) 653 (set-keyboard-coding-system 'utf-8)) 654 #+end_src 655 656 For conveniently editing accented characters like 'é' and 'è' there are quite a few options to reach that result. I have dead characters configured as an option in the operating system, but that is far from ideal, especially when programming. As I hardly need those characters outside of emacs, i can leave that option as needed and optimize Emacs to my needs. 657 658 The fallback is C-x 8 C-h which gives specific shortcuts for special characters which are available. For the exotic characters that will do just fine. For the more common characters the C-x 8 prefix is to complex. 659 660 After evaluating some systems, the TeX input method suits me the best. I'd like to enable that globally by default, which needs two things: 661 1. enable multi-language editing by default (input-method is only 662 active in a multi-language environment) 663 2. set the default input-method to tex 664 665 There is however a problem, the TeX input method assumes the first character / string produced will always be the choice I need, without allowing selecting an alternative. This turns out to be due to =quail-define-package= which determines the way the completions work. The problem is the =DETERMINISTIC= argument of the function, that is set to 't'. (8th argument). While I am at it, I also changed the =FORGET-LAST-SELECTION= (7th argument) to nil, so the last selection is remembered. 666 667 For this to work properly we have to define a whole new input-method based on a copy of latin-ltx.el 668 669 #+begin_src emacs-lisp 670 ;; No input method will be active by default, so for each mode where 671 ;; it needs to be active we need to activate it (by a hook for example) 672 (defun mrb/set-default-input-method() 673 (interactive) 674 (activate-input-method "TeX") 675 676 ;; Define a few omissions which I use regularly 677 (let ((quail-current-package (assoc "TeX" quail-package-alist))) 678 (quail-define-rules ((append . t)) 679 ("\\bitcoin" ?฿) 680 ("\\cmd" ?⌘) 681 ("\\shift" ?⇧) 682 ("\\alt" ?⎇) 683 ("\\option" ?⌥) 684 ("\\return" ?⏎) 685 ("\\tab" ?↹) 686 ("\\backspace" ?⌫) 687 ("\\delete" ?⌦) 688 ("\\plusminus" ?±) 689 ("\\_1" ?₁) 690 ("\\_2" ?₂)))) 691 692 693 ;; Set the default language environment and make sure the default 694 ;; input method is activated 695 (add-hook 'set-language-environment-hook 'mrb/set-default-input-method) 696 ;; And now we can set it 697 (set-language-environment "UTF-8") 698 ;; Make sure our timestamps within emacs are independent of system locale 699 ;; This is mostly for orgmode I guess. 700 (setq system-time-locale "nl_NL.UTF-8") 701 702 703 ;; Activate it for all text modes 704 (add-hook 'text-mode-hook 'mrb/set-default-input-method) 705 #+end_src 706 707 For even more esoteric characters we have to do some extra work. No font provides all Unicode characters. While I previously had a manual solution, I'm now relying on the package =uni-code-fonts= to do the proper mapping. The first run will be very slow, but the information gets cached. 708 709 The problem with applying this function is that we need to be 'done' with our visual initialization or otherwise they'll do nothing (at least that I can see). So, let's group our corrections in a function and call that when we are done with our visual (X) init. 710 711 #+begin_src emacs-lisp 712 (use-package unicode-fonts 713 :demand t 714 ) 715 716 (defun mrb/unicode-font-corrections() 717 "Set up Unicode font mappings with version-aware support." 718 (interactive) 719 (mrb/check-emacs-version-support 29 "unicode-fonts caching") 720 (unicode-fonts-setup) 721 ;; Add a line like the following for each char displaying improperly 722 723 ;; mu4e uses Ⓟ,Ⓛ and Ⓣ, let's correct all letters 724 (set-fontset-font t '(?Ⓐ . ?Ⓩ) "Symbola") ; noto line-height too high 725 ) 726 #+end_src 727 728 So, when characters do not show properly, the steps to take now are: 729 1. Find a font which has the char 730 2. Map the character(-range) to that font 731 3. Optional: define a convenient way to type the character 732 733 To make my life a bit easier, using the =all the icons= package for using special symbols. 734 735 #+begin_src emacs-lisp 736 (use-package all-the-icons 737 :demand t) ; Load unconditionally; package handles display type internally 738 #+end_src 739 740 The above in itself does nothing. It just makes using the icons easier and consistent. Other packages. With the command =M-x all-the-icons-install-fonts= the latest version of the proper fonts will be installed automaticaly, so run that once in a while. 741 742 * Visual 743 Many settings have to do with how the screen looks and behaves in a visual way. Things like screen layout, colors, highlighting etc. fall into this category. 744 745 Let's set up some basics first, background dark, some default frame and cursor properties: 746 747 #+begin_src emacs-lisp 748 (setq mrb/cursor-type 'box) 749 (setq mrb/cursor-color "DarkOrange") 750 751 (setq-default frame-background-mode 'dark) 752 753 ;; Only show cursor in the active window. 754 (setq-default cursor-in-non-selected-windows nil) 755 756 ;;Default frame properties frame position, color, etc 757 (setq default-frame-alist 758 `((cursor-type . ,mrb/cursor-type ) 759 (cursor-color . ,mrb/cursor-color) 760 (internal-border-width . 24) 761 (mouse-color . "white"))) 762 #+end_src 763 764 ** Mode-line and status info 765 One thing that always bothered me is the repeated mode-line for every buffer with information that only needs to be displayed once. Like the new-mail indicator or time-display for example. This section explores a configuration to scratch that itch. 766 767 *Local information*, that is per buffer or per mode, should be displayed in the =mode-line= at the bottom of each visible buffer. There will be exceptions to this for special buffers where this does not make sense. 768 769 *Global information*, typically at least what =global-mode-string= holds, needs to be displayed only once (per frame). This will be handled by =tab-bar-mode= at the top of each frame, under the menu-bar. 770 771 *** Mode-line 772 773 The format of the mode-line I would like is inspired by nano modeline: 774 775 =[ status name (primary) secondary ]= 776 777 where: 778 - status :: gives status of buffer (RO/RW/**/-- etc.) 779 - name :: name or filename to identify the contents of the buffer 780 - primary :: buffer specific primary information, left aligned 781 - secondary :: buffer specific secondary information, right aligned 782 783 Both primary and secondary are buffer and/or mode-specific Per mode configuration of the mode-line contents is possible.. 784 785 #+begin_src emacs-lisp 786 (defun mrb/modeline-counters () 787 "Display page numbers for pdf-view-mode, line numbers for other modes." 788 (if (eq major-mode 'pdf-view-mode) 789 (format "[%d/%d]" (pdf-view-current-page) (pdf-cache-number-of-pages)) 790 (format "[%s/%d]" (format-mode-line "%l") (line-number-at-pos (point-max))))) 791 792 (defun mrb/modeline-right-align () 793 "Right-align modeline content with space to the right, accounting for counter width and total window width." 794 (let* ((counter (mrb/modeline-counters)) 795 (counter-width (string-width counter)) 796 (total-width (window-total-width)) 797 (align-pos (- total-width counter-width 4))) 798 (propertize " " 'display `(space :align-to ,align-pos)))) 799 800 ;; Mmodeline using only built-in Emacs. 801 (setq-default mode-line-format 802 (list 803 "%e" mode-line-front-space 804 '(:propertize ("" mode-line-mule-info mode-line-client mode-line-modified mode-line-remote) 805 display (min-width (5.0))) 806 mode-line-frame-identification 807 mode-line-buffer-identification 808 '(vc-mode vc-mode) 809 " " 810 mode-line-modes 811 '(:eval (mrb/modeline-right-align)) 812 '(:eval (mrb/modeline-counters)))) 813 814 #+end_src 815 816 *** Status information in tab-bar 817 818 The format of the status-bar is similar to the mode-line: 819 820 =[ primary secondary ]= 821 822 where: 823 - primary :: global or mode specific information, *no* buffer specific information, left aligned 824 - secondary :: global or mode specific information, *no* buffer specific information, right aligned 825 826 One of the elements in the status bar will be a list of buffers which need attention for some reason (irc, compilation etc.). For this the =tracking= package will be used, which is part of circe, but has its own install recipe. I only use it for irc right now. 827 828 #+begin_src emacs-lisp 829 (use-package tracking 830 :custom 831 (tracking-max-mode-line-entries 1) 832 (tracking-shorten-buffer-names-p nil) 833 834 :config 835 (tracking-mode 1)) 836 #+end_src 837 838 this fills information in the =global-mode-string= which is used in the tab-bar format later on. 839 840 In the tab-bar we will display messages, like in the echo area at the bottom. Messages in the bottom are sometimes a bit annoying, so we set up a filtering mechanism so we can leave out messages that are often in the way. 841 842 #+begin_src emacs-lisp 843 ;; The order of this block is important 844 ;; for handling messages, make sure inhibit message is inserted first 845 (add-to-list 'set-message-functions 'inhibit-message) 846 847 ;; if that is the case, the inhibit-message regexps start working 848 ;; Add all messages here that are annoying, they won't get into the message area then 849 (setq inhibit-message-regexps 850 '("^\\(Wrote .+\\)$" 851 "^\\(Saving file .+\\)$" 852 "^\\(Saving all Org buffers... done\\)$" 853 "^\\(Garbage collecting.+\\)$" 854 "^\\(\\[mu4e\\] .+\\)$" 855 "^\\(Some IDs share title, see M-x org-mem-list-title-collisions\\)$" 856 )) 857 858 ;; Everything is not a good idea, if there is a question 859 ;; (setq inhibit-message-regexps 860 ;; '("^\\(.+\\)$")) 861 862 ;; but handle the message separately for the tabbar 863 (setq mrb/tab-bar-message (current-message)) 864 (add-to-list 'set-message-functions 865 '(lambda (message) (setq mrb/tab-bar-message (or message "")))) 866 867 #+end_src 868 869 #+begin_src emacs-lisp 870 ;; Tab bar configuration for global information display 871 (use-package tab-bar 872 :after base16-theme 873 :custom 874 (tab-bar-format '(tab-bar-format-menu-bar ; easy entry into menu when needed 875 tab-bar-format-history ; only when history mode active, shows arrows then 876 mrb/tab-bar-format-primary 877 tab-bar-format-align-right 878 mrb/tab-bar-format-secondary)) 879 (tab-bar-position t "Position tab-bar under the menu-bar") 880 (tab-bar-mode t "Enable tab-bar-mode unconditionally") 881 (global-tab-line-mode nil "Make sure tab-line-mode is disabled") 882 883 :custom-face 884 ;; TODO tab-bar should ideally be (:inherit mode-line) but the theme's explicit 885 ;; :background definition prevents inheritance from overriding. Must use 886 ;; explicit colors here to override the theme's tab-bar definition. 887 (tab-bar ((t (:background "#4c566a" :foreground "#eceff4" :height 1.2 888 :box (:line-width 1 :color "#5e81ac"))))) 889 890 :config 891 (display-time-mode -1) ; make sure it is off, it will appear on right of tab-bar else 892 893 (defun mrb/current-message-tab-bar() 894 "Show the current message in the tab-bar, after massaging it a bit." 895 (seq-reduce (lambda (text subst) 896 (string-replace (car subst) (cdr subst) text)) 897 898 ;; List of substitution string pairs 899 '(("\n" . "⮐") ; display return as character,prevent multiline somewhat 900 ("\"" . "")) ; no need for the quotes here? I think. 901 (propertize mrb/tab-bar-message 'face '(warning)))) 902 903 (defun mrb/tab-bar-primary-available-chars (inner-width menu-px lead-px sec-px ch-px) 904 "Compute character count available for the primary tab-bar message. 905 Pixel arguments: INNER-WIDTH is frame inner width, MENU-PX is the menu-bar 906 icon width, LEAD-PX is the leading space before the message, SEC-PX is the 907 secondary content width, CH-PX is the per-character advance width." 908 (floor (/ (max 0 (- inner-width menu-px lead-px sec-px)) ch-px))) 909 910 (defun mrb/tab-bar-format-primary () 911 "Format primary tab-bar element, truncating message to prevent tab-bar overflow." 912 (let* ((secondary-str (concat 913 " " 914 (format-mode-line tracking-mode-line-buffers) 915 (format-mode-line global-mode-string))) 916 (msg-raw (mrb/current-message-tab-bar)) 917 (available 918 (if (and (fboundp 'string-pixel-width) (display-graphic-p)) 919 ;; GUI: pixel-accurate calculation. 920 ;; The tab-bar face uses a larger font (height 1.2x). The 921 ;; per-character advance width differs from the buffer font. 922 ;; Measure it as the difference between 2-char and 1-char 923 ;; pixel widths to avoid the first-character overhead. 924 (let* ((ch-px (max 1 (- (string-pixel-width (propertize "xx" 'face '(warning tab-bar))) 925 (string-pixel-width (propertize "x" 'face '(warning tab-bar)))))) 926 (lead-px (string-pixel-width (propertize " " 'face 'tab-bar))) 927 (sec-s (let ((s (copy-sequence secondary-str))) 928 (add-face-text-property 0 (length s) 'tab-bar t s) 929 s)) 930 (sec-px (string-pixel-width sec-s)) 931 (menu-str (let ((items (tab-bar-format-list (list 'tab-bar-format-menu-bar)))) 932 (when items (nth 2 (car items))))) 933 (menu-px (if menu-str 934 (let ((s (copy-sequence menu-str))) 935 (add-face-text-property 0 (length s) 'tab-bar t s) 936 (string-pixel-width s)) 937 0))) 938 (mrb/tab-bar-primary-available-chars 939 (frame-inner-width) menu-px lead-px sec-px ch-px)) 940 ;; TTY: character-based fallback 941 (max 0 (- (frame-width) 3 (string-width secondary-str))))) 942 (msg (if (> available 0) 943 (truncate-string-to-width msg-raw available nil nil "…") 944 "…"))) 945 (concat " " msg))) 946 947 (defun mrb/tab-bar-format-secondary () 948 (concat 949 (propertize " " 'display `(raise 0.2)) 950 (format-mode-line tracking-mode-line-buffers) ; buffers that need attention 951 (format-mode-line global-mode-string)))) 952 953 #+end_src 954 955 ** Theme 956 I'm using the [[https://github.com/arcticicestudio/nord][Nord color palette]]. This palette defines 16 colors. 957 958 There is a base16 theme which uses exactly these colors and there are varieties which use the 16 colors and some derivatives of it. 959 960 Although I think the 16 color system is a bit simplistic and, for emacs at least, I will need to customize more later on, the main lure of this system is that I can use the same set for many of my programs I use (emacs, xterm, i3, dunst etc.) which sounds attractive. So, I'm starting with the =base16-nord= theme and see where this leaves me. 961 962 #+begin_src emacs-lisp 963 (use-package base16-theme 964 :custom 965 ;; Do config here and finally load the theme 966 (base16-theme-distinct-fringe-background nil) 967 (base16-theme-highlight-mode-line 'contrast) 968 (base16-theme-256-color-source "colors") ; This gives way better colors with terminal emacs? 969 970 :init 971 (load-theme 'base16-nord t) 972 973 :custom-face 974 ;; Adjust only emacs core faces here, the rest should be close to their repective packages 975 976 ;; Default font configuration 977 (default ((t (:family "Hack" :slant normal :weight normal :height 112 :width normal)))) 978 979 (font-lock-comment-face ((t (:foreground "bisque3")))) 980 (font-lock-doc-face ((t (:inherit font-lock-comment-face)))) 981 982 (button ((t (:foreground "#d08770" :weight semi-bold)))) 983 984 (show-paren-match ((t (:foreground "#eceff4" :background "#81a1c1" :inherit nil :weight normal)))) 985 (show-paren-mismatch ((t (:foreground "#2e3440" :background "#bf616a" :inherit nil :weight normal)))) 986 987 (region ((t (:foreground unspecified :background "#3b4252" :inherit nil :weight normal)))) 988 (fringe ((t (:foreground "#a3be8c")))) 989 990 (hl-line ((t (:foreground unspecified :background "#5e81ac")))) 991 992 (line-number-current-line ((t (:background "#4c566a")))) 993 994 (cursor ((t (:background ,mrb/cursor-color)))) 995 (mouse ((t (:background "white")))) 996 997 (header-line ((t (:foreground "#eceff4" :background "#4c566a")))) 998 (mode-line ((t (:background "#4c566a" :foreground "#eceff4" :height 1.2 999 :box (:line-width 1 :color "#5e81ac"))))) 1000 1001 (mode-line-inactive ((t (:background "#2e3440" :foreground "#a3be8c" :height 1.2 1002 :box (:line-width 1 :color "#434c5e"))))) 1003 1004 (completions-annotations ((t (:inherit (italic font-lock-comment-face)))))) 1005 #+end_src 1006 1007 ** Miscellaneous other visual settings follow. 1008 1009 #+begin_src emacs-lisp 1010 ;; no splash screen 1011 (setq inhibit-startup-screen t) 1012 (setq inhibit-startup-message t) 1013 (setq initial-scratch-message nil) 1014 1015 (use-package mic-paren 1016 :custom 1017 (paren-highlight-at-point nil) 1018 1019 :config 1020 (paren-activate)) 1021 1022 ;; Font locking has always been the major performance hog for me, so here's the current 1023 ;; state of variables trying to minimize typing delays. The 3 position indication in 1024 ;; comments is the experience playing with it: 1025 ;; ? unknown effect 1026 ;; ! needed can't change 1027 ;; + minor effect, but noticeable 1028 ;; ++ major effect, very noticeable 1029 ;; +++ very high effect on performance (mostly subjective, but supported by some light profiling) 1030 (setq font-lock-support-mode 'jit-lock-mode) 1031 (setq jit-lock-defer-time 0.25) ; defer fontification for 250ms after typing stops 1032 (setq jit-lock-stealth-time nil) ; disable stealth fontification, use deferred only 1033 1034 1035 ;; Make underlining nicer 1036 (setq underline-minimum-offset 3) 1037 1038 ;; Show color of '#RRBBGG texts 1039 (use-package rainbow-mode 1040 :diminish 1041 :custom 1042 ;; Enable color name globally 1043 rainbow-r-colors t) 1044 1045 ;; Give commands the option to display full-screen 1046 (use-package fullframe) 1047 #+end_src 1048 1049 ** Lines 1050 The most important element of an editor is probably how lines of text are displayed. This warrants its own section. 1051 1052 The problem is that 'line' needs clarification because there can be a difference between a logical line and a visual line. It's sometimes important that things stay on one logical line but are displayed over multiple lines. This may be different per mode. 1053 1054 I recently reversed my stance on this and am now enabling visual line mode everywhere (meaning one logical line can be smeared out over multiple display lines. This also enables word-wrap which breaks lines visually between words instead of at random characters. 1055 1056 To help me see the difference between visual lines and logical lines I let emacs display indicators in the fringe and define a function to help me unfill existing paragraphs. 1057 1058 #+begin_src emacs-lisp 1059 ;; Set some defaults 1060 (setq-default word-wrap t ; why would i want to have no wrapping by word? 1061 fill-column 100 1062 truncate-lines nil) 1063 1064 (setq sentence-end-double-space nil) ; A period ends a sentence, period. relevant for fill 1065 1066 (use-package visual-fill-column 1067 :commands (turn-on-visual-fill-column-mode)) 1068 1069 ;; Similar to mail messages, use vertical bar for wrapped paragraphs 1070 (setq visual-line-fringe-indicators 1071 '(vertical-bar nil)) 1072 1073 ;; For all text modes use visual-line-mode 1074 (add-hook 'text-mode-hook 'visual-line-mode) 1075 1076 ;; From:https://www.emacswiki.org/emacs/UnfillParagraph 1077 (defun unfill-paragraph (&optional region) 1078 "Takes a multi-line paragraph and makes it into a single line of text." 1079 (interactive (progn (barf-if-buffer-read-only) '(t))) 1080 (let ((fill-column (point-max)) 1081 ;; This would override `fill-column' if it's an integer. 1082 (emacs-lisp-docstring-fill-column t)) 1083 (fill-paragraph nil region))) 1084 1085 ;; Similar to M-q for fill, define M-Q for unfill 1086 (bind-key "M-Q" 'unfill-paragraph) 1087 #+end_src 1088 1089 Line-numbering is off by default, it tends to slow things down, but if I want it on, I almost always want them relative 1090 1091 #+begin_src emacs-lisp 1092 (use-package display-line-numbers 1093 :straight (:type built-in) 1094 :config 1095 (setq-default display-line-numbers-mode nil) 1096 :custom 1097 (display-line-numbers-type 'relative)) 1098 #+end_src 1099 1100 ** Client dependent settings 1101 Because most of my usage involves having a long lasting emacs daemon, some settings only come into scope once a client connects to that daemon. Most of these settings have to do with appearance and are related to having X available. Anyways, some settings need to be moved to the event when a client visits the server, so we can still apply these settings transparently. 1102 1103 Note that if this code is evaluated any call to =emacsclient= (be that from external or, more importantly Magit) will try to run this code and magit will fail if there's an error in the next section. Take extra care here. 1104 #+begin_src emacs-lisp 1105 ;; Universal settings that work on all frame types (GUI and terminal) 1106 (defun mrb/run-universal-settings () 1107 "Apply settings that work on both GUI and terminal frames." 1108 (menu-bar-mode -1) ;; No menu-bar (works in terminal too) 1109 (tab-bar-mode t)) ;; We *do* want the tab-bar 1110 1111 ;; GUI-specific settings that require graphic display 1112 (defun mrb/run-gui-settings () 1113 "Apply settings that only work on GUI frames." 1114 (when (display-graphic-p) 1115 (tool-bar-mode -1) ;; No tool-bar 1116 (scroll-bar-mode -1) ;; No scroll-bar 1117 (tooltip-mode -1) ;; No tooltips 1118 (setq fringe-mode '(14 . 14)) ;; Fringe, left and right for the continuation characters 1119 (set-fringe-mode fringe-mode) 1120 (setq indicate-buffer-boundaries 'left) 1121 (mrb/unicode-font-corrections))) 1122 1123 ;; Main dispatcher: runs appropriate settings based on frame type 1124 (defun mrb/run-client-settings (&optional frame) 1125 "Run client settings for FRAME (or current frame if nil). 1126 Applies universal settings to all frames, GUI settings only to graphic frames." 1127 (interactive) 1128 (with-selected-frame (or frame (selected-frame)) 1129 (mrb/run-universal-settings) 1130 (mrb/run-gui-settings))) 1131 1132 ;; Use appropriate hook based on startup mode 1133 (if (daemonp) 1134 ;; Daemon mode: wait for frame to be created by server 1135 (add-hook 'server-after-make-frame-hook 'mrb/run-client-settings) 1136 ;; Non-daemon mode: apply settings to initial frame immediately 1137 (mrb/run-client-settings) 1138 ;; Also apply to any subsequently created frames 1139 (add-hook 'after-make-frame-functions 'mrb/run-client-settings)) 1140 #+end_src 1141 ** Context dependent visualization 1142 Next to the information registered in the theme which controls the overall look and feel, there is additional visualization which depend on context dependent factors, such as: 1143 1144 - the user spells a word wrong 1145 - the syntax of a certain phrase is wrong grammatically, depending on 1146 what language the user is writing in 1147 - the status of a certain region is changing, dependent on certain 1148 rules 1149 - a part of the text could be rendered as non-text, images or formulas for example 1150 1151 *** Flycheck 1152 Flycheck is a syntax checking package which seems to have better support than flymake, which is built-in. I've no configuration for flymake specifically, but some packages enable it automatically (elpy for example). Where applicable, I gather the necessary disable actions here first. 1153 1154 Now we can start configuring flycheck 1155 1156 #+begin_src emacs-lisp 1157 (use-package flycheck-pos-tip) 1158 1159 (use-package flycheck 1160 ;; Use popup to show flycheck message 1161 :after flycheck-pos-tip 1162 :config 1163 (with-eval-after-load 'flycheck 1164 (flycheck-pos-tip-mode))) 1165 #+end_src 1166 1167 Currently actively configured are: 1168 - javascript :: eslint with a config file 1169 1170 Flyspell is similar to flycheck but for text languages. I'm setting some of the flyspell faces to the flycheck faces so they are consistent 1171 1172 #+begin_src emacs-lisp 1173 (use-package flyspell 1174 :straight (:type built-in) 1175 :custom-face 1176 (flyspell-incorrect ((t (:inherit flycheck-error)))) 1177 (flyspell-duplicate ((t (:foreground "#2e3440" :inherit flycheck-warning)))) 1178 :custom 1179 (flyspell-mark-duplications-flag nil "Flyspell Duplicates as warning") 1180 (flyspell-issue-message-flag nil) 1181 (ispell-alternate-dictionary "/usr/share/hunspell/nl_NL.dic") 1182 (ispell-use-framepop-p t)) 1183 1184 (use-package guess-language 1185 :custom 1186 (guess-language-languages '(en de nl))) 1187 #+end_src 1188 1189 Still on the wish-list: 1190 - activate flyspell automatically for all text-modes? 1191 - language detection (English and Dutch mainly) 1192 - check for language pythons, which was troublesome before 1193 - evaluate usage in org source blocks (many checks do not apply) 1194 1195 *** Math rendering 1196 Mathematics requires some specific rendering. In simple cases, using the proper unicode symbols may suffice, but for anything other than the most basis equations or formulas, some help is needed. $\LaTeX$ is the defacto standard for this and there's plenty of support for that in emacs. 1197 1198 For $\TeX$ and $\LaTeX$ fragments I use the =texfrag= package. This allows to insert math fragments inside documents. 1199 1200 Example: 1201 1202 $$e^{ix} = cos x + i sinx$$ 1203 1204 #+begin_src emacs-lisp 1205 (use-package texfrag 1206 :disabled t 1207 :diminish 1208 1209 :custom 1210 (texfrag-prefix "") 1211 1212 :config 1213 (texfrag-global-mode 1)) 1214 #+end_src 1215 *** Highlighting tags and keywords 1216 1217 In many modes, but especially in orgmode, the use of tags and keywords is abundant. In the set of packages that [[https://github.com/rougier][Nicolas P. Rougier]] has made for emacs, there's also a gem which generates SVG labels from keyword matches. 1218 1219 The use of that package is simple, just create a list of keyword matches in the =svg-tag-tags= variable and match those up with a command to create a tag. 1220 1221 I have at least the following usecases in mind: 1222 1223 1. Render my orgmode keywords as tags; 1224 2. Use in mu4e to render the tags 1225 3. perhaps some TODO keywords in code, I use that quite a bit. 1226 1227 The first is the easiest, as the format of =org-todo-keyword-faces= is nigh perfect to transform into a list that can be used by svg-tag-mode. 1228 1229 #+begin_src emacs-lisp 1230 ;; Trickiest here is to know when org-todo-keyword-faces has been filled 1231 (use-package svg-tag-mode 1232 :after (org org-journal base16-theme) 1233 :commands svg-tag-mode 1234 1235 :config 1236 ;; Only enable in GUI frames where SVG rendering works 1237 (defun mrb/maybe-enable-svg-tag-mode () 1238 "Enable svg-tag-mode only in graphic displays." 1239 (when (display-graphic-p) 1240 (svg-tag-mode 1))) 1241 1242 (add-hook 'org-mode-hook #'mrb/maybe-enable-svg-tag-mode) 1243 1244 ;; Readymade svg-tag-tags generator from org keywords 1245 (defun mrb/gen-svg-tags (kf) 1246 "Generate a `svg-tag-tags element from a `org-todo-keyword-faces element" 1247 (let ((k (concat "\\(" (car kf) "\\) ")) ; pass keyword, not the space 1248 (f (cdr kf))) 1249 `(,k . ((lambda (tag) (svg-tag-make tag :face ',f :inverse t :margin 0 )))))) 1250 1251 ;; This gets initialized the first time svg-tag-mode is called, so then it must have 1252 ;; the proper keywords in org-todo-keyword-faces 1253 (setq svg-tag-tags (mapcar 'mrb/gen-svg-tags org-todo-keyword-faces)) 1254 1255 ;; See: https://github.com/rougier/svg-tag-mode/issues/27 1256 ;; Org agenda does not use font-lock, so needs separate overlay to render 1257 (defun mrb/org-agenda-show-svg () 1258 (let* ((case-fold-search nil) 1259 (keywords (mapcar #'svg-tag--build-keywords svg-tag--active-tags)) 1260 (keyword (car keywords))) 1261 (while keyword 1262 (save-excursion 1263 (while (re-search-forward (nth 0 keyword) nil t) 1264 (overlay-put (make-overlay 1265 (match-beginning 0) (match-end 0)) 1266 'display (nth 3 (eval (nth 2 keyword)))) )) 1267 (pop keywords) 1268 (setq keyword (car keywords))))) 1269 (add-hook 'org-agenda-finalize-hook #'mrb/org-agenda-show-svg)) 1270 #+end_src 1271 * Buffers and files 1272 How do I deal with all those buffers and files? 1273 1274 For starters, make sure that they have unique buffer names so I don't get confused: 1275 1276 #+begin_src emacs-lisp 1277 (setq uniquify-buffer-name-style 'forward) 1278 #+end_src 1279 1280 For every file-based buffer, I want auto-save to be on, but not in the same location as the file, as that clutters up everything. For that, I add to the list of file-name transforms to have (local) files auto saved in a designated folder) 1281 1282 #+begin_src emacs-lisp 1283 (setq auto-save-default t 1284 mrb/auto-save-folder (concat user-emacs-directory "auto-save-list/")) 1285 1286 (add-to-list 'auto-save-file-name-transforms 1287 (list "\\(.+/\\)*\\(.*?\\)" (expand-file-name "\\2" mrb/auto-save-folder)) 1288 t) 1289 #+end_src 1290 1291 Auto save helps to automatically recover to the latest save point, but I need a bit more. This is done through versioned backup file. The recovery process is then manual though. 1292 1293 #+begin_src emacs-lisp 1294 (setq create-lockfiles nil ; just clutters for me, no real use? 1295 backup-by-copying t ; otherwise symlinks galore 1296 1297 backup-directory-alist '( 1298 ("." . "/tmp/emacs/")) 1299 1300 delete-old-versions t 1301 kept-new-versions 3 1302 kept-old-versions 2 1303 version-control t) 1304 #+end_src 1305 1306 The auto save helps for the minor disasters, my backups help for the major disasters. What else is needed is a 'normal save' but automatically when I forget to do this. 1307 1308 What I am aiming for here is to not have to think about explicitly saving for certain files. Typically when typing stuff in org-mode I just want the stuff saved that I have types so far. For /some/ files, each save is committed if I think the content warrants this (for example if I think going back to an earlier version is a likely event). 1309 1310 #+begin_src emacs-lisp 1311 (defconst mrb/idle-timer 15 1312 "Time emacs needs to be idle to trigger the save idle timer") 1313 1314 ;; This function does the actual useful thing 1315 (defun mrb/save-timer-callback() 1316 "Callback function that runs when emacs is idle for `mrb/idle-timer' seconds. 1317 Auto-saves org files unless Claude Code IDE session is active to avoid state conflicts." 1318 (unless (and (fboundp 'mrb/claude-session-active-p) 1319 (mrb/claude-session-active-p)) 1320 (org-save-all-org-buffers))) 1321 1322 ;; Activate the timer 1323 ;; The # means: evaluate first ( I keep forgetting this stuff ) 1324 (run-with-idle-timer mrb/idle-timer 'always #'mrb/save-timer-callback) 1325 #+end_src 1326 1327 Also, save the location in files between sessions. 1328 1329 #+begin_src emacs-lisp 1330 ;; Minibuffer prompt is a prompt, don't enter it as text. 1331 (setq minibuffer-prompt-properties '(read-only t point-entered minibuffer-avoid-prompt face minibuffer-prompt)) 1332 ;; Don't echo keystrokes in the minibuffer, prefix will be hinted on after 2 seconds or so. 1333 (setq echo-keystrokes 0) 1334 1335 ;; Save places in buffers between sessions 1336 (use-package saveplace 1337 :straight (:type built-in) 1338 :init 1339 (setq-default save-place-mode t)) 1340 #+end_src 1341 1342 1343 Bind renaming the visiting file to a new location to =C-x-w= now that we have =rename-visited-file= 1344 1345 #+begin_src emacs-lisp 1346 (bind-key "C-x C-w" 'rename-visited-file) 1347 (bind-key "C-S-s" 'write-file) 1348 #+end_src 1349 1350 For file management itself, dired is the defacto standard, but I do not use it much really. The =casual= package is helpfule showing the keyboard bindings. 1351 1352 #+begin_src emacs-lisp 1353 (use-package dired 1354 :straight (:type built-in) 1355 ;; auto-revert global is only for file buffers, so explicit enable is needed 1356 :hook (dired-mode-hook . auto-revert-mode) 1357 :bind (:map dired-mode-map ("C-o" . 'casual-dired-tmenu))) 1358 #+end_src 1359 1360 * Modes 1361 Customization setting for specific modes. Most of the modes I use have a separate section, so this section is only for other modes. 1362 1363 To determine the default major mode; the mode that is started with before all the magic starts is determined by buffer-file-name. If we have it, the normal routine can be followed. If there is no filename yet, the buffer-name is used to determine which mode is needed. 1364 1365 By looking at the code this may have a side-effect, because the buffer-file-name is given a value. Let's try this and see if it gives any issues. 1366 #+begin_src emacs-lisp 1367 (setq-default major-mode 1368 (lambda () 1369 (if buffer-file-name 1370 (fundamental-mode) 1371 (let ((buffer-file-name (buffer-name))) 1372 (set-auto-mode))))) 1373 #+end_src 1374 1375 When emacs does not determine the right mode, I sometimes want a mode-string somewhere in the file. Typically that string gets inserted on a line which is commented out in the proper syntax for that mode. 1376 1377 #+begin_src emacs-lisp 1378 (defun mrb/buffer-majormode (&optional buffer-or-name) 1379 "Returns the major-mode of the specified buffer, or 1380 the current buffer if no buffer is specified" 1381 (buffer-local-value 'major-mode 1382 (if buffer-or-name 1383 (get-buffer buffer-or-name) 1384 (current-buffer)))) 1385 1386 (defun mrb/insert-mode-string() 1387 "Inserts a mode string for the current mode at beginning of the current buffer" 1388 (interactive) 1389 (let ((m (symbol-name (mrb/buffer-majormode)))) 1390 (save-excursion 1391 (goto-char (point-min)) 1392 (insert "-*- mode:" 1393 (substring m 0 (string-match "-mode" m)) 1394 " -*-") 1395 (comment-region (point-min) (point))))) 1396 #+end_src 1397 1398 First bring in a bunch of modes that are used regularly, but have no other config than a map to some extensions. =:requires= is set to nil for those packages which aret built in. 1399 1400 #+begin_src emacs-lisp 1401 (use-package adoc-mode :mode ("\\.asciidoc\\'" "\\.adoc\\'")) 1402 (use-package apache-mode :mode "\\.htaccess\\'") 1403 (use-package conf-mode 1404 :straight (:type built-in)) 1405 (use-package i3wm-config-mode) 1406 (use-package css-mode 1407 :straight (:type built-in) 1408 :mode ("\\.css\\'" "\\.mss\\'")) 1409 (use-package csv-mode :mode "\\.csv\\'") 1410 1411 (use-package gnuplot) 1412 (use-package gnuplot-mode :mode "\\.gp\\'") 1413 (use-package js2-mode :mode "\\.js\\'") 1414 (use-package lua-mode :mode "\\.lua\\'") 1415 (use-package php-mode :mode "\\.php\\'") 1416 (use-package sass-mode :mode "\\.sass\\'") 1417 (use-package markdown-ts-mode :mode "\\.md\\'") 1418 (use-package udev-mode :mode "\\.rules\\'") 1419 (use-package dockerfile-mode :mode "Dockerfile") 1420 (use-package docker) 1421 #+end_src 1422 1423 Now, do the other mode related packages which require a bit of configuration. 1424 #+begin_src emacs-lisp 1425 1426 (use-package eimp 1427 :hook (image-mode . eimp-mode)) 1428 1429 (use-package rainbow-mode 1430 :hook (conf-mode css-mode)) 1431 1432 ;; Open scratch buffer by default in the mode we are in at the moment 1433 ;; with C-u prefix a mode will be asked to use 1434 (use-package scratch 1435 :straight (:type built-in)) 1436 ;; Don't throw away scratch buffer 1437 (use-package persistent-scratch 1438 :config 1439 (persistent-scratch-setup-default)) 1440 1441 (use-package nxml-mode 1442 :straight (:type built-in) 1443 :hook ((nxml-mode . (lambda () (set-input-method nil))) 1444 (nxml-mode . turn-off-auto-fill)) 1445 :custom 1446 (nxml-heading-element-name-regexp "\\|.*") 1447 (nxml-section-element-name-regexp "\\|file\\|.+")) 1448 #+end_src 1449 * Org-mode 1450 Orgmode configuration is probably the largest part of my Emacs configuration, because most of the time I spent in Emacs, when not coding, is spent in org-mode. 1451 1452 ** Initialization of Orgmode 1453 I loosely follow the GTD method for organizing things and I want a quick shortcut to start my main file. 1454 1455 #+name: gtd-starter 1456 #+begin_src emacs-lisp :tangle no 1457 (defun mrb/gtd() 1458 "Start my GTD system" 1459 (interactive) 1460 (find-file org-default-notes-file)) 1461 #+end_src 1462 1463 We do not have to load the main orgmode location, because we already did that on the main initialization to get org-babel started. 1464 1465 Also make sure we never load org from internal, this can happen when functions were defined in the included org version and not anymore in newer versions. We want an error, not a silent load of the older function. 1466 1467 #+COMMENT: This duplicates the one in init.el, problems? 1468 #+begin_src emacs-lisp 1469 (require 'org-agenda) 1470 1471 (use-package org 1472 :after outline 1473 :straight (org 1474 :includes (org-agenda org-capture org-crypt org-datetree org-protocol)) 1475 :hook ((org-mode . turn-on-visual-line-mode) 1476 (org-mode . turn-on-visual-fill-column-mode) 1477 (org-mode . (lambda () (setq-local tab-width 8))) 1478 (org-indent-mode . (lambda () (diminish 'org-indent-mode))) 1479 (org-agenda-mode . (lambda () (hl-line-mode 1)))) 1480 1481 :init 1482 <<orgmode-keywords>> 1483 <<gtd-starter>> 1484 1485 :mode ("\\.txt\\'" "\\.org\'") 1486 :bind (("C-c g" . 'mrb/gtd) 1487 ("C-c a" . 'org-agenda) 1488 ("C-c b" . 'org-switchb) 1489 ("C-s-s" . 'org-save-all-org-buffers) 1490 ("C-c l" . 'org-store-link) ; Is this not bound to C-c l by default 1491 :map org-mode-map 1492 ("s-." . 'org-todo) 1493 ("s->" . 'mrb/org-cancel-todo) 1494 ("C-s-." . 'mrb/force-org-todo) 1495 ("M-p" . 'org-set-property) 1496 :map org-agenda-mode-map 1497 ("s-." . 'org-agenda-todo) 1498 ("s->" . 'mrb/org-agenda-cancel-todo) 1499 ("C-s-." . 'mrb/force-org-agenda-todo) 1500 ("C-." . 'org-agenda-schedule) 1501 ("M-p" . 'org-set-property) 1502 ("C-x m" . 'mrb/construct-mail)) 1503 1504 :custom-face 1505 (org-date ((t (:foreground "#8fbcbb" :underline nil)))) 1506 (org-verbatim ((t (:foreground "#8fbcbb")))) 1507 (org-list-dt ((t (:foreground "#b48ead")))) 1508 (org-drawer ((t (:height 0.8 :inherit font-lock-comment-face)))) 1509 (org-code ((t (:foreground "dodger blue" :weight bold)))) 1510 (org-block ((t (:background "#313745")))) 1511 (org-block-begin-line ((t (:foreground "dark gray")))) 1512 (org-headline-done ((t (:inherit font-lock-comment-face)))) 1513 (org-inlinetask ((t (:inherit font-lock-comment-face)))) 1514 1515 ;; User defined faces for org-mode 1516 (mrb/org-todo-keyword-TODO ((t ( :foreground "#88c0d0" :weight bold)))) 1517 (mrb/org-todo-keyword-DONE ((t ( :foreground "#a3be8c" :weight bold)))) 1518 (mrb/org-todo-keyword-WAITING ((t ( :foreground "#bf616a" :weight bold)))) 1519 (mrb/org-todo-keyword-CANCELLED ((t ( :foreground "bisque4" :weight bold)))) 1520 (mrb/org-todo-keyword-BUY ((t ( :foreground "#d08770" :weight bold)))) 1521 (mrb/org-todo-keyword-HOWTO ((t ( :foreground "#5e81ac" :weight bold)))) 1522 (mrb/org-todo-keyword-INFO ((t ( :foreground "#ebcb8b" :weight bold)))) 1523 (mrb/org-todo-keyword-COLLECT ((t ( :foreground "#a3be8c" :weight bold)))) 1524 (mrb/org-todo-keyword-SOLVE ((t ( :foreground "#8fbcbb" :weight bold)))) 1525 (mrb/org-todo-keyword-READ ((t ( :foreground "#b48ead" :weight bold)))) 1526 (mrb/org-todo-keyword-READING ((t ( :foreground "#bf616a" :weight bold)))) 1527 (mrb/org-todo-keyword-PLAN ((t ( :foreground "#d8dee9" :weight bold)))) 1528 1529 1530 :config 1531 <<orgmode-generalconfig>> 1532 ;; Category icons I have configured elsewhere. 1533 <<org-agenda-visuals>> 1534 1535 <<orgmode-itemactions>> 1536 1537 <<orgmode-scheduling>> 1538 1539 ;; Allow for archiving and refiling in a date organized tree 1540 (use-package org-datetree ) 1541 (use-package org-protocol 1542 :config 1543 ;; If nothing is specified, create a TODO item 1544 (setq org-protocol-default-template-key "t"))) 1545 #+end_src 1546 1547 Gather generic config variables for orgmode in one section 1548 1549 #+name: orgmode-generalconfig 1550 #+begin_src emacs-lisp :tangle no 1551 ;; org-directory is set in personal.el 1552 (setq org-metadir (mrb/org-file "_orgmeta/") 1553 1554 org-use-fast-todo-selection t 1555 org-use-fast-tag-selection t 1556 org-use-speed-commands t 1557 org-fast-tag-selection-single-key 'expert 1558 org-enforce-todo-dependencies t ; we do want task dependencies 1559 org-enforce-todo-checkbox-dependencies nil ; but relax checkbox constraints for it 1560 1561 ;; We do not do priorities 1562 org-enable-priority-commands nil 1563 1564 ;; Agenda settings 1565 org-agenda-inhibit-startup t 1566 org-agenda-dim-blocked-tasks nil 1567 org-agenda-use-tag-inheritance nil 1568 1569 ;; NOTE: this will get overridden later on by org-mem 1570 org-agenda-files (mrb/org-file ".agenda_files") 1571 org-agenda-include-diary t 1572 org-agenda-start-with-log-mode t 1573 org-agenda-todo-ignore-scheduled "future" 1574 org-agenda-ignore-properties '(effort stats appt category) 1575 org-agenda-todo-ignore-scheduled 'future 1576 org-agenda-log-mode-items '(closed clock state) 1577 org-agenda-skip-deadline-prewarning-if-scheduled t 1578 org-agenda-text-search-extra-files (cons 'agenda-archives (directory-files 1579 (expand-file-name (concat org-metadir)) t ".+\\.org")) 1580 ;; Habits 1581 org-habit-show-habits-only-for-today nil 1582 1583 ;; Pressing enter on a link should activate it 1584 org-return-follows-link t 1585 1586 ;; Shift+arrow key handling: org-mode commands vs. text selection 1587 ;; Setting: t = org commands take precedence in org buffers, shift-select works elsewhere 1588 ;; Trade-off: 'always would enable shift-select everywhere but would disable org-mode's 1589 ;; shift+arrow commands (S-left/S-right for promote/demote, S-up/S-down for move). 1590 ;; Current setting (t) is the best compromise: org functionality works fully. 1591 org-support-shift-select t 1592 1593 1594 ;; Auto detect blank line need, this is the default, but I explicitly set this 1595 ;; because it needs to be this or my capturing breaks due to org-capture popup 1596 org-blank-before-new-entry '((heading . auto) (plain-list-item . auto)) 1597 org-export-htmlize-output-type 'css 1598 1599 ;; Use auto-mode-alist for all file links/types 1600 org-file-apps '((auto-mode . emacs)) 1601 1602 org-fontify-done-headline t 1603 org-goto-interface 'outline-path-completion 1604 ;; non nil is just direct children, what an ODD name!!!! 1605 org-hierarchical-todo-statistics nil 1606 org-provide-todo-statistics t 1607 1608 org-log-into-drawer t 1609 org-log-redeadline 'note 1610 org-log-reschedule 'time 1611 org-todo-repeat-to-state "TODO" 1612 1613 org-modules '(org-habit 1614 org-inlinetask 1615 org-toc org-mac-iCal org-mouse 1616 org-tempo) 1617 org-remember-default-headline "" 1618 org-special-ctrl-a/e t 1619 org-stuck-projects '("-inactive/+TODO" ("TODO" "WAITING") nil "") 1620 org-track-ordered-property-with-tag nil 1621 org-startup-indented t 1622 org-startup-folded 'show2levels 1623 1624 org-archive-location (concat org-metadir 1625 (format-time-string "archive-%Y.org") 1626 "::datetree/") 1627 org-default-notes-file (mrb/org-file "GTD.org") 1628 diary-file (concat org-metadir "DIARY") 1629 1630 ;; Heading and list behavior 1631 org-M-RET-may-split-line '((default . t) (headline)) 1632 org-insert-heading-respect-content nil 1633 org-list-allow-alphabetical t 1634 1635 ;; Blocking and clocking 1636 org-blocker-ignore-ancestor-siblings t 1637 org-clock-x11idle-program-name "xprintidle" 1638 org-closed-keep-when-no-todo t 1639 1640 ;; Export and entities 1641 org-entities-user '(("cmd" "\\cmd{}" nil "⌘" "⌘" "⌘" "⌘")) 1642 org-html-toplevel-hlevel 3 1643 org-timestamp-custom-formats '("<%m/%d/%y %a>" . "<%H:%M>") 1644 1645 ;; Agenda custom commands 1646 org-agenda-custom-commands 1647 '(("w" "Waiting For list" tags-todo "-inactive/WAITING" 1648 ((org-agenda-overriding-header "WAITING FOR-list") (org-agenda-dim-blocked-tasks t) 1649 (org-agenda-group-by-property "Responsible"))) 1650 ("b" "Buying list" 1651 ((tags-todo "-inactive+buy/-BUY" ((org-agenda-overriding-header "Buying list (tagged)"))) 1652 (tags-todo "-inactive/BUY" ((org-agenda-overriding-header "Buying list (keyword)")))) 1653 nil) 1654 ("p" "Active project list" tags-todo 1655 "-ignore-inactive+LEVEL>1-TODO=\"DONE\"-TODO=\"CANCELLED\"-TODO=\"INFO\"" 1656 ((org-agenda-overriding-header "Active project list") 1657 (org-agenda-skip-function 'mrb/skip-non-projects) (org-agenda-dim-blocked-tasks nil) 1658 (org-agenda-group-by-property "Group") (org-agenda-sorting-strategy '(alpha-up)))) 1659 ("A" "Active task list" tags-todo "+SCHEDULED=\"\"-inactive/TODO" 1660 ((org-agenda-group-by-property "Group") (org-agenda-dim-blocked-tasks 'invisible))) 1661 ("r" "To Review" 1662 ((tags-todo "SCHEDULED=\"\"+DEADLINE=\"\"-{.}/TODO" 1663 ((org-agenda-overriding-header "Untagged items"))) 1664 (tags-todo "-inactive+SCHEDULED=\"\"+DEADLINE=\"\"+TODO=\"TODO\"+{.}" 1665 ((org-agenda-overriding-header "Unscheduled active items")))) 1666 ((org-agenda-dim-blocked-tasks 'invisible))) 1667 ("c" "Scheduled overview" tags-todo "SCHEDULED<>\"\"|DEADLINE<>\"\"/TODO" 1668 ((org-agenda-overriding-header "SCHEDULED") (org-agenda-view-columns-initially t) 1669 (org-agenda-overriding-columns-format "%65ITEM %25Responsible %SCHEDULED %DEADLINE %TAGS") 1670 (org-agenda-dim-blocked-tasks t))) 1671 ("l" "Blocked projects and tasks" 1672 ((tags-todo "+BLOCKED=\"t\"/PROJ" 1673 ((org-agenda-overriding-header "Blocked projects") 1674 (org-agenda-dim-blocked-tasks t))) 1675 (tags-todo "+BLOCKED=\"t\"/TODO" 1676 ((org-agenda-overriding-header "Blocked tasks") (org-agenda-dim-blocked-tasks t) 1677 (org-agenda-group-by-property "Group")))) 1678 nil nil) 1679 ("n" "Next Action List [hides blocked/inactive/waiting/INBOX-ed]" tags-todo 1680 "+SCHEDULED=\"\"+DEADLINE=\"\"-BLOCKED=\"t\"-inactive-habit-ARCHIVE/-WAITING-INFO-HOWTO" 1681 ((org-agenda-overriding-header "Next Action List") (org-agenda-dim-blocked-tasks 'invisible) 1682 (org-agenda-group-by-property "CREATED"))) 1683 ("D" "Items ready for archiving" todo "DONE" 1684 ((org-agenda-overriding-header "Items ready for archiving") 1685 (org-agenda-group-by-property "CREATED"))) 1686 ("g" "AGENDA" 1687 ((agenda "" 1688 ((org-agenda-filter-preset '("-inactive")) (org-agenda-span 'day) 1689 (org-agenda-overriding-header "Day agenda"))) 1690 (tags-todo "carryover/-INFO-WAITING" 1691 ((org-agenda-overriding-header "Carry along list") 1692 (org-agenda-files `(,(org-journal--get-entry-path)))))) 1693 nil))) 1694 #+end_src 1695 1696 ** Capturing information 1697 I guess 90% of the information I keep in the main orgmode files starts life originally as a captured item. I use it for: 1698 1699 1. TODO items; 1700 2. BUY items; 1701 3. Journal entries; 1702 4. Logbook entries; 1703 5. Weblinks; 1704 6. Toots. 1705 1706 #+begin_src emacs-lisp 1707 (use-package org-capture 1708 :diminish 1709 1710 :custom 1711 ;; Do not do the automatic bookmarking when capturing 1712 (org-capture-bookmark nil) 1713 (bookmark-set-fringe-mark nil) 1714 1715 ;; Special config for handling tags while capturing 1716 <<org-capture-taghandling>> 1717 1718 :config 1719 ;; Configuration of all the capture templates 1720 <<org-capture-templates>> 1721 ;; and some shortcuts to run them 1722 <<org-capture-runners>> 1723 1724 ;; Not a :custom, we're making this up as we go. 1725 (setq ocpf-frame-parameters 1726 '((name . "*capture*") 1727 (width . 115) (height . 15) 1728 (tool-bar-lines . 0) (menu-bar-lines . 0) 1729 (internal-border-width . 5))) 1730 1731 (defun ocpf--capture-frame-p () 1732 "Return t if current frame is the capture frame." 1733 (equal (cdr (assoc 'name ocpf-frame-parameters)) 1734 (frame-parameter nil 'name))) 1735 1736 (defun ocpf--funcall (func &rest args) 1737 "Call FUNC in our capture frame only." 1738 (when (ocpf--capture-frame-p) 1739 (apply func args))) 1740 1741 (defun ocpf--delete-frame (&rest args) 1742 "Close capture frame." 1743 (ocpf--funcall 'delete-frame)) 1744 1745 (defun ocpf--delete-other-windows (&rest args) 1746 "Make sure we have the capture frame to ourselves." 1747 (ocpf--funcall 'delete-other-windows)) 1748 1749 (defun ocpf--window-system () 1750 "Return appropriate window system symbol for current OS." 1751 (cond ((eq system-type 'darwin) 'ns) 1752 ((eq system-type 'gnu/linux) 'x) 1753 ((eq system-type 'windows-nt) 'w32))) 1754 1755 (defun ocpf--org-capture (orig-fun &optional goto keys) 1756 "Create a new frame and run org-capture." 1757 (interactive) 1758 (let ((after-make-frame-functions 1759 (lambda (frame) 1760 (select-frame frame) 1761 (funcall orig-fun goto keys)))) 1762 (make-frame 1763 `((window-system . ,(ocpf--window-system)) 1764 ,@ocpf-frame-parameters)) 1765 (setq header-line-format nil))) 1766 1767 ;; All capture frames should have one window 1768 (add-hook 'org-capture-mode-hook 'delete-other-windows) 1769 1770 ;; Wrap org-capture in our org-capture 1771 (advice-add 'org-capture :around #'ocpf--org-capture) 1772 1773 ;; Proper clean up 1774 (advice-add 'org-capture-finalize :after #'ocpf--delete-frame)) 1775 1776 1777 1778 #+end_src 1779 1780 Here are the templates used by org-capture. The /todo/ template is the most used, it is the same as the /link/ template, but does not include a reference to the current context, which is, in most cases, just annoying. 1781 1782 #+name: org-capture-templates 1783 #+begin_src emacs-lisp :tangle no 1784 (setq 1785 org-capture-templates 1786 `(("b" "Buy" 1787 entry (function mrb/capture-location) "* BUY %?\n%(mrb/prop-or-nothing :annotation)\n" :prepend t :empty-lines 1) 1788 ("l" "Link" 1789 entry (function mrb/capture-location) "* TODO %?\n%(mrb/prop-or-nothing :annotation)\n" :prepend t :empty-lines 1) 1790 ("t" "Todo" 1791 entry (function mrb/capture-location) "* TODO %?\n" :prepend t :empty-lines 1) 1792 ("g" "Generic Log Entry" 1793 item (here) "- %u %?") 1794 ("z" "Ziggo Log Entry" 1795 item (file+olp 1796 (mrb/home-file "dat/org/GTD.org") 1797 "Partners" "Leveranciers" "Ziggo" "Logbook" ,(format-time-string "%Y")) 1798 "- %u %?"))) 1799 1800 (defun mrb/prop-or-nothing(prop) 1801 "Insert `[:type]: [:property]` when :property has a value, otherwise nothing. 1802 Meant to be used inside org-capture-templates 1803 1804 Example: `%(mrb/prop-or-nothing :annotation)`" 1805 1806 (let ((prop-value (plist-get org-store-link-plist prop)) 1807 (type (plist-get org-store-link-plist :type))) 1808 (if (equal prop-value "") 1809 "" 1810 (concat (when type (concat type ": ")) prop-value)))) 1811 1812 (defun mrb/capture-location() 1813 (interactive) 1814 "This function is meant to be used inside org-capture-templates 1815 to find a file and location where a captured ITEM should 1816 be stored." 1817 1818 ;; Open journal file without creating a journal entry This has the 1819 ;; side effect that it creates the file and moves TODO items over 1820 ;; on first call and leaves the cursor at the end of the file. 1821 (org-journal-new-entry 1) 1822 1823 ;; Find the id in this file and go there for the capture 1824 (setq loc (org-find-entry-with-id "new-todo-receiver")) 1825 (when loc 1826 (goto-char loc))) 1827 #+end_src 1828 1829 Define functions for each piece of information captured, so they can be easily bound to keys. 1830 1831 #+name: org-capture-runners 1832 #+begin_src emacs-lisp :tangle no 1833 :init 1834 (defun mrb/capture-todo () 1835 "Capture a TODO item" 1836 (interactive) 1837 (org-capture nil "t")) 1838 1839 (defun mrb/capture-buy () 1840 "Capture a BUY item" 1841 (interactive) 1842 (org-capture nil "b")) 1843 1844 (defun mrb/capture-link () 1845 "Capture a TODO item, but link to source when we can" 1846 (interactive) 1847 (org-capture nil "l")) 1848 #+end_src 1849 1850 These capture functions are called from shell scripts in the operating system and have a shortcut key assigned to them. The scripts are produced directly from this document, in a similar way as the main edit script was produced in [[Preparing for lift-off]] 1851 1852 #+begin_src sh :exports code :tangle ~/bin/capture-todo.sh :shebang #!/bin/sh 1853 edit-noframe --eval '(mrb/capture-todo)' 1854 #+end_src 1855 1856 #+begin_src sh :exports code :tangle ~/bin/capture-buy.sh :shebang #!/bin/sh 1857 edit-noframe --eval '(mrb/capture-buy)' 1858 #+end_src 1859 1860 By default =C-c C-c= ends the capture, but is normally the shortcut to enter tags, so I define a shortcut to define tags while capturing. 1861 1862 #+name: org-capture-taghandling 1863 #+begin_src emacs-lisp :tangle no 1864 :bind ( :map org-capture-mode-map 1865 ("C-c C-t" . mrb/add-tags-in-capture)) 1866 1867 :init 1868 (defun mrb/add-tags-in-capture() 1869 (interactive) 1870 "Insert tags in a capture window without losing the point" 1871 (save-excursion 1872 (org-back-to-heading) 1873 (org-set-tags-command))) 1874 #+end_src 1875 1876 ** Workflow 1877 Orgmode used a couple of thing which enable you to steer the workflow for items. Item states are the most prominent ones. Org-mode uses keyword definitions to denote states on items. I keep an [[file:org-config.org][Orgmode configuration file]] (=org-config.org)= file which contains the description of the workflow in a format suitable to include directly into orgmode files. The configuration of emacs itself is limited to dressing up this configuration with things less suitable to go into that config file. The configuration here and the org config file should be kept in sync. 1878 1879 Adapt the colors of the states I use a bit: 1880 1881 #+name: orgmode-keywords 1882 #+begin_src emacs-lisp :tangle no 1883 ;; Define face specs for our keywords, so they can be used in the 1884 ;; theme adjustment like standard faces 1885 (defface mrb/org-todo-keyword-TODO nil "") 1886 (defface mrb/org-todo-keyword-DONE nil "") 1887 (defface mrb/org-todo-keyword-WAITING nil "") 1888 (defface mrb/org-todo-keyword-CANCELLED nil "") 1889 (defface mrb/org-todo-keyword-BUY nil "") 1890 (defface mrb/org-todo-keyword-HOWTO nil "") 1891 (defface mrb/org-todo-keyword-INFO nil "") 1892 (defface mrb/org-todo-keyword-COLLECT nil "") 1893 (defface mrb/org-todo-keyword-SOLVE nil "") 1894 (defface mrb/org-todo-keyword-READ nil "") 1895 (defface mrb/org-todo-keyword-READING nil "") 1896 (defface mrb/org-todo-keyword-PLAN nil "") 1897 1898 1899 (setq org-todo-keyword-faces `( 1900 ("TODO" . mrb/org-todo-keyword-TODO) 1901 ("DONE" . mrb/org-todo-keyword-DONE) 1902 ("WAITING" . mrb/org-todo-keyword-WAITING) 1903 ("CANCELLED" . mrb/org-todo-keyword-CANCELLED) 1904 ("BUY" . mrb/org-todo-keyword-BUY) 1905 ("HOWTO" . mrb/org-todo-keyword-HOWTO) 1906 ("INFO" . mrb/org-todo-keyword-INFO) 1907 ("COLLECT" . mrb/org-todo-keyword-COLLECT) 1908 ("SOLVE" . mrb/org-todo-keyword-SOLVE) 1909 ("READ" . mrb/org-todo-keyword-READ) 1910 ("READING" . mrb/org-todo-keyword-READING) 1911 ("PLAN" . mrb/org-todo-keyword-PLAN))) 1912 #+end_src 1913 1914 Make sure we keep a clean tag slate when changing tag state. This means that when I move to an active state, remove inactive tags; if something is DONE, remove tags from it and automatically adding a 'buy' tag when a BUY item is created. Note: capturing does not honor this, i.e. when creating a new item. 1915 1916 #+begin_src emacs-lisp 1917 (setq org-todo-state-tags-triggers 1918 '(("TODO" ("inactive")) ; remove inactive tags if moved to any active state 1919 ("DONE" ("inactive") ("fork")) ; remove tags from any inactive state 1920 ("BUY" ("buy" . t)))) ; add buy tag when this is a buying action 1921 #+end_src 1922 1923 To keep the TODO list clean we archive completed entries. The archiving only occurs when an item is in the 'DONE' or 'CANCELLED' state and is not marked as a habit. A guard prevents archiving incomplete TODO items. 1924 1925 #+begin_src emacs-lisp 1926 ;; I need a modified version of org-is-habit, which takes inheritance 1927 ;; in to account 1928 (defun mrb/org-is-habit-p (&optional pom) 1929 "org-is-habit-p taking property inheritance into account" 1930 (string-equal "habit" (org-with-point-at (or pom (point)) 1931 (org-entry-get-with-inheritance "STYLE")))) 1932 1933 (setq mrb/archivable-todo-states '("DONE" "CANCELLED" "INFO")) 1934 1935 (defun mrb/can-archive-p (archivable-states) 1936 "Check if current heading can be archived. 1937 ARCHIVABLE-STATES is a list of TODO states from which archiving is allowed." 1938 (let ((todo-state (org-get-todo-state))) 1939 (member todo-state archivable-states))) 1940 1941 ;; Guard to org-archive-subtree to prevent archiving incomplete items 1942 (advice-add 'org-archive-subtree :before 1943 (lambda (&rest _args) 1944 (unless (mrb/can-archive-p mrb/archivable-todo-states) 1945 (let ((state (org-get-todo-state)) 1946 (allowed-states (string-join mrb/archivable-todo-states ", "))) 1947 (user-error "Cannot archive: item is in %s state (must be %s)" state allowed-states))))) 1948 #+end_src 1949 1950 In addition to the above I'm experimenting with =org-edna= (Extensible Dependencies 'N' Actions) which has a bit more flexibility. Notably the blocking of tasks is what I want to peruse a bit, because the builtin system hasn't worked for me. I may still use the builtin way to block items, but perhaps under the dynamic control of org-edna instead of manually specifying =order= properties etc. 1951 1952 For now, a basic config and enabling of =org-edna= 1953 1954 #+begin_src emacs-lisp 1955 (use-package org-edna 1956 :diminish 1957 :after org 1958 :config 1959 (org-edna-mode)) 1960 #+end_src 1961 1962 ** Marking items as DONE 1963 Marking work as completed should be a smooth process to stop getting in the way of doing the actual work. A shortcut is defined to mark items done in the standard way and have an additional shortcut to mark it done should it be blocked. 1964 1965 When an item changes to the DONE state, a question is asked if the item should be archived, to which the normal answer should be 'Yes' to keep the active file as clean as possible. 1966 1967 #+name: orgmode-itemactions 1968 #+begin_src emacs-lisp :tangle no 1969 ;; Macro to consolidate duplicated org-todo command logic 1970 (defmacro mrb/make-org-todo-command (name base-fn &optional state) 1971 "Create an interactive TODO command that toggles state with org-inhibit-blocking. 1972 NAME is the function name to create, BASE-FN is org-todo or org-agenda-todo, 1973 STATE is an optional target state like \"CANCELLED\"." 1974 `(defun ,name () 1975 ,(format "Toggle org TODO%s." (if state (format " to %s" state) "")) 1976 (interactive) 1977 (let ((org-inhibit-blocking t)) 1978 (funcall ',base-fn ,@(if state `(,state) nil))))) 1979 1980 ;; Create the four todo command variants 1981 (mrb/make-org-todo-command mrb/force-org-todo org-todo) 1982 (mrb/make-org-todo-command mrb/force-org-agenda-todo org-agenda-todo) 1983 (mrb/make-org-todo-command mrb/org-cancel-todo org-todo "CANCELLED") 1984 (mrb/make-org-todo-command mrb/org-agenda-cancel-todo org-agenda-todo "CANCELLED") 1985 #+end_src 1986 1987 The above may be influenced, or even made redundant, by the =org-edna= package configuration. 1988 ** Registering creation time of todo items 1989 Over time it gets a bit messy in my orgmode files. I can not remember when something was created and thus, by judging the time I didn't do anything with the item, decide if it is still important or not. 1990 1991 So, to help with that I created a little glue to make sure each actionable item gets a =CREATED= property with the date in it on which that item was created. I use the contributed =org-expiry= for that and adjust it a bit. 1992 1993 I want the property to be name 'CREATED' (I don't remember what the org-expiry default name is, but it is different) and the timestamps inserted must not be active, otherwise they'll appear all over the place in the agenda. 1994 1995 #+begin_src emacs-lisp 1996 (use-package org-contrib) 1997 (use-package org-expiry 1998 :custom 1999 (org-expiry-created-property-name "CREATED") 2000 (org-expiry-inactive-timestamps t) 2001 2002 :config 2003 <<org-expiry-createdtimestampfunction>> 2004 2005 ) 2006 #+end_src 2007 2008 So, to create the timestamp I need a little helper function which actually inserts it, using org-expiry. There is some additional cursor munging to make sure it is used comfortably during editing. 2009 To actually make this active we advice the operations that insert headers: =org-insert-todo-heading= and =org-capture= when we are actually capturing a TODO item. 2010 2011 #+name: org-expiry-createdtimestampfunction 2012 #+begin_src emacs-lisp :tangle no 2013 (defun mrb/insert-created-timestamp(&rest args) 2014 "Insert a CREATED property using org-expiry.el for TODO entries" 2015 (org-expiry-insert-created) 2016 (org-back-to-heading) 2017 (org-end-of-line)) 2018 2019 ;; for insert todo heading, always add the CREATED property 2020 (advice-add 'org-insert-todo-heading 2021 :after #'mrb/insert-created-timestamp) 2022 2023 ;; for capturing, only when state is in the TODO keywords list active in buffer 2024 (advice-add 'org-capture 2025 :after (lambda (&rest r) 2026 (when (member (org-get-todo-state) org-todo-keywords-1) 2027 (mrb/insert-created-timestamp)))) 2028 #+end_src 2029 2030 Related to the above, with some regularity I want to record timestamps, for example for documenting longer during tasks and recording incremental progress or information on them. Orgmode provides the binding '=C-c !=' for this which inserts an inactive date-stamp, optionally including the time as well. 2031 2032 Lastly, there's a mechanism in emacs which can automatically insert time-stamps, based on a placeholder in the file and a format variable. I sometimes use that, so I add it to my before save hook. Typically, the format string is file specific and will be held in a file local variable. 2033 2034 #+begin_src emacs-lisp 2035 (add-hook 'before-save-hook 'time-stamp) 2036 #+end_src 2037 2038 ** Scheduling items 2039 Orgmode has a number of provisions to schedule items, either explicitly by setting the SCHEDULE property, inferring a deadline by setting the DEADLINE property, thus scheduling the task in an interval before the deadline expires. 2040 2041 The main key for scheduling will be =C-.= (Control key with dot). A todo is marked to the next state with =M-.= so this makes sense, at least to me. In plain org and in org-agenda mode this key is used most often, but I expect this to be useful in other modes as well. I will try to u use the same keybinding in those modes as well. 2042 2043 I had /schedule-for-today/ functions earlier, but those really just save me from pressing return in a normal =org-schedule= function, so their added value was minimal and I have since deleted them. 2044 2045 ** Visual settings 2046 Having an attractive screen to look at becomes more important if you use the system all day long. /Attractive/ is rather subjective here. For me it mainly consists of functional things. Anyways, this section groups settings for the visual characteristics of orgmode. 2047 2048 I want to hide the leading stars in the out-liner, and do it *exactly* in the background color. This is redundant actually in my case, as it is also specified in the org config file that I include. Or rather, it is redundant there, because I want it always to be the case. 2049 2050 #+begin_src emacs-lisp 2051 (setq org-hide-leading-stars t) 2052 #+end_src 2053 2054 For the collapsed items in the outline orgmode uses the variable =org-ellipsis= to determine what character-sequence should be used to show that the item can be expanded. The variable can contain a string, which will then be used instead of the standard 3 dots, or a face which will then be used to render the standard 3 dots. 2055 2056 #+begin_src emacs-lisp 2057 ;; Use larger ellipsis symbol 2058 (setq org-ellipsis "⋯") 2059 #+end_src 2060 2061 There are a couple of ways within org to emphasize text inline for *bold*, /italics/, _underlined_ etc. These are set in the text by enclosing regions with delimiters. I do not want to see these delimiters, but rather render the text. The org-appear package makes this even fancier by showing the characters when inside the marked area, so editing is a lot easier. 2062 2063 #+begin_src emacs-lisp 2064 (use-package org-appear 2065 :after org 2066 :hook (org-mode . org-appear-mode) 2067 :custom 2068 (org-hide-emphasis-markers t) ; Is this used by org-appear mode? 2069 ) 2070 #+end_src 2071 2072 Similar to inline emphasis is the /rewriting/ with pretty entity characters (like '\delta' for example). These characters can be added to the text by adding a '\' before a symbol name ('delta' in the example). I make an exception for the sub- and superscript characters. This happens a lot in variable names etc. and I a big annoyance if those get rendered to subscript all the time. 2073 2074 #+begin_src emacs-lisp 2075 (setq org-pretty-entities 1) 2076 (setq org-pretty-entities-include-sub-superscripts nil) 2077 #+end_src 2078 2079 Related to that is the display of links. I want them to be explicit most of the time to avoid confusion, but the 'fancy' display is easier at first: 2080 2081 #+begin_src emacs-lisp 2082 (setq org-link-descriptive t) 2083 #+end_src 2084 2085 For most of the source blocks I want Emacs to render those blocks in their native mode. This had a serious performance problem in the past, but I think it has been solved recently. 2086 2087 #+begin_src emacs-lisp 2088 (setq org-src-fontify-natively t) 2089 #+end_src 2090 2091 For the headings at each level a =*= is normally used. As we're in unicode world now, we can do a bit better. 2092 2093 #+begin_src emacs-lisp 2094 (use-package org-bullets 2095 :hook (org-mode . (lambda () (org-bullets-mode 1)))) 2096 #+end_src 2097 2098 The item lists can be made a whole lot more attractive by attaching some icons based on the category an items belongs to. The category assignment itself is done by setting the =CATEGORY= property explicitly on the item or on the file. 2099 2100 #+name: org-agenda-visuals 2101 #+begin_src emacs-lisp :tangle no 2102 (setq org-agenda-category-icon-alist 2103 `( 2104 ("Afspraak" ,(mrb/org-file "images/stock_new-meeting.png") nil nil :ascent center) 2105 ("Blogging" ,(mrb/org-file "images/edit.png") nil nil :ascent center) 2106 ("Car" ,(mrb/org-file "images/car.png") nil nil :ascent center) 2107 ("Cobra" ,(mrb/org-file "images/car.png") nil nil :ascent center) 2108 ("DVD" ,(mrb/org-file "images/media-cdrom.png") nil nil :ascent center) 2109 ("Emacs" ,(mrb/org-file "images/emacs.png") nil nil :ascent center) 2110 ("Finance" ,(mrb/org-file "images/finance.png") nil nil :ascent center) 2111 ("Habitat" ,(mrb/org-file "images/house.png") nil nil :ascent center) 2112 ("Habit" ,(mrb/org-file "images/stock_task-recurring.png") nil nil :ascent center) 2113 ("Hobbies" ,(mrb/org-file "images/hobbies.png") nil nil :ascent center) 2114 ("Partners" ,(mrb/org-file "images/partners.png") nil nil :ascent center) 2115 ("Personal" ,(mrb/org-file "images/personal.png") nil nil :ascent center) 2116 ("Task" ,(mrb/org-file "images/stock_todo.png") nil nil :ascent center) 2117 ("Org" ,(mrb/org-file "images/org-mode-unicorn.png") nil nil :ascent center) 2118 ("Systeem" ,(mrb/org-file "images/systeembeheer.png") nil nil :ascent center) 2119 ("Wordpress" ,(mrb/org-file "images/wordpress.png") nil nil :ascent center) 2120 )) 2121 #+end_src 2122 2123 Showing items in the agenda views reacts to a number of settings. In my setup I want blocked tasks hidden, that is the reason for blocking. Hide tasks which are DONE already and a deadline is coming up, no use showing those; the same goes for tasks which are DONE and are scheduled. In short, anything that does not need my attention needs to be hidden. 2124 2125 #+begin_src emacs-lisp 2126 (setq 2127 org-agenda-dim-blocked-tasks t 2128 org-agenda-skip-deadline-if-done t 2129 org-agenda-skip-scheduled-if-done t 2130 org-agenda-skip-archived-trees nil 2131 ) 2132 #+end_src 2133 2134 After experimenting with automatic aligning of tags I've come to the conclusion I want to have it manual. For now, I set the tags to align flush right with column 110 but ideally I'd want something more dynamic (in the sense that visual-line mode is also dynamic) 2135 2136 #+begin_src emacs-lisp 2137 (setq org-tags-column -95) 2138 (defun mrb/org-align-all-tags () 2139 "Wrap org-align-tags to be interactive and apply to all" 2140 (interactive) 2141 (org-align-tags t)) 2142 (bind-key "M-\"" 'mrb/org-align-all-tags) 2143 #+end_src 2144 ** Agenda customization 2145 Settings which are just applicable for the org-mode agenda view. 2146 2147 #+begin_src emacs-lisp 2148 ;; Show properties in agenda view 2149 (use-package org-agenda-property 2150 :custom 2151 (org-agenda-property-list '("LOCATION" "Responsible"))) 2152 2153 ;; When done rendering, go to the top 2154 (add-hook 'org-agenda-finalize-hook #'(lambda () (goto-char (point-min))) 100) 2155 #+end_src 2156 2157 A snippet which resized the agenda to its window whenever its rebuilt by orgmode The 'auto value of the =org-agenda-tags-column= variable right aligns the tags in the agenda view 2158 2159 #+begin_src emacs-lisp 2160 (setq org-agenda-tags-column 'auto) 2161 (defun mrb/fit-agenda-window (&rest _) 2162 "Fit the Org Agenda to its buffer." 2163 (fit-window-to-buffer) 2164 (goto-char (point-min))) 2165 (advice-add 'org-agenda-redo :after #'mrb/fit-agenda-window) 2166 #+end_src 2167 *** Calendaring 2168 Traditionally somewhat related to mail, although mostly through orgmode these days, I want a simple setup to pull in some calendaring information. I run a radicale calendar server somewhere, but having the same info in orgmode makes sense for me. 2169 2170 I have used =org-caldav= but even in just a read only setup it does not work properly. Now rocking a setup which uses =vdirsyncer= to pull in calendaring information and =khal= and =khalel= to use that information from the command line and integrated with org-mode in Emacs. 2171 2172 Set up vdirsyncer and khal first; the khalel config in Emacs is than rather straightforward 2173 2174 #+begin_src elisp 2175 ;; `'khalel-import-events`' fills calendar.org, by default 1 Month in the past/future from today 2176 (use-package khalel 2177 :after org 2178 :custom 2179 (khalel-khal-command "khal") 2180 (khalel-vdirsyncer-command "vdirsyncer") 2181 ;; khalel-import-org-file is set in personal.el 2182 (khalel-import-org-file-confirm-overwrite nil) 2183 (khalel-import-start-date "-30d") 2184 (khalel-import-end-date "+30d") 2185 (khalel-import-format "* {title} {cancelled} \n:PROPERTIES:\n:CALENDAR: {calendar}\n:LOCATION: {location}\n:ID: {uid}\n:END:\n- When: <{start-date-long} {start-time}>--<{end-date-long} {end-time}>\n- Where: {location}\n- Description: {description}\n- URL: {url}\n- Organizer: {organizer}\n\n[[elisp:(khalel-edit-calendar-event)][Edit this event]] [[elisp:(progn (khalel-run-vdirsyncer) (khalel-import-events))][Sync and update all]]\n") 2186 :config 2187 (khalel-add-capture-template)) 2188 #+end_src 2189 2190 This creates a =calendar.org= file with events from the last and next month. Adding that file to =org-agenda-files= makes sure the events show up in orgmode agenda related views. 2191 2192 There exists a vdirsyncer timer which syncs the events with the CalDAV server to a folder, filling it with *.ics files. The command =khalel-import-events= filters those ics files into an orgmode file. 2193 2194 All events from all calendars are put into one file for now. I plan to separate the events from different calendars into different files and have tasks sync as well. 2195 2196 ** Babel / Literate programming 2197 Specific settings for babel and literate programming within org-mode 2198 2199 #+begin_src emacs-lisp 2200 (setq org-babel-load-languages 2201 '((awk . t) 2202 (calc . t) 2203 (css . t) 2204 (ditaa . t) 2205 (emacs-lisp . t) 2206 (gnuplot . t) 2207 (haskell . t) 2208 (js . t) 2209 (lisp . t) 2210 (org . t) 2211 (plantuml . t) 2212 (python . t) 2213 (scheme . t) 2214 (shell . t) 2215 (sql . t))) 2216 2217 ;; Activate Babel languages 2218 (org-babel-do-load-languages 'org-babel-load-languages org-babel-load-languages) 2219 #+end_src 2220 2221 The elements of org babel are blocks of source code. Some of the modes for sources have some helper programs. 2222 2223 Plantuml transforms diagram specifications with a /mini programming language/ into diagrams. It has support for many types of diagram. Ditaa transforms ascii diagrams into graphics. 2224 2225 #+begin_src emacs-lisp 2226 (use-package plantuml-mode 2227 :after org ; strictly not needed, but i use it mainly from org 2228 :init 2229 (setq plantuml-jar-path "/usr/share/java/plantuml/plantuml.jar") 2230 (setq org-plantuml-jar-path plantuml-jar-path) 2231 (setq plantuml-default-exec-mode 'jar)) 2232 2233 (setq org-ditaa-jar-path "/usr/share/java/ditaa/ditaa-0.11.jar") 2234 #+end_src 2235 2236 ** Refiling 2237 A big part of organizing information and task is shuffling things around. The 'thing' to throw around is a heading and 'refiling' is the term org-mode uses for throwing. 2238 2239 When filing, or capturing we want the items at the bottom of what we are filing it into. The main reason for this is that a large part of the sections that contain items are ordered. Should we file the item at the top, in many cases that would mean it is the most imminent thing to do, which is not the case. For some files, we *do* want the items on top though 2240 2241 #+begin_src emacs-lisp 2242 (setq org-reverse-note-order '(("workshop.org" . t) 2243 (".*" . nil)) ; File for others at the bottom of an entry 2244 org-refile-allow-creating-parent-nodes 'confirm 2245 org-refile-use-outline-path 'file 2246 org-outline-path-complete-in-steps nil 2247 org-refile-use-cache t) 2248 #+end_src 2249 2250 The base list of files I want to be able to refile to are: 2251 1. The /active/ file list, i.e. everything in =org-agenda-files=, regardless of these files are currently opened or not; 2252 2. All open orgmode based files which, surprisingly, I need to generate myself? 2253 2254 #+begin_src emacs-lisp 2255 (setq org-refile-targets '((org-agenda-files :maxlevel . 10))) 2256 2257 ;; Borrows from https://yiming.dev/blog/2018/03/02/my-org-refile-workflow/ 2258 (defun mrb/org-opened-buffers () 2259 "Return the list of org files opened in emacs" 2260 2261 (defun buffer-mode (buf) 2262 (buffer-local-value 'major-mode (get-buffer buf))) 2263 2264 (delq nil 2265 (mapcar (lambda (x) 2266 (if (and (buffer-file-name x) ;-buffer has a file? 2267 (provided-mode-derived-p 2268 (buffer-mode x) 'org-mode)) 2269 (buffer-file-name x))) 2270 (buffer-list)))) 2271 2272 ;; Adjust refile target to include all opened org files 2273 ;; Is it bad that we have duplicates here? 2274 (add-to-list 'org-refile-targets 2275 '(mrb/org-opened-buffers :maxlevel . 10)) 2276 2277 2278 #+end_src 2279 2280 The type of headers to refile to is, in the default orgmode config, basically everything. I limit it above to a maximum depth of 10, but then still. By using =org-refile-target-verify-function= we can fine-tune the decision whether to use a header as target or not. The following conditions have been implemented: 2281 2282 1. The header must /not/ be a DONE item; 2283 2. The header needs to have children. 2284 2285 The latter is the most important one and will prevent creating 'gathering' items to tasks themselves. 2286 2287 #+begin_src emacs-lisp 2288 (defun mrb/has-DONE-keyword() 2289 "Return t when the heading at point has a `DONE' like keyword" 2290 (member (nth 2 (org-heading-components)) org-done-keywords)) 2291 2292 (defun mrb/verify-refile-target() 2293 "Decide if a target header can be used as a refile target 2294 Conditions to return t: 2295 - header must not have one of the DONE keywords 2296 - it must be a parent of something already" 2297 ;; interactive during testing 2298 (interactive) 2299 2300 (and 2301 ; exclude done keyword headers 2302 (not (mrb/has-DONE-keyword)) 2303 ; must have a child 2304 (save-excursion (org-goto-first-child)))) 2305 2306 2307 (setq org-refile-target-verify-function 'mrb/verify-refile-target) 2308 #+end_src 2309 ** Exporting to other formats 2310 Orgmode can export to a variety of formats, I mainly use LaTeX (PDF) and HTML as destination format. 2311 2312 #+begin_src emacs-lisp 2313 (use-package htmlize) ; for html export source highlighting 2314 ;; {% raw %} 2315 (setq org-latex-title-command " " 2316 2317 org-html-htmlize-output-type 'css 2318 2319 org-latex-src-block-backend 'engraved 2320 2321 org-export-copy-to-kill-ring 'if-interactive 2322 2323 org-latex-classes 2324 '( 2325 ("article" "\\documentclass[11pt,a4paper,twoside]{article}" 2326 ("\\section{%s}" . "\\section*{%s}") 2327 ("\\subsection{%s}" . "\\subsection*{%s}") 2328 ("\\subsubsection{%s}" . "\\subsubsection*{%s}") 2329 ("\\paragraph{%s}" . "\\paragraph*{%s}") 2330 ("\\subparagraph{%s}" . "\\subparagraph*{%s}")) 2331 ("report" "\\documentclass[11pt]{report}" 2332 ("\\part{%s}" . "\\part*{%s}") 2333 ("\\chapter{%s}" . "\\chapter*{%s}") 2334 ("\\section{%s}" . "\\section*{%s}") 2335 ("\\subsection{%s}" . "\\subsection*{%s}") 2336 ("\\subsubsection{%s}" . "\\subsubsection*{%s}")) 2337 ("book" "\\documentclass[11pt]{book}" 2338 ("\\part{%s}" . "\\part*{%s}") 2339 ("\\chapter{%s}" . "\\chapter*{%s}") 2340 ("\\section{%s}" . "\\section*{%s}") 2341 ("\\subsection{%s}" . "\\subsection*{%s}") 2342 ("\\subsubsection{%s}" . "\\subsubsection*{%s}")) 2343 ("beamer" "\\documentclass{beamer}" org-beamer-sectioning)) 2344 2345 ;; No tags, todo keywords or time-stamp 2346 org-export-with-tags nil 2347 org-export-with-todo-keywords nil 2348 org-export-time-stamp-file nil 2349 org-export-backends '(ascii html icalendar latex md odt org texinfo) 2350 ) 2351 ;; {% endraw %} 2352 #+end_src 2353 2354 ** Journaling 2355 While the standard capture method is useful I found myself not using it very much. Not sure why. It turns out that org-journal is a much better fit for my workflow, especially using the carry-over functionality. 2356 2357 #+begin_src emacs-lisp 2358 (use-package org-journal 2359 :after (org) 2360 :commands (org-journal-new-entry) 2361 :hook ((org-journal-mode . turn-on-visual-line-mode) 2362 (org-journal-mode . turn-on-visual-fill-column-mode)) 2363 :bind (("C-c j" . org-journal-new-entry) 2364 ("C-s-j" . mrb/open-current-journal)) 2365 2366 :config 2367 (defun mrb/open-current-journal() 2368 "Open current journal file, applying startup folding on first visit only." 2369 (interactive) 2370 (let* ((org-journal-file (org-journal--get-entry-path)) 2371 (buf-exists (find-buffer-visiting org-journal-file))) 2372 (if buf-exists 2373 (switch-to-buffer buf-exists) 2374 ;; New entry: create buffer and apply startup folding after creation 2375 (org-journal-new-entry 1) 2376 (org-cycle-set-startup-visibility)))) 2377 2378 :custom 2379 (org-journal-dir (mrb/org-file "journal/")) 2380 2381 ;; Bring our config to every journal file 2382 (org-journal-file-header (concat "#+SETUPFILE: " user-emacs-directory "org-config.org")) 2383 2384 ;; Match the journal files (TODO make this independent of earlier assignments) 2385 (org-agenda-file-regexp "^[0-9]+\\.org") 2386 (org-journal-file-format "%Y%m%d.org") 2387 2388 ;; Put day on top of file, uses `format-time-string` 2389 (org-journal-date-format "%A, %-e-%m-%Y") 2390 2391 ;; Put day on top of file, uses `org-journal-date-format` 2392 (org-journal-date-prefix "#+TITLE: ") 2393 ;; New entries go at the bottom, make sure we are at top level 2394 (org-journal-time-format "[%R] ") 2395 (org-journal-time-prefix "* ") 2396 2397 ;; Carry over TODO items and items explicitly marked 2398 (org-journal-carryover-items "+carryover|+TODO=\"TODO\"") 2399 ;; and for this to work, we need agenda integration 2400 (org-journal-enable-agenda-integration t) 2401 ;; Remove empty journals after carryover 2402 (org-journal-carryover-delete-empty-journal 'always) 2403 2404 ;; I want to have encryption, but how do TODO items bubble up then in the agenda 2405 (org-journal-enable-encryption nil) 2406 (org-crypt-disable-auto-save t) 2407 (org-journal-enable-cache t) 2408 (org-element-use-cache t)) 2409 #+end_src 2410 2411 Particularly in journaling, but also in org-mode in general, I want to be able to quickly insert screenshots. Rather, images in general but 90% of my use-case is really screenshots. 2412 2413 There's a package =org-attach-screenshot= which matches my use-case 99% so let's use that and worry about extending it to images later on. 2414 2415 #+begin_src emacs-lisp 2416 (use-package org-attach-screenshot 2417 :bind (("C-c i" . org-attach-screenshot))) 2418 #+end_src 2419 2420 The 1% I was referring to above is that the original package exclusively supports org-mode. I patched it, which was trivial to support org-mode and all its derived modes. (org-journal in my case) 2421 2422 ** Publishing 2423 There's plenty ways to publish orgmode content on the web. I use a couple of them sparingly. For most of them I don't need a permanent configuration. For the ones that I do need a config, there's this section. 2424 2425 *** Writefreely 2426 Writefreely is a Write.as derivative and published as open source. This allows me to just kick out a quick orgmode file with a =#+TITLE:= and a =#+DATE:= header and throw it on a blog like site rather quickly. 2427 2428 #+begin_src emacs-lisp 2429 ;; Still needed: 2430 ;; - post as draft by default, this prevents automatic posting by accident for federated collections 2431 ;; - save augment -> publish / update automatically, probably a custom hook on minor mode? 2432 ;; - wf: have autorefresh GET parameter which monitors updates and 2433 ;; refreshes automatically 2434 ;; - set local save dir for the files 2435 ;; Make sure the variables of THIS file are loaded before THIS file is loaded 2436 (use-package writefreely 2437 :after org 2438 :custom 2439 ;; writefreely-instance-url and writefreely-instance-api-endpoint are set in personal.el 2440 (writefreely-maybe-publish-created-date t) 2441 ;; Auth token requires yubikey via password-store; fetch on publish, not at startup 2442 (writefreely-auth-token nil)) 2443 #+end_src 2444 2445 *** Hugo 2446 I use hugo to sporadically publish some content to my blog. Not sure if this is what I want, I post too little to spend much time on it. But here is a small config for the main posts: 2447 2448 #+begin_src emacs-lisp 2449 (use-package easy-hugo 2450 :init 2451 (setq ;; easy-hugo-basedir and easy-hugo-url are set in personal.el 2452 easy-hugo-postdir "content/post/main" 2453 easy-hugo-default-ext ".org" 2454 easy-hugo-org-header t)) 2455 #+end_src 2456 2457 ** Encrypting information in org-mode 2458 I use the /encrypt/ tag for encrypting sections in org-mode (and sometimes my journal). The sections get encrypted and decrypted automatically on saving and opening. This uses the EasyPG library to get to my GPG key. 2459 2460 #+begin_src emacs-lisp 2461 (use-package org-crypt 2462 :custom 2463 (org-crypt-tag-matcher "encrypt") 2464 (org-crypt-key user-gpg-key) 2465 2466 :config 2467 (org-crypt-use-before-save-magic)) 2468 #+end_src 2469 2470 We do not want to inherit this tag automatically, as its behavior is already subsection inclusive. When you encrypt a section, everything below it is considered content of that section and gets encrypted. I also add the value "crypt" as that is the org default, so it won't be inherited by mistake. 2471 2472 #+begin_src emacs-lisp 2473 (add-to-list 'org-tags-exclude-from-inheritance '"encrypt") 2474 (add-to-list 'org-tags-exclude-from-inheritance '"crypt") 2475 (add-to-list 'org-tags-exclude-from-inheritance '"area") 2476 #+end_src 2477 2478 ** Dynamic behaviour 2479 Not sure where to put this configuration in general. The problem I'm trying to solve is to keep only relevant files in org-agenda. Too much and the performance collapses, too little and nothing can be found. I'm trying =org-mem= for this, and perhaps add =org-node= later on. 2480 2481 =org-mem= creates a cache of metadata about org-mode files. =org-node= uses this as a library and is similar to =org-roam=, which many people tout as a game changer in todo management, so I think it is worth to dedicate some time to it. 2482 2483 First step is to have org-agenda-files managed automatically based on the cache, filtered for relevance. It does not add much to my current setup, but has the advantage to have this cache available for other purposes (finding things much/much faster is the main thing). 2484 2485 #+begin_src emacs-lisp 2486 (use-package org-mem 2487 :after org 2488 :custom 2489 ;; Basically list all folders which have org files in them. 2490 (org-mem-watch-dirs `(,org-directory "~/dat/blogs" "~/dat/caldata")) 2491 (org-mem-do-sync-with-org-id t) ;; not sure what this does exactly 2492 :config 2493 ;; Include in agenda-files: 2494 ;; - non archive named files 2495 ;; - not my org config file 2496 ;; - files with active timestamps 2497 ;; - having entries in /todo/ states 2498 ;; - having entries scheduled/deadlined 2499 (defun mrb/agenda-exclude-file-p (file) 2500 "Return t if FILE should be excluded from agenda." 2501 (or (string-search "archive" file) 2502 (string-search "org-config.org" file))) 2503 2504 (defun mrb/agenda-relevant-entry-p (entry) 2505 "Return t if ENTRY has properties that make it relevant for the agenda." 2506 (or (org-mem-entry-active-timestamps entry) 2507 (org-mem-entry-todo-state entry) 2508 (org-mem-entry-closed entry) 2509 (org-mem-entry-scheduled entry) 2510 (org-mem-entry-deadline entry))) 2511 2512 (defun mrb/set-agenda-files (&rest _) 2513 "Update `org-agenda-files' to include only files with relevant entries." 2514 (setq org-agenda-files 2515 (cl-loop for file in (org-mem-all-files) 2516 unless (mrb/agenda-exclude-file-p file) 2517 when (seq-find #'mrb/agenda-relevant-entry-p 2518 (org-mem-entries-in file)) 2519 collect file))) 2520 (add-hook 'org-mem-post-full-scan-functions #'mrb/set-agenda-files) 2521 (org-mem-updater-mode)) 2522 2523 (use-package vulpea-journal 2524 :custom 2525 (vulpea-journal-default-template 2526 '(:file-name "vulpea-journal/%Y-%m-%d.org" 2527 :title "%Y-%m-%d %A" 2528 :tags ("journal") 2529 :head "#+created: %<[%Y-%m-%d]>"))) 2530 #+end_src 2531 2532 ** Old configuration 2533 Below is what was contained in the old configuration. I will slowly migrate this into more literal sections 2534 2535 #+begin_src emacs-lisp 2536 ;; Bit of a leftover from reorganizing bits, do this later 2537 (add-to-list 'org-tags-exclude-from-inheritance '"sell") 2538 2539 (defun mrb/has-todo-subtask-p () 2540 "Return t if current heading has at least one TODO subtask." 2541 (let ((subtree-end (save-excursion (org-end-of-subtree t)))) 2542 (save-excursion 2543 (forward-line 1) 2544 (catch 'found 2545 (while (and (< (point) subtree-end) 2546 (re-search-forward "^\*+ " subtree-end t)) 2547 (when (member (org-get-todo-state) org-todo-keywords-1) 2548 (throw 'found t))))))) 2549 2550 (defun mrb/is-project-p () 2551 "Return t if the current entry is a project. 2552 A project is a heading with a TODO keyword and at least one TODO subtask." 2553 (and (member (nth 2 (org-heading-components)) org-todo-keywords-1) 2554 (mrb/has-todo-subtask-p))) 2555 2556 ;; Generic skip helpers to reduce duplication 2557 (defun mrb/skip-unless (predicate) 2558 "Skip subtree unless PREDICATE returns true. 2559 PREDICATE should be a function with no arguments that returns t or nil." 2560 (let ((subtree-end (save-excursion (org-end-of-subtree t)))) 2561 (unless (funcall predicate) subtree-end))) 2562 2563 (defun mrb/skip-when (predicate) 2564 "Skip subtree when PREDICATE returns true. 2565 PREDICATE should be a function with no arguments that returns t or nil." 2566 (let ((subtree-end (save-excursion (org-end-of-subtree t)))) 2567 (when (funcall predicate) subtree-end))) 2568 2569 ;; Helper to check for next task in subtree 2570 (defun mrb/has-next-task-p (subtree-end) 2571 "Return t if there's a TODO/BUY/WAITING task after current position within SUBTREE-END." 2572 (save-excursion 2573 (forward-line 1) 2574 (and (< (point) subtree-end) 2575 (re-search-forward "^*+ \\(TODO\\|BUY\\|WAITING\\)" subtree-end t)))) 2576 2577 ;; TODO testing for tag presence should be easier than a re-search forward 2578 ;; TODO are we not searching for all 'incomplete' type keywords here?, 2579 ;; there must be an org function for that 2580 (defun mrb/skip-non-stuck-projects () 2581 "Skip trees that are not stuck projects." 2582 (let ((subtree-end (save-excursion (org-end-of-subtree t)))) 2583 (unless (and (mrb/is-project-p) 2584 (not (mrb/has-next-task-p subtree-end))) 2585 subtree-end))) 2586 2587 (defun mrb/skip-non-projects () 2588 "Skip trees that are not projects." 2589 (mrb/skip-unless #'mrb/is-project-p)) 2590 2591 (defun mrb/skip-projects () 2592 "Skip trees that are projects." 2593 (mrb/skip-when #'mrb/is-project-p)) 2594 2595 ;; Remove empty property drawers 2596 (defun mrb/org-remove-empty-property-drawers () 2597 "*Remove all empty property drawers in current file." 2598 (interactive) 2599 (unless (eq major-mode 'org-mode) 2600 (error "You need to turn on Org mode for this function.")) 2601 (save-excursion 2602 (goto-char (point-min)) 2603 (while (re-search-forward ":PROPERTIES:" nil t) 2604 (save-excursion 2605 (org-remove-empty-drawer-at "PROPERTIES" (match-beginning 0)))))) 2606 2607 (defun mrb/org-remove-redundant-tags () 2608 "Remove redundant tags of headlines in current buffer. 2609 2610 A tag is considered redundant if it is local to a headline and 2611 inherited by a parent headline." 2612 (interactive) 2613 (when (eq major-mode 'org-mode) 2614 (save-excursion 2615 (org-map-entries 2616 '(lambda () 2617 (let ((alltags (split-string (or (org-entry-get (point) "ALLTAGS") "") ":")) 2618 local inherited tag) 2619 (dolist (tag alltags) 2620 (if (get-text-property 0 'inherited tag) 2621 (push tag inherited) (push tag local))) 2622 (dolist (tag local) 2623 (if (member tag inherited) (org-toggle-tag tag 'off))))) 2624 t nil)))) 2625 2626 2627 (defvar org-agenda-group-by-property nil 2628 "Set this in org-mode agenda views to group tasks by property") 2629 2630 (defun mrb/org-group-items-by-property (prop items) 2631 "Group ITEMS by the value of property PROP. 2632 Returns a sorted alist of (PROP-VALUE . ITEMS) pairs." 2633 (let ((buckets (make-hash-table :test 'equal))) 2634 ;; Accumulate items into hash table buckets (O(n) with O(1) lookups) 2635 (dolist (item items) 2636 (let* ((marker (get-text-property 0 'org-marker item)) 2637 (pvalue (org-entry-get marker prop t))) 2638 (puthash pvalue 2639 (cons item (gethash pvalue buckets nil)) 2640 buckets))) 2641 ;; Convert hash to alist, reverse each bucket, and sort by property value 2642 (let ((result nil)) 2643 (maphash (lambda (key items) 2644 (push (cons key (nreverse items)) result)) 2645 buckets) 2646 (sort result (lambda (i1 i2) 2647 (string< (first i1) (first i2))))))) 2648 2649 (defun mrb/org-group-agenda-finalize (orig-fun list &optional nosort) 2650 "Prepare bucketed agenda entry lists. 2651 Wraps `org-agenda-finalize-entries' to group entries by property." 2652 (if org-agenda-group-by-property 2653 ;; Collect parts in list, then join efficiently (O(n) instead of O(n²)) 2654 (let ((parts nil)) 2655 (dolist (bucket (mrb/org-group-items-by-property 2656 org-agenda-group-by-property 2657 list)) 2658 (let ((header (concat "Property " 2659 org-agenda-group-by-property 2660 " is " 2661 (or (first bucket) "<nil>") ":\n"))) 2662 (add-text-properties 0 (1- (length header)) 2663 (list 'face 'org-agenda-structure) 2664 header) 2665 (push header parts) 2666 (push (let ((org-agenda-group-by-property nil)) 2667 (org-agenda-finalize-entries 2668 (rest bucket) nosort)) 2669 parts) 2670 (push "\n\n" parts))) 2671 (mapconcat #'identity (nreverse parts) "")) 2672 (funcall orig-fun list nosort))) 2673 (advice-add 'org-agenda-finalize-entries :around #'mrb/org-group-agenda-finalize) 2674 2675 2676 (defvar mrb/org-my-archive-expiry-days 365 2677 "The number of days after which a completed task should be auto-archived. 2678 This can be 0 for immediate, or a floating point value.") 2679 2680 (defun mrb/days-since (time-string) 2681 "Return number of days elapsed since TIME-STRING." 2682 (let ((parsed (org-parse-time-string time-string))) 2683 (time-to-number-of-days 2684 (time-subtract (current-time) 2685 (apply #'encode-time parsed))))) 2686 2687 (defun mrb/heading-expired-p (state-regexp end) 2688 "Return t if heading's closed time exceeds archive expiry threshold. 2689 Search for STATE-REGEXP before END position." 2690 (when (re-search-forward state-regexp end t) 2691 (>= (mrb/days-since (match-string 2)) 2692 mrb/org-my-archive-expiry-days))) 2693 2694 (defun mrb/org-my-archive-done-tasks () 2695 "Archive DONE tasks older than `mrb/org-my-archive-expiry-days'." 2696 (interactive) 2697 (save-excursion 2698 (goto-char (point-min)) 2699 (let ((done-regexp 2700 (concat "\\* \\(" (regexp-opt org-done-keywords) "\\) ")) 2701 (state-regexp 2702 (concat "- State \"\\(" (regexp-opt org-done-keywords) 2703 "\\)\"\\s-*\\[\\([^]\n]+\\)\\]"))) 2704 (while (re-search-forward done-regexp nil t) 2705 (let ((end (save-excursion (outline-next-heading) (point)))) 2706 (goto-char (line-beginning-position)) 2707 (when (mrb/heading-expired-p state-regexp end) 2708 (org-archive-subtree))))))) 2709 2710 (defalias 'archive-done-tasks 'mrb/org-my-archive-done-tasks) 2711 #+end_src 2712 2713 * Key and mouse bindings 2714 Keyboard bindings are the primary way to interact for me. I have been struggling with consistent keyboard shortcuts and how to integrate them with the other systems on my machine which capture shortcut keys. At this time the following applications capture shortcut keys: 2715 2716 1. the awesome window manager captures keys; 2717 2. xbindkeys provides a number of key bindings for application 2718 dependent operations; 2719 3. emacs (and obviously all other applications, but those are largely 2720 irrelevant). 2721 4. the X-windows server has the kbd extension which has some keyboard 2722 related things to configure. 2723 5. The linux kernel provides key mapping, so I have to look at that 2724 place too (xmodmap) 2725 2726 Because I am daft, here is the notation for the modifiers: 2727 - C - :: control 2728 - s - :: super, meaning the (left) windows key in my configuration 2729 - M - :: meta, meaning the (left) alt key in my configuration 2730 - S - :: shift 2731 2732 To help me out with this when writing about key bindings, the lisp function =key-description= can help out, with a little bit of glue around it: 2733 2734 #+begin_src emacs-lisp 2735 (defun mrb/insert-key-description () 2736 "Insert a pretty printed representation of a key sequence" 2737 (interactive) 2738 (insert (key-description (read-key-sequence "Type a key sequence:")))) 2739 #+end_src 2740 2741 I like the explicit notation where the name of the key is spelled out better, and I'll move all configured keybindings to that eventually. 2742 2743 The right alt and the right <menu> key should be the same as the left alt and the super key, but I haven't gotten around to configuring that yet. 2744 2745 Furthermore, because I still can't remember keys, after pressing a prefix key like =C-c= the package =which-keys= can show me a formatted menu with the combinations of keys that can follow it. 2746 2747 #+begin_src emacs-lisp 2748 ; which keys shows menu with completions of prefix-key 2749 (use-package which-key 2750 :diminish 2751 :custom 2752 (which-key-idle-delay 1.8) 2753 (which-key-show-operator-state-maps t) 2754 2755 :config 2756 (which-key-mode)) 2757 #+end_src 2758 2759 Similarly, the =casual= package shows menu completions, when summoned, for several modes 2760 2761 #+begin_src elisp 2762 (use-package casual) 2763 #+end_src 2764 2765 For binding keys there are many ways it seems, with all different syntaxes and uses. I've tried to do everything with the =bind-key= package, because that is part of =use-package= so we get that for free. 2766 2767 Bind-key can define both global keys as map-based key settings and accepts all kinds of key specifications, including strings. 2768 2769 ** First, unsetting the keys I don't want. 2770 Let's begin with killing some bindings which are in my way, notably the standard right mouse click behavior. This is because I want it to behave in org-mode, which apparently sets this. I should probably find out a better way for this. 2771 2772 #+begin_src emacs-lisp 2773 (unbind-key "<mouse-3>") 2774 2775 ;; Make `C-x C-m' and `C-x RET' be different (since I tend 2776 ;; to type the latter by accident sometimes.) 2777 (unbind-key "C-x RET") 2778 #+end_src 2779 2780 ** Setting keys 2781 The standard open-line-splits the line, which is useful, but not what I want, so define a version which can open a line above or below the current line without changing the cursor position. 2782 2783 #+begin_src emacs-lisp 2784 (defun mrb/openline (arg) 2785 (interactive "P") 2786 (save-excursion 2787 (end-of-line (if arg nil 0)) ; with prefix, open line below, else above 2788 (open-line 1))) 2789 2790 ;; should prefix be for above or below open line? 2791 (bind-key "C-o" 'mrb/openline) 2792 2793 #+end_src 2794 2795 Not sure how to bind it, C-o seems kinda busy. 2796 ** Key bindings 2797 *** Global 2798 I am running the emacs daemon and sometime when I quit emacs, I want it to quit too. This sounds a bit counter-intuitive, but as long as my emacs config is moving and I am not proficient enough in making sure I can apply the changed settings reliably from within emacs, restarting emacs is just easier. This saves me from having to kill the emacs daemon from the terminal. 2799 2800 #+begin_src emacs-lisp 2801 (bind-key "C-x C-q" 'save-buffers-kill-emacs) 2802 #+end_src 2803 2804 Probably the most important key is =M-x= (as set by default). That key gives access to other commands within emacs, so it better be effective. If I wasn't already used to it, I'd certainly not consider =M-x= as a first candidate. The main objection I have is that the two keys are close to each-other, making it hard to press in a typing flow. 2805 2806 Set of bindings on which I would like to have cut, copy and paste and friends: 2807 2808 #+begin_src emacs-lisp 2809 ;; this kinda sucks now, because the rest of the OS does not do this 2810 ;; SOLUTION: learn to work with standard emacs keybinding and adjust the OS ? 2811 (bind-keys ("s-z" . undo) 2812 ("s-x" . clipboard-kill-region) 2813 ("s-c" . clipboard-kill-ring-save) 2814 ("s-v" . yank) 2815 ("s-a" . mark-whole-buffer)) 2816 #+end_src 2817 2818 Some operations on buffers: 2819 2820 #+begin_src emacs-lisp 2821 ;; Buffer handling shortcuts 2822 (bind-keys ("s-n" . (lambda () (interactive) (switch-to-buffer (generate-new-buffer "Untitled")))) 2823 ("s-s" . save-buffer) 2824 ("s-k" . kill-buffer)) 2825 #+end_src 2826 2827 And the rest, for now uncategorized: 2828 2829 #+begin_src emacs-lisp 2830 ;; Open my emacs config; I wanted something with '~' 2831 (bind-key "C-~" 'mrb/open-config) 2832 2833 ;; Font scaling, like in firefox 2834 (bind-key "C-+" 'text-scale-increase) 2835 (bind-key "C--" 'text-scale-decrease) 2836 2837 ;; Line handling functions 2838 (bind-key "s-`" 'toggle-truncate-lines) 2839 ;; Most of the time I want return to be newline and indent 2840 ;; Every mode can augment this at will obviously (org-mode does, for example) 2841 (bind-key "RET" 'newline-and-indent) 2842 2843 ;; Comment code lines, command reacts based on the major mode. 2844 (bind-key "s-/" 'comment-dwim) 2845 2846 ;; Keypad delete 2847 (bind-key [(kp-delete)] 'delete-char) 2848 2849 #+end_src 2850 **** What should have been in emacs 2851 Sometimes there are /logical gaps/ in emacs' keybinding. I put them here 2852 2853 #+begin_src emacs-lisp 2854 (bind-keys :map help-map 2855 ("A" . describe-face)) 2856 #+end_src 2857 2858 **** Special keys 2859 For some special keys I have defined some commands. Special keys are those keys that may not be on every keyboard, within reason. I consider the function keys also as special, although they do not fit the previous definition. 2860 2861 #+begin_src emacs-lisp 2862 ;; Menu key does M-x, if we have it. 2863 ;;(bind-key (kbd "<apps>") 'execute-extended-command) 2864 (bind-key "<f1>" 'help-command) 2865 (bind-key "<f2>" 'save-buffer) 2866 (bind-key "<f4>" 'find-file) 2867 #+end_src 2868 2869 **** Resizing and switching windows and frames 2870 2871 Treading cautiously here as ideally frames sizing is the responsibility of the window manager. 2872 2873 #+begin_src emacs-lisp 2874 ;; Moving back and forth in windows For now, I'm using the Fn Key + 2875 ;; Arrows, seems consistent with the other window movements 2876 (bind-key [XF86AudioNext] 'next-multiframe-window) 2877 (bind-key [XF86AudioPrev] 'previous-multiframe-window) 2878 2879 ;; Alt-Cmd left-right arrows browse through buffers within the same frame 2880 (bind-key "<M-s-left>" 'previous-buffer) 2881 (bind-key "<M-s-right>" 'next-buffer) 2882 2883 ;; These would conflict with awesome bindings, perhaps we should change those bindings 2884 ;; (bind-key "<C-S-left>" 'buf-move-left) 2885 ;; (bind-key "<C-S-right>" 'buf-move-right) 2886 2887 ;; Awesome uses Super+Arrows to move between its 'frames' 2888 ;; Emacs uses Shift-Super+Arrows to move between its windows 2889 (bind-key "<S-s-right>" 'windmove-right) 2890 (bind-key "<S-s-left>" 'windmove-left) 2891 (bind-key "<S-s-up>" 'windmove-up) 2892 (bind-key "<S-s-down>" 'windmove-down) 2893 2894 #+end_src 2895 *** For active regions 2896 In quite a few situations I would like to use keybindings which are only valid when a selection/region is active. 2897 2898 We can bind keys into the selected-keymap if we want keys to be active on every region. When only needed for a certain major mode, define a keymap named =selected-<major-mode>-map= and bind keys into that. 2899 2900 Within orgmode I'd like to have the emphasis markers react when a region is active, so let's create the necessary fragment for those bindings. These should in theory also work in org-journal mode, but they don't. 2901 2902 During mail composition, when deleting text, this should be replace by '[...]' to signal that we removed text from the original mail. 2903 2904 #+begin_src emacs-lisp 2905 (use-package selected 2906 :diminish selected-minor-mode 2907 :after (org org-journal mu4e) ; all modes we supply binding for 2908 :demand t ; So we can use global mode enable in :config 2909 :commands (selected-global-mode selected-minor-mode) ; redundant? 2910 :init 2911 (setq selected-org-mode-map (make-sparse-keymap) 2912 selected-mu4e-compose-mode-map (make-sparse-keymap)) 2913 2914 ;; Signal a deletion in compose 2915 ;; TODO do something clever with the quote level? 2916 (defun mrb/compose-mark-deletion () 2917 "Delete selected text and insert '[...]' to indicate elided content." 2918 (interactive) 2919 (delete-active-region) 2920 (insert "[...]")) 2921 2922 :bind ( :map selected-keymap ; bindings for all active regions 2923 ("C-q" . selected-off) 2924 ("C-u" . upcase-region) 2925 ("C-d" . downcase-region) 2926 :map selected-org-mode-map 2927 ("~" . (lambda () (interactive) (org-emphasize ?~))) 2928 ("_" . (lambda () (interactive) (org-emphasize ?_))) 2929 ("*" . (lambda () (interactive) (org-emphasize ?*))) 2930 ("C-b" . (lambda () (interactive) (org-emphasize ?*))) 2931 ("+" . (lambda () (interactive) (org-emphasize ?+))) 2932 ("=" . (lambda () (interactive) (org-emphasize ?=))) 2933 ("/" . (lambda () (interactive) (org-emphasize ?/))) 2934 :map selected-mu4e-compose-mode-map 2935 ("." . mrb/compose-mark-deletion)) 2936 2937 :config 2938 (selected-global-mode)) 2939 #+end_src 2940 2941 2942 ** Other key and mouse related settings 2943 Emacs has, like for everything else, a peculiar idea on scrolling and moving from screen to screen. 2944 2945 These settings work better for me. 2946 2947 I have my keyboard repeat rate rather quick; this helps by moving the cursor fast. It also means that if I press a key like backspace things disappear quite quickly, so it's important that what happens on the screen is 'real-time'. The effect I want to prevent is that when releasing the backspace key, the cursor keeps on going and deletes way more than needed. I think, by default, this is properly configured, and there used to be a variable for it, which has now been obsoleted. 2948 2949 2950 When scrolling, I don't tend to think in /half-screens/ like emacs does, I just want the text in the window to move up or down without having to guess where it's going to be. Make sure we scroll 1 line and have a small, or none at all, scroll-margin. Having both at a value of 1 is intuitive. 2951 2952 #+begin_src emacs-lisp 2953 (setq scroll-margin 1 scroll-step 1) 2954 #+end_src 2955 2956 Make sure that the scroll wheel scrolls the window that the mouse pointer is over and that we are scrolling 1 line at a time. I don't use any modifiers with the scroll wheel. 2957 2958 #+begin_src emacs-lisp 2959 (xterm-mouse-mode) 2960 (setq mouse-wheel-follow-mouse 't) 2961 (setq mouse-wheel-scroll-amount '(1 ((shift) . 1))) 2962 (setq focus-follows-mouse t) ;; i3wm changes focus automatically 2963 #+end_src 2964 2965 * Terminals, Character encodings and emulation 2966 My policy is to use Emacs for as many things and use as little other programs as necessary. All this within reason obviously. 2967 2968 This sections describes how terminals and shells are used from within Emacs. In an ideal situation I won't be needing any other terminal emulator program, other than the ones used directly from Emacs. 2969 2970 ** Terminal 2971 While eshell is the more emacsy solution, it has some limitations which makes me reach for xterm for daily work. However, there's still a good option to benefit from all the emacs configuration in that case and that is the =vterm= package. This is an interface to the =libvterm= library creating a real terminal emulation using emacs for display. 2972 2973 The basic install is easy, but does need an emacs compiled with module support. 2974 2975 #+begin_src emacs-lisp 2976 (use-package vterm 2977 ;;:straight (:type built-in) 2978 :if (fboundp'module-load) 2979 :custom-face 2980 (vterm-color-black ((t (:foreground "#2e3440")))) 2981 (vterm-color-red ((t (:foreground "#bf616a")))) 2982 (vterm-color-green ((t (:foreground "#a3be8c")))) 2983 (vterm-color-yellow ((t (:foreground "#ebcb8b")))) 2984 (vterm-color-blue ((t (:foreground "#81a1c1")))) 2985 (vterm-color-cyan ((t (:foreground "#88c0c0")))) 2986 (vterm-color-magenta ((t (:foreground "#b48ead")))) 2987 :custom 2988 (vterm-timer-delay 0) 2989 (vterm-min-window-width 0) ;; Allow vterm to adapt to any window width 2990 :config 2991 ;; Note: DFT_BACKGROUND=light produces correct difftastic colors despite dark terminal background 2992 (setenv "DFT_BACKGROUND" "light")) 2993 #+end_src 2994 2995 To benefit from some of the most useful features, there is a little bit of shell configuration involved. The package includes a shell script for that which I am sourcing in my =.zshrc= configuration file. 2996 2997 The =vterm= brings me some advantages, even close to being able to replace xterm for daily use, for example: 2998 - magit is now effectively a terminal package everywhere available 2999 - a terminal is an emacs buffer, so it's easy to have it available anywhere in emacs 3000 - most emacs commands will just work in a predictable way. 3001 3002 The most important downside is that if emacs does weird things, like hang or crash, this will affect all my terminals too, which is unworkable in some situations, so xterm is not going anywhere soon (but eshell is out I think) 3003 3004 ** Process handling 3005 Sometimes processes get stuck and i want a way to delete those processes easily. 3006 3007 #+begin_src emacs-lisp 3008 (defun mrb/delete-process-interactive () 3009 "Based on an auto-completed list of process, choose one process to kill" 3010 (interactive) 3011 (let ((pname (completing-read "Process Name: " 3012 (mapcar 'process-name (process-list))))) 3013 (delete-process (get-process pname)))) 3014 #+end_src 3015 ** Log files 3016 Although handling log files is still best done in an actual terminal, not emacs, sometimes there is a need for it. The =journalctl= package is configured for that. 3017 3018 #+begin_src elisp 3019 (use-package journalctl-mode 3020 :bind (("C-x j" . journalctl))) 3021 #+end_src 3022 3023 * Completion 3024 3025 There are 2 types of completion: 3026 1. Input completion in the minibuffer 3027 2. Inline completion in another buffer 3028 3029 I want completion to work as follows: 3030 3031 1. completion functions are always bound to a keybinding involving the TAB-key, with as little modifiers as possible; 3032 2. completion should *always* produce *something*, even if emacs has no special semantic knowledge of the current mode, it should produce /something/ which makes sense; 3033 3. completion should be inline whenever possible. 3034 4. for each mode, a specialization is ok, if that improves the situation; I expect to have many specializations to improve the auto-complete quality; 3035 5. if a completion window *must* be opened, do this at the same place always and do not mess up other windows. 3036 6. Completion should behave somewhat like real-time systems. An answer *must* be produced within a certain amount of time. If a completion answer takes longer than the amount of type to type it in full, the system has collapsed, so the time needs to be in the order of one third of the typing time. 3037 3038 The next sections deal with the above requirements 3039 ** Ad 1. Bind completion always involves TAB-key 3040 The package =smart-tab= seems to fit this bill, but the thing that I care about can be achieved fine without it (I only found this out after considerable time using smart-tab). 3041 3042 So, tab tries to indent, which is the main expectation, and if it can't it tries to complete stuff. 3043 3044 #+begin_src emacs-lisp 3045 (setq tab-always-indent 'complete) 3046 #+end_src 3047 3048 There are some situations where tab does completion (which is good), but the type of completion is not what I want (which is bad). Currently this includes only =quail-completion= of which I don't even know what it is for. This code prevents having a quail completion window when pressing tab inline. 3049 3050 #+begin_src elisp 3051 ; Bit of a kludge, but when quail loads, get rid of the completion 3052 (with-eval-after-load 'quail (defun quail-completion ())) 3053 #+end_src 3054 3055 In a standard emacs installation, TAB indents, depending on mode obviously. If indenting would not make sense, a TAB can be inserted or completion could start. The function =completion-at-point= is used in some situations. Ideally the =corfu-complete= function could take over in many cases. Here's a simplistic approach to get me started: 3056 1. if in minibuffer, do completion there like we are used to; 3057 2. if cursor is at the end of a symbol, try to complete it with corfu; 3058 3. else, indent according to mode. 3059 3060 This is probably incomplete or wrong even in some cases, but it's a start. 3061 3062 This way, TAB always does completion or indent, unless corfu-mode is not active. 3063 3064 ** Ad 2. Completion should always produce something 3065 Not sure if there is anything to do here. 3066 ** Ad 3. Inline completion when possible 3067 With inline completion I mean without opening a whole new **Completions** window if not needed. 3068 3069 Content that I want: 3070 - languages: lisp, python, ruby, bash, C/C++ roughly in that order (function and argument completion) 3071 - for all languages, function/method signature shorthands 3072 - speed, slowness is killing here 3073 - prevent minibuffer distractions, put info where my eyes are and that is the cursor in most cases. 3074 - maybe: spelling suggestions 3075 - nick completion in irc channels 3076 3077 Candidates: 3078 - auto-complete :: http://cx4a.org/software/auto-complete/ 3079 - company-mode :: http://company-mode.github.io 3080 - corfu-mode :: https://github.com/minad/corfu 3081 3082 All of these work fine, but corfu gets installed because it fits right in with the rest of the completion packages and does more than enough for what I need. Given the quick popup is nice to complete inline, but when still confused, press =M-m= to move the completion list to the minibuffer (or whatever vertico has been configured to) to show the list and have more info on the items in the list, while competion still continues. 3083 3084 #+begin_src emacs-lisp 3085 (use-package corfu 3086 :custom 3087 (corfu-auto t) 3088 (corfu-auto-delay 0.5) ; increased from default 0.2s to reduce typing lag 3089 (corfu-quit-at-boundary 'separator) 3090 (corfu-cycle t) 3091 3092 3093 :bind ( :map corfu-map 3094 ("M-m" . mrb/corfu-move-to-minibuffer)) 3095 :init 3096 ;; Enable it globally 3097 (global-corfu-mode 1) 3098 3099 ;; But not for some 3100 (setq global-corfu-modes 3101 '(message-mode 3102 (not text-mode) 3103 t))) 3104 #+end_src 3105 3106 ** Ad 4. Mode specialization 3107 There's always exceptions to the rule; with Emacs doubly so. Here's the relevant exceptions for my configuration. 3108 3109 ** Ad 5. Open completion window predictably 3110 The configuration of the completion is handled by vertico now, so nothing needs to be done here. 3111 3112 For most of the completion that is needed which is not inline (for which I am using the =corfu= package above, helm seems to be the most powerful solution, but feels isolated. For every integration something extra seems to be needed. =Vertico= seems the completing framework that fits the integration requirement best; it clearly states so in its goal to only depending on the Emacs API and not create a new one. Integrating it with others looks a lot more orthogonal than Helm. 3113 3114 #+begin_src emacs-lisp 3115 (use-package vertico 3116 :demand t 3117 :straight (vertico :files (:defaults "extensions/*") 3118 :includes (vertico-reverse vertico-multiform)) 3119 3120 :custom-face 3121 (vertico-current ((t (:background "#5e81ac")))) 3122 :config 3123 (vertico-mode)) 3124 #+end_src 3125 3126 As =M-x= or =execute-extended-command= is probably the most used command in Emacs, I want to give it a bit more prominent place on the scree than just the tiney minibuffer at the bottom. The =vertico-posframe= package turns the minibuffer into a floating popup sort of screen by using posframe. This puts the minibuffer more in my face and gives some better options to present the command information. 3127 3128 #+begin_src emacs-lisp 3129 (use-package vertico-posframe 3130 ;; Customize border face to same color as cursor 3131 :custom-face 3132 (vertico-posframe-border ((t (:background "DarkOrange" :inherit default)))) 3133 :config 3134 (setq vertico-posframe-parameters 3135 `((left-fringe . 18) 3136 (right-fringe . 18) 3137 (border-width . 4) 3138 (border-color . ,mrb/cursor-color) 3139 (child-frame-border-width . 4))) 3140 3141 (setq vertico-multiform-commands 3142 '((consult-line posframe (vertico-posframe-fallback-mode . vertico-buffer-mode)) 3143 (t posframe))) 3144 3145 (vertico-multiform-mode 1)) 3146 #+end_src 3147 3148 The first thing to complement =vertico= is the =orderless= package. My main use is to have the matching for completing items match 'first second' as well as 'second first' regardless of their order. Note that this works for every completion, also within =corfu= which is especially useful. 3149 3150 #+begin_src emacs-lisp 3151 (use-package orderless 3152 :custom-face 3153 (orderless-match-face-0 ((t (:foreground "dark orange")))) 3154 :custom 3155 (completion-styles '(orderless basic))) 3156 #+end_src 3157 3158 The default list of candidates can be nicely augmented with =marginalia= which shows extra info about the matching items in the right aligned margin. 3159 3160 #+begin_src emacs-lisp 3161 (use-package marginalia 3162 :custom 3163 (marginalia-align 'right) 3164 3165 :init 3166 (marginalia-mode)) 3167 #+end_src 3168 3169 The =consult= package exposes a number of =consult-= commands which work with the completing framework and typically put an original command on steroids. Some of the commands support live previewing while browsing the selections. 3170 3171 #+begin_src emacs-lisp 3172 (use-package consult 3173 ;; Replace some bindings with a consult command, so we get things on steroids 3174 :bind (("C-x b" . consult-buffer))) 3175 #+end_src 3176 3177 Embark is the last of the set, and adds a new level of interaction. The basic principle normally in Emacs is that you specify a command and then tell what to act on. Like opening a file is the `find-file` command and then specifying what file to find. 3178 3179 Embark is the other way around. Something is active, depending on context and `embark-act` give a list of things you can do with it. 3180 3181 #+begin_src emacs-lisp 3182 3183 (use-package embark 3184 :straight (embark :files (:defaults "embark-org.el")) 3185 3186 :bind 3187 (("C-." . embark-act) ;; pick some comfortable binding 3188 ("C-;" . embark-dwim) ;; good alternative: M-. 3189 ("C-h B" . embark-bindings)) ;; alternative for `describe-bindings' 3190 3191 :init 3192 ;; Optionally replace the key help with a completing-read interface 3193 (setq prefix-help-command #'embark-prefix-help-command) 3194 3195 :config 3196 ;; Hide the mode line of the Embark live/completions buffers 3197 (add-to-list 'display-buffer-alist 3198 '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*" 3199 nil 3200 (window-parameters (mode-line-format . none))))) 3201 3202 ;; Consult users will also want the embark-consult package. 3203 (use-package embark-consult 3204 :ensure t ; only need to install it, embark loads it after consult if found 3205 :hook 3206 (embark-collect-mode . consult-preview-at-point-mode)) 3207 #+end_src 3208 ** Ad 6. Guaranteed response-time 3209 I haven't found a solution for this yet, but also have not found it to be a problem that needs solving in practice so far. 3210 3211 * Editing control 3212 Editing control are generic features which make editting easier, faster, more productive etcetera 3213 3214 Techniques in use by me: 3215 - multiple cursors :: doing the same edit actions over multiple locations at once 3216 - templates :: using a /shortcut/ or /abbreviation/ to insert templates/forms to fill in common constructs 3217 3218 Multiple cursors is a package which turns 1 cursor into many, depending on the active region, and enters insertions at all cursor locations. 3219 3220 #+begin_src emacs-lisp 3221 (use-package multiple-cursors 3222 :bind (("C-c e" . 'mc/edit-lines))) 3223 #+end_src 3224 3225 Tempel is a templating system, with templates being lisp data, which hooks into modes and autocompletion system. The folder =~`emacs-user-dir`/templates= holds =*.eld= files which specify templates which get autoloaded based on the major mode of the current buffer. 3226 3227 I set it up for relevant modes as a CAPF handler (for programming and orgmode for now) 3228 #+begin_src emacs-lisp 3229 (use-package tempel 3230 :custom 3231 ;; This would start completion directly, which I do NOT want 3232 (tempel-trigger-prefix nil) 3233 (tempel-path `(,(mrb/emacs-file "templates/*.eld"))) 3234 3235 :bind (("M-<insert>" . tempel-complete)) 3236 3237 :after (org) 3238 3239 :init 3240 3241 (setq org-tab-before-tab-emulation-hook nil) 3242 (defun mrb/tempel-setup-capf() 3243 ;; Remove org structured templates, which are just tempo templates as well 3244 ;; TODO make this smarter? (there might be other things in there which I *do* need) 3245 (setq org-tab-before-tab-emulation-hook nil) 3246 (setq-local completion-at-point-functions 3247 (cons #'tempel-complete ; use tempel-expand if only exact matches wanted 3248 completion-at-point-functions))) 3249 3250 ;; Do template name completion in programming, textmodes and orgmode 3251 ;; TODO why not global? 3252 :hook ((prog-mode text-mode org-mode) . mrb/tempel-setup-capf)) 3253 #+end_src 3254 3255 Formatting a file properly is trusted by indent rules, this one indents the whole buffer at once, making formatting completely proper 🫠 3256 3257 #+begin_src elisp 3258 (defun mrb/indent-whole-buffer () 3259 "Indent the entire buffer without affecting point or mark." 3260 (interactive) 3261 (save-excursion 3262 (save-restriction 3263 (indent-region (point-min) (point-max))))) 3264 3265 (global-set-key (kbd "C-c TAB") 'mrb/indent-whole-buffer) 3266 #+end_src 3267 ** Navigation 3268 Navigating pieces of text effectively is probably the best optimization to make in the emacs configuration. 3269 3270 In my browser I use vimium to jump to links with the keyboard. In emacs =avy= does something similar. On pressing the hotkey, all matches of the first char typed are highlighted (frame wide) by a single character. By pressing that character the cursor is place there directly. This makes =within-frame= navigation a 3-key operation, which is considerably faster than anything else. 3271 3272 #+begin_src emacs-lisp 3273 ;; Bind super-j globally to jump quickly to anything in view 3274 (use-package avy 3275 :bind (("s-j" . 'avy-goto-char))) 3276 #+end_src 3277 In a more general sense, =evil-mode=, the VI emulation layer on top of emacs, is the ultimate navigational package. I have tried this for about 6 months, but it's not for me (yet). 3278 3279 ** Searching 3280 Incremental search (isearch) is a core navigation tool for finding text in buffers. The default faces don't provide enough visual distinction between the current match and other matches. With better color contrast, it's immediately clear which match is selected and where the other matches are located. 3281 3282 #+begin_src emacs-lisp 3283 ;; Improve search highlighting to clearly distinguish current match from others 3284 (use-package isearch 3285 :straight (:type built-in) 3286 :custom-face 3287 ;; Current match: bright yellow text on dark background for maximum visibility 3288 ;; Other matches: bright purple text on dark background for clear distinction 3289 (isearch ((t (:foreground "#f0d77d" :background "#2e3440")))) 3290 (lazy-highlight ((t (:foreground "#b988dd" :background "#2e3440"))))) 3291 #+end_src 3292 3293 ** Outlining 3294 Outline mode provides a way to structure text hierarchically. The outline heading levels use distinct colors to visually separate different depth levels, making it easy to navigate document structure at a glance. 3295 3296 Provide base faces for the outline levels, packages supporting some form of outlining should inherit from these. 3297 3298 #+begin_src emacs-lisp 3299 (use-package outline 3300 :straight (:type built-in) 3301 :custom-face 3302 (outline-1 ((t (:foreground "#5e81ac")))) 3303 (outline-2 ((t (:foreground "#a3be8c")))) 3304 (outline-3 ((t (:foreground "#b48ead")))) 3305 (outline-4 ((t (:foreground "#81a1c1")))) 3306 (outline-5 ((t (:foreground "#ebcb8b")))) 3307 (outline-6 ((t (:foreground "#8fbcbb")))) 3308 (outline-7 ((t (:foreground "#d08770")))) 3309 (outline-8 ((t (:foreground "#bf616a"))))) 3310 #+end_src 3311 3312 * Remote editing 3313 As my default shell is =zsh= tramp does not work out of the box for me. It gets confused by my prompt, so on top of my =.zshrc= file I have the following line: 3314 3315 #+begin_src sh 3316 # Immediately bail out if we are coming in with a dumb terminal 3317 # which, in my case, mostly means TRAMP is asking for something. 3318 [[ $TERM == "dumb" ]] && unsetopt zle && PS1='$ ' && return 3319 #+end_src 3320 3321 Once that is in place files can be opened with =/ssh:/host:/path/to/file= syntax. Surprisingly, the syntax I got used to in the past (i.e. =/host:/path/to/file=) does not work for me anymore. 3322 3323 #+begin_src emacs-lisp 3324 (use-package tramp 3325 :straight (:type built-in) ; tramp is closely tied to emacs, use builtin 3326 :custom 3327 (tramp-default-method "sshx") 3328 (tramp-syntax 'default) 3329 (tramp-terminal-type "dumb") 3330 (tramp-default-proxies-alist '(("^ha$" "^root$" "/sshx:ha:"))) 3331 3332 :config 3333 (setenv "SHELL" "/bin/sh") 3334 (add-to-list 'tramp-connection-properties 3335 (list ".*" "locale" "LC_ALL=C"))) 3336 3337 #+end_src 3338 * Browser integration 3339 My default browser, as set in the OS, should be started automatically on opening =http:= like stuff and =html= files. The only link to the /outside world/ is specifying that =xdg-open= should figure out what program to use. 3340 3341 #+begin_src emacs-lisp 3342 (setq browse-url-browser-function 'browse-url-generic) 3343 (setq browse-url-generic-program "xdg-open") 3344 #+end_src 3345 3346 Not exactly browser integration, but 'openstreetmap browsing' is close enough. I would like to use this for planning trips, so something like a 'set of view settings' for this package would be nice. This would allow me to gather maps views, augment them with a gpx route and bookmarks. Not sure if gpx waypoints/POI sets would work. 3347 3348 The package as is, is already useful for searching locations and storing links to locations. 3349 3350 #+begin_src emacs-lisp 3351 (use-package osm 3352 :straight (:host github :repo "minad/osm") 3353 :commands (osm osm-mode) 3354 :bind ( :map osm-mode-map 3355 ("q" . (lambda() (interactive) (quit-window t)))) 3356 :init 3357 ;; Load Org link support 3358 (with-eval-after-load 'org 3359 (require 'osm-ol)) 3360 3361 :custom 3362 (osm-outdoor-url 3363 (concat "https://tile.thunderforest.com/outdoors/{%z}/{%x}/{%y}.png?apikey=" 3364 (password-store-get "API/tile.thunderforest.com"))) 3365 (map5-base (concat "https://s.map5.nl/map/" 3366 (password-store-get "API/map5.nl") 3367 "/tiles/")) 3368 (osm-map5-opentopo-url 3369 (concat map5-base "opentopo/EPSG900913/{%z}/{%x}/{%y}.jpeg")) 3370 3371 (osm-map5-opensimpletopo-url 3372 (concat map5-base "opensimpletopo/EPSG900913/{%z}/{%x}/{%y}.jpeg")) 3373 3374 (osm-map5-openlufo-url 3375 (concat map5-base "openlufo/EPSG900913/{%z}/{%x}/{%y}.jpeg")) 3376 3377 (osm-server-list 3378 `((default 3379 :name "Mapnik" 3380 :description "Standard Mapnik map provided by OpenStreetMap" 3381 :url "https://%s.tile.openstreetmap.org/%z/%x/%y.png" 3382 :group "Standard") 3383 (outdoor :name "Outdoor" 3384 :description "Outdoor focussed maps" 3385 :url ,osm-outdoor-url 3386 :group "Personal") 3387 (opentopo :name "OpenTopo NL" 3388 :description "Map5.nl OpenTopo" 3389 :url ,osm-map5-opentopo-url 3390 :group "Personal") 3391 (opensimpletopo :name "OpenSimpleTopo NL" 3392 :description "Map5.nl OpenSimpleTopo" 3393 :url ,osm-map5-opensimpletopo-url 3394 :group "Personal") 3395 (openlufo :name "OpenLufo NL" 3396 :description "Map5.nl OpenLufo" 3397 :url ,osm-map5-openlufo-url 3398 :group "Personal"))) 3399 3400 ;; set proper home location, TODO read this from private store 3401 (osm-home '(51.6441759 4.4377029 17)) 3402 (osm-copyright nil)) 3403 #+end_src 3404 * Messaging and chatting 3405 ** Mail 3406 This section describes a search for the mail setup I want in Emacs. There are a few mail related packages and I'm unsure at what I want to use; reading the feature list doesn't cut it. So, I'm having a setup where multiple mail handling packages may exist and hopefully one will float to the top as being *the one* 3407 3408 *** Composing mail 3409 Composing mail is often an /out of band/ activity, like creating a tweet or a capture, so I would like to have roughly the same behavior. This is by default provided by compose-mail-other-frame, which in turn calls the right mua to do the job. Oddly enough, it is still required to have =mu4e-compose-in-new-frame= set to actually have another frame, so I'm writing it with mu4e-compose directly. 3410 3411 #+name: mu4e-composeconfig 3412 #+begin_src emacs-lisp :tangle no 3413 :bind (("C-x m" . 'mrb/compose-mail) 3414 ( :map mu4e-compose-mode-map 3415 ("C-;" . mu4e-compose-context-switch))) 3416 3417 :custom 3418 (mu4e-compose-format-flowed t) 3419 (mu4e-compose-switch nil) 3420 (mu4e-compose-dont-reply-to-self t) 3421 (mu4e-compose-complete-only-after nil) ; no limit on contact completion for now 3422 3423 :config 3424 (defun mrb/compose-mail (&optional mailto-url) 3425 "Run mail-compose, use mailto URI if it is given." 3426 (interactive) 3427 3428 ;; If we have a mailto argument, parse and use it 3429 (if (and (stringp mailto-url) 3430 (string-match "\\`mailto:" mailto-url)) 3431 (browse-url-mail mailto-url) 3432 ;; No mailto, argument, just run the mua 3433 (mu4e-compose-new))) 3434 3435 (defun mrb/mailfile() 3436 "Compose mail message from current buffer file, typically a pdf is viewed for my use-case" 3437 (interactive) 3438 (if-let ((file buffer-file-name)) 3439 (let ((mimetype (mm-default-file-type file))) 3440 (mu4e-compose-new) 3441 (mml-attach-file file mimetype (concat mimetype " attachment") "attachment")) 3442 (user-error "Current buffer is not visiting a file"))) 3443 3444 ;; Context switching in compose 3445 <<mu4e-context-switch-in compose>> 3446 3447 ;; Extend attaching files with extra info insertion 3448 <<extend-attach>> 3449 3450 3451 #+end_src 3452 3453 To be able to use the =mrb/compose-mail= function as a mailto handler we need to be able to call it from outside of emacs. Let's define a small shell script that does exactly this. The SRC attributes tangle it into my =bin= directory where is will be in the path. In the OS, this script will need to be set as the default mail handler. 3454 3455 #+begin_src sh :exports code :tangle ~/bin/mailto-handler :shebang #!/bin/bash 3456 # Emacs mailto URI handler 3457 3458 # Make sure mailto: is always prepended to $1 3459 mailto="mailto:${1#mailto:}" 3460 mailto=$(printf '%s\n' "$mailto" | sed -e 's/[\"]/\\&/g') 3461 3462 # Call the elisp function handling our mailto URI 3463 elisp_expr="(mrb/compose-mail \"$mailto\")" 3464 3465 # Do not create new frame, because mu4e does that already 3466 # which is a simpler solution in this case 3467 edit-noframe --eval "$elisp_expr" 3468 #+end_src 3469 3470 On top of that, I want a manual capture script which is basically the same as the mailto handler. 3471 #+begin_src sh :exports code :tangle ~/bin/capture-mail.sh :shebang #!/bin/sh 3472 edit -e '(mrb/compose-mail)' 3473 #+end_src 3474 3475 **** Attachments 3476 When attaching files, it is sometimes useful to extract some info from the attached file and include it in the message. My main usecase is extracting PDF annotations (mostly for people who cannot process the annotations in their PDF viewer directly. 3477 3478 The way I have implemented this is by advicing the function =mml-insert-empty-tag= which is used by the =mml-attach-file= function. At that point in the code the file and point in the active buffer are known, and we just have to process the extra actions. 3479 The advice function extracts the annotations from the pdf file (unconditionally at the moment) and inserts a rendering of them just before inserting the attachment itself. 3480 3481 #+name: extend-attach 3482 #+begin_src emacs-lisp :tangle no 3483 :config 3484 ;; TODO return info, do not use insert directly, so we have more control 3485 (defun mrb/pdf-extract-info (file) 3486 "Extract and render the pdf annotations in FILE" 3487 (mapc 3488 (lambda (annot) ;; traverse all annotations 3489 (progn 3490 (let ((page (cdr (assoc 'page annot))) 3491 (highlighted-text 3492 (if (pdf-annot-get annot 'markup-edges) 3493 (let ((highlighted-text 3494 (pdf-info-gettext (pdf-annot-get annot 'page) 3495 (pdf-tools-org-edges-to-region 3496 (pdf-annot-get annot 'markup-edges)) 3497 t 3498 file))) 3499 (replace-regexp-in-string "\n" " " highlighted-text)) 3500 nil)) 3501 (note (pdf-annot-get annot 'contents)) 3502 (type (pdf-annot-get annot 'type))) ; underline, strike-through etc. 3503 ;; Stuff gathered, start rendering 3504 (when (or highlighted-text (> (length note) 0)) 3505 (insert (format "\n- page %s" page)) 3506 3507 (when highlighted-text 3508 (insert (format ": “%s”\n" highlighted-text))) 3509 3510 (if (> (length note) 0) 3511 (insert (format "\n %s\n" note)) 3512 (insert "\n" )))))) 3513 ;; list to traverse, second arg of mapc 3514 (cl-remove-if ; don't process links? 3515 (lambda (annot) (member (pdf-annot-get-type annot) (list 'link))) 3516 ;; file is typically already visited 3517 (pdf-annot-getannots nil '() (find-file-noselect file))) 3518 )) 3519 3520 3521 (defun mrb/extended-attach-file (name &rest plist) 3522 "Advice `:before `mml-insert-empty-tag to grab additional info from the attachment." 3523 (let* ((file (plist-get plist 'filename)) 3524 (mimetype (if file (mm-default-file-type file) nil))) 3525 (when (string= mimetype "application/pdf") 3526 ;; Extract and render annotations 3527 (mrb/pdf-extract-info file)))) 3528 3529 (advice-add 3530 'mml-insert-empty-tag ; a lot easier because we already know the file 3531 :before 3532 'mrb/extended-attach-file 3533 '((name . "extended-attach"))) 3534 #+end_src 3535 3536 **** Footnotes 3537 While composing mail referring to external resources through links is done very often. While creating links in HTML mail, which I do not use, gives the option to name a link, there's no such thing in plain text. This usually works out fine, but when there are many links I fall back to creating 'footnotes' which actuall hold the links while in the text there is just an identifier to point the reader to the proper footnote. 3538 3539 Of course these footnotes can be used for other things as well. 3540 3541 Here's what I would like: 3542 3543 - easy shortcut to start adding a footnote 3544 - easy shortcut to return to the main text 3545 - insert the proper numbers automatically, optionally renumber when I put an extra in later on. 3546 - insert superscript like \sup1 or \sup2 into the text 3547 - insert captured content at bottom of current text (but not below quotes) 3548 3549 Footnote mode seems to do exactly that, configured properly. Its prefix key is a bit awkward, so I have changed it and having the unicode parentheses for start and end tag is not ideal, but the package requires there to be /something/; I'd prefer there to be /nothing/ but that messes up renumbering and other operations. 3550 3551 - Key shortcuts are a bit awkward, as ! is awkward 3552 A: changed the prefix to ="C-c n"= which makes more sense 3553 3554 - auto enable in message mode, mu4e compose 3555 3556 #+begin_src emacs-lisp 3557 (use-package footnote 3558 :straight (:type built-in) 3559 3560 ;; Use a better prefix than the default 'C-c !' 3561 :bind-keymap ("C-c n" . footnote-mode-map) 3562 3563 :hook (message-mode . footnote-mode) ; I use it in mu4e 3564 :config 3565 (setq footnote-section-tag "" ; No introduction needed? 3566 footnote-start-tag "⁽" ; doc says not to use empty string 3567 footnote-end-tag "⁾" 3568 footnote-style 'unicode ; this makes footnotes use superscript numbering 3569 footnote-body-tag-spacing 1 ; between the \sup1 and the footnote text 3570 )) 3571 #+end_src 3572 *** Sending mail 3573 Sending mail through smtp, using the smtpmail package 3574 3575 #+begin_src emacs-lisp 3576 (use-package smtpmail 3577 :straight (:type built-in) 3578 :custom 3579 (smtpmail-default-smtp-server "localhost") 3580 (smtpmail-service 25) 3581 (smtpmail-local-domain user-domain) 3582 (smtpmail-sendto-domain user-domain) 3583 ;; User agent style is message mode by default, specific mail packages may override this 3584 (mail-user-agent 'message-user-agent) 3585 (send-mail-function 'smtpmail-send-it) ;This is for mail-mode 3586 (message-send-mail-function 'message-smtpmail-send-it) ; This is for message-mode 3587 3588 (password-cache t) ; default is true, so no need to set this actually 3589 (password-cache-expiry 28800)) ; default is 16 seconds, which is ridiculously low 3590 #+end_src 3591 3592 At times we want to send signed and/or encrypted mail 3593 3594 #+begin_src emacs-lisp 3595 (use-package mml-sec 3596 :straight (:type built-in) 3597 :config 3598 ;; Use my key to sign messages and make it safe if other keys exist 3599 (add-to-list 'mml-secure-openpgp-signers user-gpg-key) 3600 (setq mml-secure-key-preferences 3601 '((OpenPGP 3602 (sign) 3603 (encrypt 3604 (user-mail-address user-gpg-key))) 3605 (CMS 3606 (sign) 3607 (encrypt)))) 3608 3609 ;; Add my bcc address to list of safe addresses in bcc for secure message 3610 (add-to-list 'mml-secure-safe-bcc-list mrb/bcc-address) 3611 3612 ;; Always encrypt to self, so I can read my own messages 3613 (setq mml-secure-openpgp-encrypt-to-self `(,user-gpg-key))) 3614 #+end_src 3615 3616 *** Generic mail message settings 3617 It's not entirely clear which package is exactly responsible for what, but there are a few settings which are related to the =message= package 3618 3619 #+begin_src emacs-lisp 3620 (use-package message 3621 :demand t ; mu4e won't load otherwise 3622 :straight (:type built-in) 3623 3624 :init 3625 (defun mrb/message-mode-hook () 3626 "This configures message mode and thus other modes which inherit message mode." 3627 ;; We have to make sure that the messages we produce are 3628 ;; format=flowed compatible. This happens on encoding when sending 3629 ;; it. However, during compose we need to make sure that what we 3630 ;; offer to encode is suitable for it. 3631 3632 ;; Let's do the compose part first No reflowing is possible without 3633 ;; having hard newlines, so lets always enable those 3634 (use-hard-newlines t 'always) 3635 3636 ;; Use visual line mode and visually break at word boundaries which 3637 ;; is the same as the email encoding on send (see below) 3638 (visual-line-mode 1) 3639 (auto-fill-mode 0) ; Makes no sense in visual mode 3640 (setq-local adaptive-fill-mode nil) ; This messes up fill-flowed-encode 3641 3642 (setq visual-fill-column-width nil) ; Make sure `fill-column gets used 3643 (setq fill-column 80) ; Not the same as encode column below! 3644 (visual-fill-column-mode 1) ; Show it 3645 3646 ;; Next, do the encoding and sending part 3647 ;; Set the fill column on encoding (send) 3648 ;; 66 would be the proper value, but to help rendering 3649 ;; on the other end 998 is the max which helps a bit 3650 (setq fill-flowed-encode-column 66) 3651 3652 ;; This enables the f=f encoding for sending 3653 (setq mml-enable-flowed t)) 3654 3655 :hook (message-mode . mrb/message-mode-hook) 3656 3657 :config 3658 (setq 3659 message-signature-directory mrb/maildir 3660 message-signature-file mrb/default-signature-file 3661 3662 ;; Register my alternative email-adresses, other than the default 3663 ;; This steers From header when replying and as such has some overlap with mu4e contexts 3664 message-alternative-emails (regexp-opt mrb/private-addresses) 3665 3666 ;; When citing, remove the senders signature. 3667 ;; TODO often I want to remove a lot more, like the disclaimer 3668 message-cite-function 'message-cite-original-without-signature 3669 3670 ;; Citing/Quoting when replying 3671 message-yank-prefix "> " ; Add '> ' when quoting lines 3672 message-yank-cited-prefix ">" ; Add '>' for cited lines 3673 message-yank-empty-prefix "" ; Add nothing for empty lines 3674 ) 3675 3676 ;; Define the headers that will be sent 3677 3678 ;; Add openpg preferences 3679 ;; message-openpgp-header is set in personal.el 3680 (add-hook 'message-header-setup-hook 'message-add-openpgp-header) 3681 3682 ;; This will have an effect in mu4e starting from 1.11.23 3683 (setq 3684 ;; NOTE: mu4e sets message-hidden-headers to a local value based on by mu4e-compose-hidden-headers 3685 message-hidden-headers '(not "To:" "From:" "Cc:" "Subject:") 3686 3687 ;; How to attribute 3688 message-citation-line-format "[%N]:" 3689 message-citation-line-function 'message-insert-formatted-citation-line 3690 3691 message-kill-buffer-on-exit t)) 3692 #+end_src 3693 3694 *** Mu4e specific settings 3695 After a journey with many different MUAs I seem to be settling on =mu4e=. 3696 3697 #+begin_src emacs-lisp 3698 ;; TODO check for system packages meson, g++, pkg-config, 3699 ;; cmake, libglib2.0-dev, libgmime3.0-dev, libxapian-dev, texinfo 3700 ;; (names are debian package names) 3701 (use-package mu4e 3702 ;; Straight needs some tweaking 3703 :straight ( :type git :host github :repo "djcb/mu" 3704 :pre-build ( 3705 ("./autogen.sh") 3706 ("meson" "configure" "-D" "use-embedded-fmt=true" "build") 3707 ("ninja" "-C" "build")) 3708 :files (:defaults "build/mu4e/*.el") 3709 :includes (mu4e-icalendar)) 3710 3711 :custom-face 3712 (mu4e-header-highlight-face ((t (:inherit hl-line :underline nil 3713 :weight normal :foreground unspecified)))) 3714 (mu4e-unread-face ((t (:inherit nil :underline nil :weight bold)))) 3715 (mu4e-context-face ((t (:inherit mu4e-link-face)))) 3716 (mu4e-flagged-face ((t (:foreground "#ebcb8b" )))) 3717 (mu4e-title-face ((t (:foreground "#5e81ac")))) 3718 3719 :custom 3720 (mu4e-mu-binary (mrb/emacs-file "straight/repos/mu/build/mu/mu")) 3721 3722 :after (message) 3723 :commands (mu4e mrb/compose-mail mrb/mailfile) 3724 :hook ((mu4e-view-mode . mrb/mu4e-view-mode-hook) 3725 (mu4e-headers-mode . (lambda () (eldoc-mode -1)))) 3726 3727 :config 3728 (defun mrb/mu4e-view-mode-hook () 3729 (interactive) 3730 (turn-on-visual-line-mode) 3731 ;; Reflow messages 3732 (setq mm-fill-flowed t)) 3733 3734 :bind ( :map mu4e-search-minor-mode-map 3735 ("/" . mu4e-search) 3736 ("s" . nil) 3737 ("e" . mu4e-search-edit)) 3738 3739 ;; Configuration sections 3740 <<mu4e-composeconfig>> 3741 <<mu4e-main-config>> 3742 <<mu4e-headers-config>> 3743 <<mu4e-view-config>> 3744 <<mu4e-packages>> 3745 3746 ;; TODO move these up 3747 (defun mrb/setsigfile (ctxname) 3748 (expand-file-name (concat ".signature-" (downcase ctxname)) mrb/maildir)) 3749 3750 (setq 3751 ;; Override the default mail user agent with the mu4e specific one 3752 mail-user-agent 'mu4e-user-agent 3753 mu4e-sent-folder "/Sent" 3754 mu4e-drafts-folder "/Drafts" 3755 mu4e-trash-folder "/Trash" 3756 mrb/mu4e-junk-folder "/Junk" ; Added for my specific config 3757 mrb/mu4e-archive-folder "/Archives" 3758 ;; mu4e-attachment-dir is set in personal.el 3759 3760 ;; Refiling to /Archives/YYYY 3761 mu4e-refile-folder (lambda (msg) 3762 "Refile message to archive organized by year." 3763 (let* ((archive-base (concat mrb/mu4e-archive-folder "/")) 3764 (msg-date (mu4e-message-field msg :date)) 3765 (year (when msg-date (format-time-string "%Y" msg-date)))) 3766 (if year 3767 (concat archive-base year) 3768 archive-base))) 3769 3770 3771 mu4e-use-fancy-chars t 3772 mu4e-get-mail-command "mbsync quick" ; Just get inbox and important stuff, not everything 3773 mu4e-decryption-policy 'ask 3774 mu4e-update-interval 120 ; we just index, no retrieve, so 2mins is fine 3775 3776 ;; Set completing function to standard, so completing frameworks can use it 3777 mu4e-read-option-use-builtin nil 3778 mu4e-completing-read-function 'completing-read 3779 mu4e-save-multiple-attachments-without-asking t 3780 3781 ;; Use same hidden setting as message mode 3782 mu4e-compose-hidden-headers message-hidden-headers 3783 3784 ;; Context definitions 3785 ;; Goal 1. use global config, unless private address is used 3786 ;; TODO Can we use the one that matched? 3787 mu4e-context-policy 'pick-first ; When nothing matches only! 3788 mu4e-compose-context-policy 'nil ; for compose, use current if nothing matches 3789 3790 mu4e-search-results-limit -1 ; until we run into performance problems 3791 mu4e-search-include-related nil ; do not included related by default 3792 3793 3794 mu4e-contexts 3795 `(,(let* ((ctxname "Default") 3796 (sigfile (mrb/setsigfile ctxname))) 3797 (make-mu4e-context 3798 :name ctxname 3799 :match-func (lambda (msg) 3800 (when msg 3801 (mu4e-message-contact-field-matches 3802 msg `(:to :cc :from) mrb/default-email-address))) 3803 ;; Use global config, but restore what was changed in other contexts 3804 :vars `((user-mail-address . ,mrb/default-email-address) 3805 (message-signature-file . ,sigfile) 3806 (mu4e-compose-signature . (with-current-buffer 3807 (find-file-noselect message-signature-file) 3808 (buffer-string)))))) 3809 ,(let* ((ctxname "Private") 3810 (sigfile (mrb/setsigfile ctxname))) 3811 (make-mu4e-context 3812 :name "Private" 3813 :match-func (lambda (msg) 3814 (when msg 3815 (mu4e-message-contact-field-matches 3816 msg `(:to :cc :from) mrb/private-addresses))) 3817 :vars `((user-mail-address . ,mrb/default-private-address) 3818 (message-signature-file . ,sigfile) 3819 (mu4e-compose-signature . (with-current-buffer 3820 (find-file-noselect message-signature-file) 3821 (buffer-string)))))) 3822 ) 3823 ) 3824 3825 ;; Rendering of html mail is done with shr, but I prefer not to show it at all 3826 (with-eval-after-load "mm-decode" 3827 (add-to-list 'mm-discouraged-alternatives "text/html") 3828 (add-to-list 'mm-discouraged-alternatives "text/richtext")) 3829 3830 (defun mrb/mu4e-shr2text (msg) 3831 "Overridden mu4e-shr2text" 3832 (mu4e~html2text-wrapper 3833 (lambda () 3834 (let ((shr-inhibit-images nil) ; 3835 (shr-width nil) ; Use whole window width for rendering 3836 (shr-use-colors nil) ; Don't use colors from html, they ugly 3837 (shr-bullet "• ")) ; Original was "* " 3838 (shr-render-region (point-min) (point-max)))) msg)) 3839 3840 (setq mu4e-html2text-command 'mrb/mu4e-shr2text)) 3841 #+end_src 3842 **** Main screen 3843 When mu4e start, it always opens in its main screen, which contains the main operations, visible bookmarks. 3844 3845 #+name: mu4e-main-config 3846 #+begin_src emacs-lisp :tangle no 3847 :bind ( :map mu4e-main-mode-map 3848 ("q" . mrb/mu4e-quit) ; just close the menu 3849 ("Q" . mu4e-quit) ; really Quit 3850 (";" . nil) ; this was too easy to press by mistake 3851 ("G" . (lambda () (interactive) (mu4e-update-mail-and-index 1))) 3852 ("C-;" . mu4e-context-switch)) 3853 :init 3854 ;; Claim the whole screen in mu4e main window 3855 (defun mrb/mu4e-quit () 3856 (interactive) 3857 (bury-buffer)) 3858 3859 :config 3860 (setq mu4e-main-buffer-hide-personal-addresses t ; no need to see my own addresses 3861 mu4e-main-hide-fully-read nil 3862 mu4e-confirm-quit nil 3863 3864 mu4e-bookmarks (list 3865 '( :name "Unread messages" 3866 :query "flag:unread and not flag:trashed" 3867 :key ?u) 3868 '( :name "INBOX" 3869 :query "(flag:unread or flag:new or maildir:/INBOX) and not (flag:draft or flag:flagged or maildir:/Trash or flag:trashed)" 3870 :favorite t 3871 :key ?i) 3872 '( :name "TODO list" 3873 :query "flag:flagged and not flag:trashed" 3874 :key ?+) 3875 '( :name "Today's messages" 3876 :query "date:today..now" 3877 :key ?d) 3878 '( :name "Trash" 3879 :query "maildir:/Trash or flag:trashed" 3880 :key ?t) 3881 '( :name "Junk" 3882 :query "maildir:/Junk" 3883 :key ?j))) 3884 #+end_src 3885 **** Header view 3886 The first view after searching is a list of mail headers. Set config vars for this view and define the visualisation of the marks. I need an extra action to mark mail as junk which, on processing, moves the marked messages to the junk folder 3887 3888 #+name: mu4e-headers-config 3889 #+begin_src emacs-lisp :tangle no 3890 :bind ( :map mu4e-headers-mode-map 3891 ("s" . mu4e-headers-mark-for-junk) 3892 ("r" . mu4e-compose-reply) 3893 ("R" . mu4e-compose-wide-reply) 3894 ("A" . mu4e-headers-mark-for-refile) 3895 ("SPC" . mu4e-headers-mark-for-something) 3896 ("X" . (lambda () (interactive) (mu4e-mark-execute-all t)))) 3897 3898 :custom 3899 (mu4e-headers-include-related nil) 3900 3901 (mu4e-headers-date-format "%F" "ISO format yyyy-mm-dd") 3902 (mu4e-headers-results-limit -1) ; For now, needed to have related search results 3903 (mu4e-headers-visible-lines 20) ; Default of 10 is a bit small 3904 (mu4e-headers-auto-update nil) ; Prevent confusion 3905 3906 ;; Make header configuration explicit 3907 (mu4e-headers-fields '((:flags . 6) 3908 (:human-date . 12) 3909 (:mailing-list . 10) 3910 (:from . 22) 3911 (:subject . nil))) 3912 (mu4e-use-fancy-chars t) 3913 3914 3915 ;; Define marks, possibly redundant, but I'd like to see them here explicitly 3916 ;; Note that these are not custom vars, why not?? (they wont work as :custom entries) 3917 :config 3918 (setq mu4e-headers-attach-mark '("a" . "📎 ")) 3919 (setq mu4e-headers-calendar-mark '("c" . "📅")) 3920 (setq mu4e-headers-draft-mark '("D" . "🚧 ")) 3921 (setq mu4e-headers-encrypted-mark '("x" . "🔑 ")) 3922 (setq mu4e-headers-flagged-mark '("F" . "🚩 ")) 3923 (setq mu4e-headers-list-mark '("s" . "🔈")) 3924 (setq mu4e-headers-new-mark '("N" . "✨ ")) 3925 (setq mu4e-headers-passed-mark '("P" . "↪ ")) 3926 (setq mu4e-headers-personal-mark '("p" . "👨")) 3927 (setq mu4e-headers-replied-mark '("R" . "↩ ")) 3928 (setq mu4e-headers-seen-mark '("S" . " ")) 3929 (setq mu4e-headers-signed-mark '("s" . "🖊 ")) 3930 (setq mu4e-headers-trashed-mark '("T" . "🗑️")) 3931 (setq mu4e-headers-unread-mark '("u" . "📩 ")) 3932 3933 ;; Add one specifically for marking as junk 3934 (add-to-list 'mu4e-marks 3935 '(junk 3936 :char ("J" . "💀") :prompt "Mark as junk" 3937 :show-target (lambda (dyn-target) "Junk") 3938 :ask-target (lambda () mrb/mu4e-junk-folder) 3939 :action (lambda (docid msg target) 3940 (mu4e--server-move 3941 docid 3942 (mu4e--mark-check-target target) "+S-N-u")))) 3943 3944 ;; Let mu4e define functions for it 3945 (mu4e~headers-defun-mark-for junk) 3946 (mu4e--view-defun-mark-for junk) 3947 3948 #+end_src 3949 3950 **** Message view 3951 After selecting a message in the header view, a message view will open. 3952 3953 #+name: mu4e-view-config 3954 #+begin_src emacs-lisp :tangle no 3955 :bind ( :map mu4e-view-mode-map 3956 ("s" . mu4e-view-mark-for-junk) 3957 ("r" . mu4e-compose-reply) 3958 ("A" . mu4e-view-mark-for-refile) 3959 ("X" . (lambda () (interactive) (mu4e~view-in-headers-context (mu4e-mark-execute-all t))))) 3960 3961 :custom 3962 (mu4e-view-use-old nil) 3963 (mu4e-view-show-images t) ; This creates connections, possible privacy issue? 3964 (mu4e-view-image-max-width 800) 3965 (mu4e-view-image-max-height 600) 3966 (mu4e-view-show-addresses t) ; Show actual addresses instead of names 3967 3968 (mu4e-view-actions '(("c - capture message" . mu4e-action-capture-message) 3969 ("v - view in browser" . mu4e-action-view-in-browser) 3970 ("s - save attachments" . mu4e-view-save-attachments) 3971 ("t - show this thread" . mu4e-action-show-thread))) 3972 3973 #+end_src 3974 **** Addon packages 3975 There are quite a few mu4e packages out there. Let's load them in one group for now, so it will be easy to /disable plugins/ if we need to. 3976 #+name: mu4e-packages 3977 #+begin_src emacs-lisp :tangle no 3978 :config 3979 ;; Show an alert when new mails come in, this relies on mu4e being active though 3980 (use-package mu4e-alert 3981 :hook (after-init . mu4e-alert-enable-notifications) 3982 :config 3983 ;; Just show subjects, not counts 3984 (setq mu4e-alert-email-notification-types '(subjects)) 3985 (mu4e-alert-set-default-style 'libnotify)) 3986 3987 ;; Support icalendar Yes/No/Maybe reactions to calendar invites 3988 ;; TODO not needed anymore now? 3989 (use-package mu4e-icalendar 3990 :config 3991 (mu4e-icalendar-setup)) 3992 3993 (use-package mu4e-query-fragments) ; Support reusable query pieces '%junk' etc. 3994 (use-package mu4e-jump-to-list) ; Shortcut key 'l' to directly focus on list threads 3995 3996 ;; Patch highlighting inside messages 3997 (use-package message-view-patch 3998 :after (magit) 3999 :hook (gnus-part-display . message-view-patch-highlight)) 4000 4001 (use-package org-mime 4002 :custom 4003 (org-mime-use-property-inheritance t)) 4004 4005 (use-package autocrypt 4006 ;; autocrypt-accounts is set in personal.el 4007 ) 4008 4009 ;(use-package org-mu4e :after org) 4010 #+end_src 4011 4012 *** Sieve 4013 Sieve is used on the server to filter our mail and this is accessed by the sieve-manage package. For some reason I can't get the '^M' characters out of the process, so here's a hack for that. 4014 4015 #+begin_src emacs-lisp 4016 (use-package sieve 4017 :straight (:type built-in) 4018 :init 4019 (defun dos2unix () 4020 "Replace DOS eolns CR LF with Unix eolns CR" 4021 (interactive) 4022 (goto-char (point-min)) 4023 (while (search-forward "\r" nil t) (replace-match ""))) 4024 :commands sieve-manage 4025 :hook (sieve-mode . dos2unix) 4026 :custom 4027 (sieve-manage-authenticators '(plain digest-md5 cram-md5 scram-md5 ntlm login))) 4028 #+end_src 4029 4030 which basically goes over the whole sieve script and removes the '^M' characters from the buffer 4031 ** Elfeed 4032 Elfeed handles my RSS feeds, which I specify using an orgmode file =feeds.org= 4033 4034 #+begin_src emacs-lisp 4035 (use-package notifications 4036 :straight (:type built-in)) 4037 4038 (use-package elfeed 4039 :after notifications 4040 :commands elfeed 4041 :bind (("C-c f" . 'elfeed) 4042 :map elfeed-show-mode-map 4043 ("w" . 'mrb/elfeed-show-toggle-watchlater) 4044 ("v" . 'mrb/elfeed-play-with-mpv) 4045 :map elfeed-search-mode-map 4046 ("v" . 'mrb/elfeed-play-with-mpv) 4047 ("w" . 'mrb/elfeed-search-toggle-watchlater)) 4048 :init 4049 (setf url-queue-timeout 30) 4050 ;; elfeed-db-directory is set in personal.el 4051 4052 :custom-face 4053 (elfeed-search-tag-face ((t (:foreground "#a3be8c")))) 4054 (elfeed-search-feed-face ((t (:foreground "#ebcb8b")))) 4055 (elfeed-search-date-face ((t (:foreground "#88c0d0")))) 4056 :config 4057 (defun mrb/elfeed-search-toggle-tag(tag) 4058 (let ((entries (elfeed-search-selected))) 4059 (cl-loop for entry in entries do 4060 (if (elfeed-tagged-p tag entry) 4061 (elfeed-untag entry tag) 4062 (elfeed-tag entry tag))) 4063 (mapc #'elfeed-search-update-entry entries) 4064 (unless (use-region-p) (forward-line)))) 4065 4066 (defun mrb/elfeed-search-toggle-watchlater() 4067 (interactive) 4068 (mrb/elfeed-search-toggle-tag 'watchlater)) 4069 4070 (defun mrb/elfeed-show-toggle-tag(tag) 4071 (interactive) 4072 (if (elfeed-tagged-p tag elfeed-show-entry) 4073 (elfeed-show-untag tag) 4074 (elfeed-show-tag tag))) 4075 4076 (defun mrb/elfeed-show-toggle-watchlater() 4077 (interactive) 4078 (mrb/elfeed-show-toggle-tag 'watchlater)) 4079 4080 ;; umpv maintains a playlist, so adding more videos wil automatically queue 4081 (defun mrb/elfeed-play-with-mpv () 4082 "Play elfeed link in mpv" 4083 (interactive) 4084 (notifications-notify 4085 :title "Elfeed action" 4086 :body "Playing video with MPV" 4087 :app-name "Elfeed") 4088 4089 (start-process "elfeed-mpv" nil 4090 "~/bin/umpv" 4091 (elfeed-entry-link (elfeed-search-selected t)))) 4092 4093 4094 ;; New entry hook allows meta information manipulation 4095 ;; without directly having to change elfeed-feeds 4096 (add-hook 'elfeed-new-entry-hook 4097 (elfeed-make-tagger :feed-url "youtube\\.com" 4098 :add '(video youtube))) 4099 (add-hook 'elfeed-new-entry-hook 4100 (elfeed-make-tagger :feed-url "vimeo\\.com" 4101 :add '(video vimeo)))) 4102 4103 #+end_src 4104 4105 Elfeed allows to interactively subscribe to a feed (defaulting to what is in the clipboard. Managing elfeed-feeds as a variable is kinda clumsy, although very flexible. There is a middle ground which fits me even better. At the cost of the bare-bone flexibility of having the feeds directly in lisp, I'm using an orgmode file to record the feeds i want. 4106 4107 #+begin_src emacs-lisp 4108 (use-package elfeed-org 4109 :after elfeed 4110 :config 4111 (setq rmh-elfeed-org-files (list (concat elfeed-db-directory "feeds.org"))) 4112 (elfeed-org)) 4113 #+end_src 4114 4115 Quite a few of my feeds are youtube channels, the =elfeed-tube= package has some extra features for reading those feeds 4116 4117 #+begin_src elisp 4118 (use-package elfeed-tube 4119 :after elfeed 4120 :config 4121 (elfeed-tube-setup) 4122 4123 :bind (:map elfeed-show-mode-map 4124 ("F" . elfeed-tube-fetch) 4125 ([remap save-buffer] . elfeed-tube-save) 4126 :map elfeed-search-mode-map 4127 ("F" . elfeed-tube-fetch) 4128 ([remap save-buffer] . elfeed-tube-save))) 4129 #+end_src 4130 4131 ** IRC 4132 4133 I used to run a weechat relay on a vps for IRC. The main reason for this was that the relay allowed multiple clients to be active at the same time an have at least an attempt at saving the state betwee multiple devices. 4134 4135 The major downside is that a special weechat client is needed to make this work, which is not the same as a standard IRC client. This made the choice for clients very limited, but luckily there was an emacs client. 4136 4137 After using it some time and maintaining some local patches; the main program is largely unmaintained, it was time to look for an alternative. 4138 4139 While having the weechat relay is very nice, I also want to have a /"standard"/ IRC configuration where everything is happening in the client, possibly augmented by having an IRC bouncer to maintain persistent connections. [[https://wiki.znc.in/ZNC][ZNC]] might almost have the featureset to do what weechat does, or very closely to it. 4140 4141 ERC seems to be the most used IRC client for emacs and included with it, so it would make sense to use it. I like the simplicity of [[https://github.com/emacs-circe/circe/wiki][circe]] though, so I opted to create a config for that. 4142 4143 #+begin_src emacs-lisp 4144 (use-package circe 4145 :commands (circe mrb/chat) 4146 :custom 4147 ;; circe-default-nick and circe-default-user are set in personal.el 4148 (circe-network-options 4149 `(("libera" 4150 :host "chat.hsdev.com" :port 1066 4151 :pass ,(concat "mrb@emacs/libera:" 4152 (password-store-get "chat.hsdev.com")) 4153 :channels ("#emacs" 4154 "#talos-workstation" 4155 "#vikings")) 4156 ("oftc" 4157 :host "chat.hsdev.com" :port 1066 4158 :pass ,(concat "mrb@emacs/oftc:" 4159 (password-store-get "chat.hsdev.com")) 4160 :channels ("#osm")))) 4161 4162 ;; Simplify and improve formatting, remove some details 4163 (circe-format-say "{nick:10s}: {body}" "right align nicks at 10th pos") 4164 (circe-format-self-say "{nick:10s}: {body}") 4165 (circe-format-server-part "*** Part: {nick} left {channel}") 4166 (circe-format-server-quit-channel "*** Quit: {nick} left {channel}") 4167 (circe-format-server-quit "*** Quit: {nick}") 4168 (circe-format-server-join "*** Join: {nick}") 4169 (circe-format-server-rejoin "*** Re-joined: {nick}") 4170 (lui-time-stamp-position 'right-margin) 4171 (lui-fill-type nil) 4172 (circe-reduce-lurker-spam t "dont show notices for non talkers") 4173 (circe-default-part-message "" "no parting reason") 4174 (circe-default-quit-message "" "no quitting reason") 4175 4176 :custom-face 4177 (circe-prompt-face ((t (:foreground ,mrb/cursor-color :height 1.3)))) 4178 (circe-originator-face ((t (:height 0.8)))) 4179 (lui-time-stamp-face ((t (:height 0.8)))) 4180 (lui-track-bar ((t (:background "LawnGreen")))) 4181 4182 :config 4183 (enable-lui-track) ; show indicator to where has been read 4184 (enable-circe-display-images) ; turn links to images into images 4185 (enable-circe-color-nicks) ; color nicks 4186 4187 ;; Gather buffer local and line UI settings 4188 (defconst mrb/lui-nick-column-width 12 4189 "Width allocated for IRC nicknames, matching `circe-format-say'.") 4190 4191 (defconst mrb/lui-timestamp-width 7 4192 "Width of timestamp margin, sized for [hh:mm].") 4193 4194 (defun mrb/lui-setup () 4195 "Configure buffer-local display settings for lui IRC buffers." 4196 (setq fringes-outside-margins t 4197 right-margin-width mrb/lui-timestamp-width 4198 word-wrap t 4199 wrap-prefix (make-string mrb/lui-nick-column-width ?\s)) 4200 (setf (cdr (assoc 'continuation fringe-indicator-alist)) nil)) 4201 (add-hook 'lui-mode-hook 'mrb/lui-setup) 4202 4203 ;; Set the prompt properly on entering the chat mode 4204 (defun mrb/set-circe-prompt () 4205 (lui-set-prompt (propertize (concat "[" circe-chat-target "]➜ ") 4206 'face 'circe-prompt-face))) 4207 (add-hook 'circe-chat-mode-hook 'mrb/set-circe-prompt) 4208 4209 ;; Make a convenient connect function 4210 ;; TODO make this re-entrant 4211 (defun mrb/chat() 4212 (interactive) 4213 (circe "libera") 4214 (circe "oftc"))) 4215 #+end_src 4216 4217 ** Mastodon 4218 Not messaging or chatting perse, but a micro blog social network. The 'capture a toot' is hot-keyed through =xbindkeys= like other capture commands. 4219 4220 #+begin_src emacs-lisp 4221 ;; Mastodon in emacs 4222 (use-package mastodon 4223 :straight (mastodon :host codeberg :repo "martianh/mastodon.el" 4224 :branch "develop") 4225 ;; mastodon-instance-url and mastodon-active-user are set in personal.el 4226 :config 4227 ;; Convenience function 4228 (defun mrb/capture-toot() 4229 (interactive) 4230 (mastodon-toot))) 4231 #+end_src 4232 4233 #+begin_src sh :exports code :tangle ~/bin/capture-toot.sh :shebang #!/bin/sh 4234 edit-noframe --eval '(mrb/capture-toot)' 4235 #+end_src 4236 4237 * Development settings 4238 Some settings which aid in development tasks. 4239 4240 ** Generic 4241 Let's start with some generic development related settings and packages 4242 #+begin_src emacs-lisp 4243 (use-package request 4244 :custom 4245 (request-log-level 'verbose)) 4246 4247 (use-package cmake-mode) ; adjusts auto-mode-alist on load 4248 4249 (use-package yaml-mode) ; used by ansible, magit(?) 4250 #+end_src 4251 4252 For most of the languages I use (Bash, python, C, Go, HTML Haskell, Lua), the Language Server Protocol seems to be supported by =eglot=, which is now a built-in package, so I'm setting this up generally here and enable it for each language I'm using if it is supported. 4253 4254 #+begin_src emacs-lisp 4255 (use-package eglot 4256 :straight (:type built-in)) 4257 #+end_src 4258 4259 Diff and merge tools for version control: 4260 4261 #+begin_src emacs-lisp 4262 (use-package diff-mode 4263 :straight (:type built-in) 4264 :mode "\\.patch\\'" 4265 :custom-face 4266 ;; Unified diff faces — diff-mode is the single source of truth, 4267 ;; magit, ediff and smerge inherit from it. 4268 (diff-added ((t (:foreground "#a3be8c" :background "#2e4a2e")))) 4269 (diff-removed ((t (:foreground "#bf616a" :background "#4a2e2e")))) 4270 (diff-refine-added ((t (:foreground "#eceff4" :background "#5a7a52")))) 4271 (diff-refine-removed ((t (:foreground "#eceff4" :background "#943b44")))) 4272 (diff-header ((t (:foreground "#e5e9f0" :background "#3b4252")))) 4273 (diff-hunk-header ((t (:foreground "#e5e9f0" :background "#434c5e")))) 4274 (diff-context ((t (:foreground "#d8dee9")))) 4275 ) 4276 4277 (use-package ediff 4278 :straight (:type built-in) 4279 :custom-face 4280 ;; ediff — A = removed/old, B = added/new 4281 (ediff-current-diff-A ((t (:inherit diff-removed)))) 4282 (ediff-current-diff-B ((t (:inherit diff-added)))) 4283 (ediff-fine-diff-A ((t (:inherit diff-refine-removed)))) 4284 (ediff-fine-diff-B ((t (:inherit diff-refine-added)))) 4285 (ediff-even-diff-A ((t (:background "#3b4252")))) 4286 (ediff-even-diff-B ((t (:background "#3b4252")))) 4287 (ediff-odd-diff-A ((t (:background "#434c5e")))) 4288 (ediff-odd-diff-B ((t (:background "#434c5e"))))) 4289 4290 (use-package smerge-mode 4291 :straight (:type built-in) 4292 :custom-face 4293 (smerge-upper ((t (:inherit diff-removed)))) 4294 (smerge-lower ((t (:inherit diff-added)))) 4295 (smerge-refined-added ((t (:inherit diff-refine-added)))) 4296 (smerge-refined-removed ((t (:inherit diff-refine-removed))))) 4297 #+end_src 4298 4299 Make our source look a bit more attractive by enabling =prettify-symbol-mode= in all programming modes 4300 4301 #+begin_src emacs-lisp 4302 (use-package prog-mode 4303 :straight (:type built-in) 4304 ;; TODO move hooks to their defining packages? 4305 :hook ((prog-mode ; all modes that derive from prog-mode 4306 lisp-interaction-mode) . mrb/prettify-symbols) 4307 :init 4308 4309 ;; TODO Move this to visual? 4310 ;; TODO this is a bit messy, do this in mode packages and per mode 4311 (defun mrb/prettify-symbols () 4312 (interactive) 4313 ;; set buffer local variable to map the symbols 4314 (setq prettify-symbols-alist 4315 '(("lambda" . ?λ) 4316 ;("lambda*" . (?λ (Br . Bl) ?*)) ; just in scheme really 4317 ;("-lambda" . (?- (Br . Bl) ?λ)) 4318 ;("map" . ?↦) ; this one is a bit of a pain actually 4319 ("->" . ?⟶) 4320 ("<-" . ?⟵) 4321 ("=>" . ?⟹) 4322 ;("#t" . ?⟙) ; bad idea 4323 ;("#f" . ?⟘) ; bad idea 4324 ("|>" . ?▷) 4325 ("<|" . ?◁) 4326 ("->>" . ?↠) 4327 ("<=" . ?≤) 4328 (">=" . ?≥))) 4329 (prettify-symbols-mode))) 4330 #+end_src 4331 4332 In general, the syntax highlighting of emacs is sufficient, but is based on defining regular expression to match language contents to color them. There is a better way to do this; based on generating a semantic tree representation of the language in question. There is a tool called [[https://tree-sitter.github.io/][tree-sitter]] which does this. 4333 4334 The main advantages of the method that tree-sitter uses are: 4335 - the same tree representation is used for all languages, so adding new languages is relatively easy 4336 - using the representation for syntax highlighting is just one application: code folding or semantic extending a region, all of which are available in emacs in other ways, is a lot better using the tree-sitter method. 4337 - it's super fast. fast enough to keep up with typing. 4338 4339 As of emacs 29 the treesit package is built-in, so let's use that in any case for highlighting where it is possible. I think over time the mapping of the modes below will just disappear and everything will be treesitter based. 4340 4341 #+begin_src emacs-lisp 4342 ;; Use the built-in treesit and load all language grammars 4343 (use-package treesit 4344 :straight (:type built-in) 4345 :config 4346 (setq treesit-extra-load-path 4347 `(,(mrb/emacs-file "var/tree-sitter/langs"))) 4348 ;; Replace relevant modes with the treesitter variant 4349 (dolist (mode 4350 '((bash-mode . bash-ts-mode) 4351 (c-mode . c-ts-mode) 4352 (c++-mode . c++-ts-mode) 4353 (css-mode . css-ts-mode) 4354 (dockerfile-mode . dockerfile-ts-mode) 4355 (go-mode . go-ts-mode) 4356 (javascript-mode . js-ts-mode) 4357 (js-json-mode . json-ts-mode) 4358 (markdown-mode . markdown-ts-mode) 4359 (python-mode . python-ts-mode) 4360 (typescript-mode . typescript-ts-mode) 4361 (yaml-mode . yaml-ts-mode))) 4362 (add-to-list 'major-mode-remap-alist mode))) 4363 #+end_src 4364 4365 Code folding via tree-sitter. Provides semantic-aware folding (on functions, classes, blocks) across all tree-sitter modes. 4366 4367 #+begin_src emacs-lisp 4368 (use-package treesit-fold 4369 :ensure t 4370 :demand t 4371 :custom-face 4372 ;; Treesit-fold replacement text (ellipsis when code is folded) 4373 (treesit-fold-replacement-face ((t (:foreground "white" :weight bold)))) 4374 4375 ;; Treesit-fold fringe indicators (the triangles in the fringe) 4376 ;; Use subtle grey to reduce visual noise from range indicators 4377 (treesit-fold-fringe-face ((t (:foreground "#81a1c1")))) 4378 4379 :config 4380 ;; Define C-c f as a prefix for folding commands 4381 (define-prefix-command 'treesit-fold-prefix-command) 4382 (global-set-key (kbd "C-c t") 'treesit-fold-prefix-command) 4383 (define-key treesit-fold-prefix-command (kbd "t") 'treesit-fold-toggle) 4384 (define-key treesit-fold-prefix-command (kbd "c") 'treesit-fold-close-all) 4385 (define-key treesit-fold-prefix-command (kbd "o") 'treesit-fold-open-all) 4386 (define-key treesit-fold-prefix-command (kbd "r") 'treesit-fold-open-recursively) 4387 4388 (global-treesit-fold-mode 1) 4389 (global-treesit-fold-indicators-mode 1) 4390 4391 ;; Use full rendering with filtering to remove range indicators 4392 (setq treesit-fold-indicators-render-method 'full) 4393 4394 ;; Fix YAML folding issue where block_sequence_item can return ranges with nil values 4395 ;; Override the YAML parser to handle missing key/value nodes gracefully 4396 ;; See: https://github.com/emacs-tree-sitter/treesit-fold/issues/48 4397 4398 (require 'treesit-fold-parsers) 4399 (let ((yaml-parsers (treesit-fold-parsers-yaml))) 4400 ;; Wrap the block_sequence_item handler to validate beg/end values 4401 (setf (alist-get 'block_sequence_item yaml-parsers) 4402 (lambda (node offset) 4403 (let* ((key (treesit-search-subtree 4404 node 4405 (lambda (n) 4406 (string= "key" (treesit-node-field-name n))))) 4407 (value (treesit-search-subtree 4408 node 4409 (lambda (n) 4410 (string= "value" (treesit-node-field-name n))))) 4411 ;; Only compute beg if we have a valid key or value 4412 (beg (when (or key value) 4413 (treesit-node-end 4414 (if (and value (string= "block_node" (treesit-node-type value))) 4415 key 4416 value)))) 4417 (end (treesit-node-end node))) 4418 ;; Only return a range if both beg and end are valid numbers 4419 (when (and (numberp beg) (numberp end) beg end) 4420 (treesit-fold--cons-add (cons beg end) offset))))) 4421 ;; Update the alist for YAML 4422 (setf (alist-get 'yaml-ts-mode treesit-fold-range-alist) yaml-parsers)) 4423 4424 ;; Define custom fringe bitmaps for fold indicators 4425 ;; Right-pointing triangle (▶) for folded code, down-pointing (▼) for open code 4426 ;; Right-pointing triangle (▶) - 16x16 fully filled 4427 (define-fringe-bitmap 'treesit-fold-indicators-fr-plus 4428 (vector #b1000000000000000 4429 #b1100000000000000 4430 #b1111000000000000 4431 #b1111110000000000 4432 #b1111111100000000 4433 #b1111111111000000 4434 #b1111111111110000 4435 #b1111111111111100 4436 #b1111111111111100 4437 #b1111111111110000 4438 #b1111111111000000 4439 #b1111111100000000 4440 #b1111110000000000 4441 #b1111000000000000 4442 #b1100000000000000 4443 #b1000000000000000) 4444 16 16) 4445 ;; Down-pointing triangle (▼) - 16x16 4446 (define-fringe-bitmap 'treesit-fold-indicators-fr-minus-tail 4447 (vector #b1111111111111111 4448 #b1111111111111111 4449 #b0111111111111110 4450 #b0011111111111100 4451 #b0001111111111000 4452 #b0000111111110000 4453 #b0000011111100000 4454 #b0000001111000000 4455 #b0000000110000000 4456 #b0000000100000000 4457 #b0000000000000000 4458 #b0000000000000000 4459 #b0000000000000000 4460 #b0000000000000000 4461 #b0000000000000000 4462 #b0000000000000000) 4463 16 16) 4464 4465 ;; Define empty bitmaps for range indicators to hide vertical lines 4466 (define-fringe-bitmap 'treesit-fold-indicators-fr-center 4467 (vector #b0000000000000000 4468 #b0000000000000000 4469 #b0000000000000000 4470 #b0000000000000000 4471 #b0000000000000000 4472 #b0000000000000000 4473 #b0000000000000000 4474 #b0000000000000000 4475 #b0000000000000000 4476 #b0000000000000000 4477 #b0000000000000000 4478 #b0000000000000000 4479 #b0000000000000000 4480 #b0000000000000000 4481 #b0000000000000000 4482 #b0000000000000000) 4483 16 16) 4484 4485 (define-fringe-bitmap 'treesit-fold-indicators-fr-end-left 4486 (vector #b0000000000000000 4487 #b0000000000000000 4488 #b0000000000000000 4489 #b0000000000000000 4490 #b0000000000000000 4491 #b0000000000000000 4492 #b0000000000000000 4493 #b0000000000000000 4494 #b0000000000000000 4495 #b0000000000000000 4496 #b0000000000000000 4497 #b0000000000000000 4498 #b0000000000000000 4499 #b0000000000000000 4500 #b0000000000000000 4501 #b0000000000000000) 4502 16 16) 4503 4504 (define-fringe-bitmap 'treesit-fold-indicators-fr-end-right 4505 (vector #b0000000000000000 4506 #b0000000000000000 4507 #b0000000000000000 4508 #b0000000000000000 4509 #b0000000000000000 4510 #b0000000000000000 4511 #b0000000000000000 4512 #b0000000000000000 4513 #b0000000000000000 4514 #b0000000000000000 4515 #b0000000000000000 4516 #b0000000000000000 4517 #b0000000000000000 4518 #b0000000000000000 4519 #b0000000000000000 4520 #b0000000000000000) 4521 16 16) 4522 4523 ) 4524 #+end_src 4525 4526 To have some control over the environment for specific projects I use [[http://direnv.net][direnv]], so let's install emacs support for it as well. 4527 4528 #+begin_src emacs-lisp 4529 (use-package envrc 4530 :config 4531 (envrc-global-mode)) 4532 4533 #+end_src 4534 ** Reference & documentation 4535 While emacs itself has a very nicely documentation system, I still don't understand why this is not interpolated into every other mode and or language. 4536 4537 Example: =C-h-f= in lisp-mode describes the function to me. If I am in a python file, I want it to do exactly the same, but for the python function. There's a page on emacs wiki dealing with this a bit: https://www.emacswiki.org/emacs/Context_sensitive_help 4538 4539 There's a package =ghelp= (/generic help/ I guess) which promises to do what I want, but I can't get it installed properly with =use-package=. That package can use the =helpful= package as back-end which is also helpful on it own, so I'm installing that anyways and replace the internal help commands from emacs with them. 4540 4541 #+begin_src emacs-lisp 4542 (use-package helpful 4543 :bind (("C-h f" . #'helpful-callable) 4544 ("C-h h" . #'helpful-at-point) ; I don't care about the emacs 'hello' file 4545 ("C-h v" . #'helpful-variable) 4546 ("C-h k" . #'helpful-key))) 4547 4548 #+end_src 4549 4550 Anyways, here's what I have configured related to reference information and documentation 4551 4552 *** Context sensitive 4553 Regardless of (programming) language there's certain information that's needed on the spot. These are the function signatures, the specific syntax when typing Math symbols, orgmode oddities etc. 4554 4555 I want that type of information directly at hand. One part of the solution is [[https://www.emacswiki.org/emacs/ElDoc][ElDoc]]. This shows documentation in the echo area for a language. ElDoc is built into Emacs so that's a good start. Let's enable it for the modes that have support for it. 4556 4557 #+begin_src emacs-lisp 4558 (dolist (mode-hook '(emacs-lisp-mode-hook 4559 lisp-interaction-mode-hook 4560 ielm-mode-hook 4561 python-mode)) 4562 (add-hook mode-hook 'eldoc-mode)) 4563 (diminish 'eldoc-mode) 4564 #+end_src 4565 4566 The location of the eldoc information in the message area is a bit far away from the editing point. I have looked at eldoc-overlay and others to resolve that, but haven't found a satisfying solution yet. 4567 4568 *** Reference information 4569 If the inline documentation presented by ElDoc is not sufficient, I want a way to spawn reference documentation based on the context. 4570 4571 The closest thing I found is =zeal= or =dash= which allow docsets to be searched. There is a GUI and helm-dash is an interface to the docsets for emacs. Typically I install docsets through the GUI or manually (the folder names are sometimes not the same when I use helm-dash to install docsets). This solution gives me at least /access/ to reference information for most of the programming languages I use. The direct access to the sources is missing, or I don't know how to do this yet. 4572 4573 #+begin_src emacs-lisp 4574 ;; Unify reference documentation with dash 4575 (use-package helm-dash 4576 :after helm 4577 :commands (helm-dash helm-dash-at-point) 4578 :config 4579 ;; Make sure docset directory exists 4580 (make-directory helm-dash-docsets-path t) 4581 :custom 4582 (dash-docs-enable-debugging nil) 4583 (helm-dash-browser-func 'eww "Within dash, keep browser links within emacs") 4584 (helm-dash-common-docsets (helm-dash-installed-docsets))) 4585 #+end_src 4586 4587 Dash show documentation inside emacs, which is my preferred method. There are times however I need the desktop GUI Zeal. For one, it's a lot easier to see which docsets are installed and manage them. 4588 4589 #+begin_src emacs-lisp 4590 ;; Global keybinding ot open documentation 4591 ;; This should probably be somewhere else 4592 (use-package zeal-at-point 4593 :bind 4594 ("s-d" . zeal-at-point)) 4595 #+end_src 4596 4597 *** RFC document reader 4598 I often need to consult RFC documents, so not having to leave emacs for that and be able to use all tools on RFC documents is very helpful. 4599 4600 #+begin_src emacs-lisp 4601 ;; Special mode to read rfc documents locally 4602 (use-package rfc-mode 4603 :custom 4604 ;; rfc-mode-directory is set in personal.el 4605 (rfc-mode-index-path (concat rfc-mode-directory"rfc-index.txt"))) 4606 #+end_src 4607 4608 *** Man pages 4609 There's a builtin package to read man pages, let's make it explicit. 4610 4611 #+begin_src emacs-lisp 4612 (use-package man 4613 :straight (:type built-in) 4614 :custom-face 4615 ;; Man pages inside emacs 4616 (Man-overstrike ((t (:foreground "#bf616a" :weight bold)))) 4617 (Man-underline ((t (:foreground "#a3be8c" :weight bold))))) 4618 #+end_src 4619 4620 Not really man pages as such, but meant as their /advanced/ cousin for GNU programs is the info format. 4621 4622 #+begin_src emacs-lisp 4623 (use-package info 4624 :straight (:type built-in) 4625 :bind (:map Info-mode-map ("C-o" . 'casual-info-tmenu))) 4626 #+end_src 4627 4628 *** Epub documents 4629 Not often, but increasingly so, reference books are in epub format. So, for that we need a reader package. =nov= seems to be the best option 4630 4631 #+begin_src emacs-lisp 4632 (use-package nov 4633 :mode ("\\.epub" . nov-mode)) 4634 #+end_src 4635 ** Coding styles 4636 Different projects use different coding styles. The ones I need I'll gather here for now. 4637 4638 My personal style will be called =mrb= and basing it on the =linux= style. 4639 #+begin_src emacs-lisp 4640 4641 ;; Basing it on k&r, no real reason, does it really matter? 4642 (c-add-style "mrb" 4643 '("k&r" 4644 (indent-tabs-mode . nil) 4645 (c-basic-offset . 2) 4646 (c-cleanup-list . (scope-operator 4647 space-before-funcall)))) 4648 4649 ;; Make my style the default 4650 (setq c-default-style 4651 '((java-mode . "java") 4652 (awk-mode . "awk") 4653 (other . "mrb"))) 4654 4655 ;; EditorConfig support, not used much by the looks of it 4656 (use-package editorconfig 4657 :disabled t 4658 :diminish 4659 :config 4660 (editorconfig-mode 1)) 4661 #+end_src 4662 ** Language support 4663 *** Python 4664 Just enabling eglot for now. 4665 4666 #+begin_src emacs-lisp 4667 (use-package python 4668 :straight (:type built-in) 4669 :hook (python-mode . eglot-ensure)) 4670 #+end_src 4671 *** Haskell 4672 I am just starting out with haskell, but the two things that are probably needed in any case are a mode to use when editing Haskell source (*.hs files) and the ghc-mod package to help with completion and showing syntax errors. 4673 4674 #+begin_src emacs-lisp 4675 (use-package haskell-mode 4676 :hook (;(haskell-mode . interactive-haskell) ;; Produces elc load error 4677 (haskell-mode . turn-on-haskell-doc) 4678 (haskell-mode . haskell-indentation-mode)) 4679 4680 :mode "\\.hs\\'" 4681 :custom 4682 (haskell-font-lock-symbols t) 4683 (haskell-interactive-popup-errors nil) 4684 (haskell-process-type 'stack-ghci) 4685 :config 4686 ;; Replace ⇒ with ⇉ 4687 (delete '("=>" . "⇒") haskell-font-lock-symbols-alist) 4688 (add-to-list 'haskell-font-lock-symbols-alist '("=>" . "⇉"))) 4689 #+end_src 4690 4691 Enable the language server for haskell 4692 4693 #+begin_src emacs-lisp 4694 (use-package lsp-haskell 4695 :after (haskell-mode lsp-mode) 4696 :hook ((haskell-mode haskell-literate-mode) . lsp)) 4697 #+end_src 4698 4699 Because the haskell environment can be different for each project, we need a way to adjust to that for certain things. One such thing is flycheck which will give false-negatives when checking haskell files if the specific build environment is not taken into account. The package flycheck-haskell automatically adjusts flycheck. 4700 4701 #+begin_src emacs-lisp 4702 (use-package flycheck-haskell 4703 :after flycheck 4704 :hook (flycheck-mode . flycheck-haskell-setup)) 4705 #+end_src 4706 4707 For creating web apps I use yesod, which is supported by a number of packages 4708 #+begin_src emacs-lisp 4709 ;; Yesod's html like files 4710 (use-package hamlet-mode) 4711 #+end_src 4712 *** Go 4713 #+begin_src emacs-lisp 4714 (use-package go-mode 4715 ;; TODO godef is fairly heavy on my little machines, make optional? 4716 :ensure-system-package godef 4717 4718 :init 4719 ;; Set up before-save hooks to format buffer and add/delete imports. 4720 ;; Make sure you don't have other gofmt/goimports hooks enabled. 4721 ;; TODO would this not add hooks for all files that are saved?? 4722 (defun lsp-go-install-save-hooks () 4723 (add-hook 'before-save-hook #'lsp-format-buffer t t) 4724 (add-hook 'before-save-hook #'lsp-organize-imports t t)) 4725 (add-hook 'go-mode-hook #'lsp-go-install-save-hooks)) 4726 #+end_src 4727 *** Rust 4728 #+begin_src emacs-lisp 4729 (use-package rust-mode) 4730 #+end_src 4731 *** Lisp-like 4732 There are a number of languages which are lisp-like, not in the least Emacs lisp itself. It makes sense to group the configuration for these languages together. In the first part there will be configuration for all lisp-like languages (like the =puni= configuration for example). For each specific implementation there may be augmentations to the config, or even a completely separate. 4733 4734 For all of those languages, I want to use =puni= which makes editing, longer term, a lot more productive. For autopairing, which =puni= does not do itself unlike =paredit=, I use the built-in =electric-pair-mode= Both of these support multiple modes, so are really generic packages, but lisp-like modes are the modes typically benefitting the most from them. 4735 4736 #+begin_src emacs-lisp 4737 (use-package puni 4738 :init 4739 (puni-global-mode) ; perhaps too much, we'll see 4740 4741 :bind ( :map puni-mode-map 4742 ("C-<right>" . puni-slurp-forward) ; foo (bar|) baz -> foo (bar| baz) 4743 ("C-<left>" . puni-barf-forward) ; foo (bar| baz) -> foo (bar|) baz 4744 ) 4745 4746 :hook ((org-mode . puni-disable-puni-mode) 4747 (prog-mode . electric-pair-local-mode))) 4748 #+end_src 4749 4750 Evaluating an expression with the cursor after it is often used, I'd like being able to do the same when the cursor is in front of such an expression 4751 4752 #+begin_src emacs-lisp 4753 ;; The reciprocate of C-x C-e 4754 ;; As in: _cursor_(...eval what is in here...) 4755 (defun mrb/eval-next-sexp () 4756 (interactive) 4757 (save-excursion 4758 (forward-sexp) 4759 (eval-last-sexp nil))) 4760 #+end_src 4761 4762 Not sure how to bind this though. I'd like it close to C-x C-e obviously 4763 4764 When evaluating expressions, the result is way down in the status area. I would like to have it inline in the buffer in an overlay. The package =cider= does this, but I have no need for the complete clojure environment. There is a specific package for emacs-lisp though, called =eros= 4765 4766 #+begin_src emacs-lisp 4767 (use-package eros 4768 :config 4769 (eros-mode 1)) 4770 #+end_src 4771 4772 **** Scheme 4773 The generic scheme support is the builtin scheme package, let's tell it that I use guile as my default scheme program. 4774 4775 #+begin_src emacs-lisp 4776 (use-package scheme 4777 :straight (:type built-in) 4778 :custom 4779 (scheme-program-name "/usr/bin/guile")) 4780 #+end_src 4781 4782 [[https://www.nongnu.org/geiser][Geiser]] is, among other things, a repl for schemes. I try to use one, mostly for educational purposes. I chose guile because it also claims to support =emacs-lisp= and might be the future vm on which emacs-lisp will run. 4783 4784 #+begin_src emacs-lisp 4785 ;; Specific implementations depend on geiser 4786 (use-package geiser-guile 4787 :straight (:host gitlab :repo "emacs-geiser/guile") 4788 :defer t 4789 :commands (run-guile) 4790 :hook ((geiser-repl-mode . mrb/prettify-symbols)) 4791 4792 :custom 4793 (geiser-guile-binary "/usr/bin/guile") 4794 (geiser-active-implementations '(guile)) 4795 (geiser-default-implementation 'guile) 4796 4797 ;; Do NOT evaluate on return when inside an expression 4798 (geiser-repl-send-on-return-p nil) 4799 4800 ;; start repl and eval results inline in buffer 4801 ;; TODO I do want this eval result to be transient, not inserted! 4802 (geiser-mode-start-repl-p t) 4803 (geiser-mode-eval-last-sexp-to-buffer t) 4804 (geiser-mode-eval-to-buffer-prefix " ;;=> ") 4805 4806 ) 4807 #+end_src 4808 4809 Similar to using eros for emacs-lisp, I use geiser-eros for languages that geiser supports. 4810 4811 #+begin_src emacs-lisp 4812 (use-package geiser-eros 4813 :after (eros geiser) 4814 :straight '(:type git :host sourcehut :repo "sokolov/geiser-eros") 4815 :config 4816 ;; Make sure geiser does not insert eval into buffer 4817 (setq geiser-mode-eval-last-sexp-to-buffer nil) 4818 (geiser-eros-mode 1)) 4819 #+end_src 4820 While we're in the scheme section, let's configure =guix= and friends, which use scheme heavily 4821 4822 #+begin_src emacs-lisp 4823 (use-package guix 4824 :config 4825 ;; geiser-guile-load-path is set in personal.el 4826 4827 ;; Use tempel templates 4828 (with-eval-after-load 'tempel 4829 ;; Ensure tempel-path is a list -- it may also be a string. 4830 (add-to-list 'tempel-path "~/dat/src/guix/etc/snippets/tempel/*")) 4831 4832 ;; guix uses debbugs, but this should probably be closer to mu4e and gnus config 4833 (use-package debbugs)) 4834 #+end_src 4835 **** Common lisp 4836 The defacto standard for a development environment in Emacs for common-lisp is either the =slime= or =sly= package. The latter seems a bit more modern and easier to configure, so trying that one 4837 4838 #+begin_src emacs-lisp 4839 (use-package sly 4840 :hook ((sly-mrepl-mode . sly-mrepl-font-lock-setup)) 4841 4842 :init 4843 4844 ;; Copy the keywords from lisp mode 4845 (defvar sly-mrepl-font-lock-keywords lisp-el-font-lock-keywords-2) 4846 4847 ;; Set them up 4848 (setq sly-mrepl-font-lock-keywords 4849 (cons '(sly-mrepl-font-lock-find-prompt 4850 . 'sly-mrepl-prompt-face) 4851 sly-mrepl-font-lock-keywords)) 4852 (defun sly-mrepl-font-lock-setup () 4853 (setq font-lock-defaults 4854 '(sly-mrepl-font-lock-keywords 4855 ;; From lisp-mode.el 4856 nil nil (("+-*/.<>=!?$%_&~^:@" . "w")) nil 4857 (font-lock-syntactic-face-function 4858 . lisp-font-lock-syntactic-face-function)))) 4859 4860 :custom 4861 ;; Prevent usage of linedit when we are inside emacs, it's just for the terminal 4862 ;; Q: How about vterm? 4863 (inferior-lisp-program "sbcl –-noinform –-no-linedit") 4864 4865 (sly-lisp-implementations 4866 '((sbcl ("sbcl" "--core" (mrb/home-file ".sbcl/sbcl.core-for-sly"))) 4867 (qlot ("qlot" "exec" "sbcl")))) 4868 (sly-net-coding-system 'utf-8-unix) 4869 4870 :config 4871 ;; Nyxt exposes a repl we can use 4872 <<nyxt-sly-config>> 4873 4874 ;; Correct the REPL prompt 4875 (defun sly-mrepl-font-lock-find-prompt (limit) 4876 ;; Rough: (re-search-forward "^\\w*>" limit t) 4877 (let (beg end) 4878 (when (setq beg (text-property-any 4879 (point) limit 'sly-mrepl-prompt-face t)) 4880 (setq end (or (text-property-any 4881 beg limit 'sly-mrepl-prompt-face nil) 4882 limit)) 4883 (goto-char beg) 4884 (set-match-data (list beg end)) 4885 t)))) 4886 #+end_src 4887 *** Gcode 4888 I'm not in the habit of editing gcode, but every now and then I need to look at it and I want it reasonably decent. My use-case is just sending gcode to my 3D-printer which runs Marlin firmware. 4889 4890 Using =define-generic-mode= is more than enough to define something useful for me. 4891 4892 #+begin_src emacs-lisp 4893 (use-package generic-x 4894 :straight (:type built-in)) 4895 4896 (define-generic-mode marlin-gcode-mode 4897 '((";")) ;; ; starts a comment 4898 (apply 'append 4899 (mapcar #'(lambda (s) (list (upcase s) (downcase s) (capitalize s))) 4900 '("keywords?" "exist?"))) 4901 '(("\\([GM]\\)[0-9]+" (1 font-lock-function-name-face)) ; code letters 4902 ("\\([ABCDEFHIJKLNOPQRSTUVWXYZ]\\)[-]?[0-9]+" (1 font-lock-string-face)) ; parameter letters 4903 ("\\([\-+]?[0-9]*\\.[0-9]+\\)" (1 font-lock-constant-face)) ; 9.9 type numbers 4904 ("\\([\-+]?[0-9]+\\)" (1 font-lock-constant-face))) ; 999 type numbers 4905 '("\\.gcode\\'") 4906 nil 4907 "Mode for marlin g-code files.") 4908 #+end_src 4909 *** Openscad 4910 Openscad is a script based 3d parametric drawing program. Obviously I'm editing those scripts in emacs. 4911 4912 #+begin_src emacs-lisp 4913 (use-package scad-mode 4914 :custom 4915 (scad-keywords '("return" "true" "false" "include"))) 4916 #+end_src 4917 *** SQL 4918 SQL has a number of different dialects I use, depending on the database software in use. 4919 4920 #+begin_src emacs-lisp 4921 (use-package sql 4922 :straight (:type built-in) 4923 4924 :custom 4925 ;; sql-server is set in personal.el 4926 (sql-postgres-options '("-P" "pager=off" "-p 5434"))) 4927 #+end_src 4928 ** GIT integration 4929 A common factor in all my development is the use of git. For emacs this automatically means Magit. I've used eshell in the past but that config didn't work out. 4930 4931 *** Magit 4932 For most, if not all development work (and some other work too) I use git as the revision control system. In emacs that translates to using magit, so let's begin with bringing that in. 4933 4934 Magit forge is a sub-module for magit which helps in managing repositories in forges (notably github and gitlab). This looks very promising, albeit slow. 4935 4936 Features I'm interested in: issue commenting, pull requests 4937 4938 #+begin_src emacs-lisp 4939 (use-package magit 4940 :straight (:host github :repo "magit/magit") 4941 4942 :demand t 4943 :after (org fullframe) 4944 :commands magit-status 4945 :bind 4946 ("C-c m" . magit-status) 4947 4948 :init 4949 (fullframe magit-status magit-mode-quit-window) 4950 4951 :custom 4952 (magit-last-seen-setup-instructions "1.4.0") 4953 (magit-diff-refine-hunk 'all) 4954 4955 :custom-face 4956 ;; magit — inherit base, highlight variants get brighter bg 4957 (magit-diff-added ((t (:inherit diff-added)))) 4958 (magit-diff-removed ((t (:inherit diff-removed)))) 4959 (magit-diff-added-highlight ((t (:inherit diff-added :background "#3b5c3b")))) 4960 (magit-diff-removed-highlight ((t (:inherit diff-removed :background "#5c3b3b")))) 4961 (magit-diff-context ((t (:inherit diff-context)))) 4962 (magit-diff-context-highlight ((t (:inherit diff-context :background "#3b4252")))) 4963 (magit-diff-hunk-heading ((t (:inherit diff-hunk-header)))) 4964 (magit-diff-hunk-heading-highlight ((t (:inherit diff-hunk-header :background "#5e81ac")))) 4965 4966 4967 :config 4968 ;; Setting this to nil switches with-editor from using emacsclient to its 4969 ;; "sleeping editor" mode (signal-based). Counter-intuitive, but necessary: 4970 ;; emacsclient with $DISPLAY set sends -window-system to the Emacs server, 4971 ;; which creates a new X frame for COMMIT_EDITMSG. The sleeping editor 4972 ;; instead opens the buffer in the current frame via switch-to-buffer. 4973 ;; FIXME Not happy with this, it's counterintuitive, the manual says it should 4974 ;; not be used and if you do, only temporarily. So, needs attention. 4975 (setq with-editor-emacsclient-executable nil) 4976 4977 ;; Manage pre-commit from inside magit 4978 (use-package magit-pre-commit 4979 :straight (:host github :repo "DamianB-BitFlipper/magit-pre-commit.el")) 4980 4981 ;; Enable links to magit fromm org 4982 (use-package orgit) 4983 4984 (use-package forge 4985 :disabled 4986 :custom 4987 (forge-add-pullreq-refspec 'ask) 4988 (forge-pull-notifications t) 4989 (forge-topic-list-limit '(60 . 0)) 4990 4991 :config 4992 ;; Only show assigned stuff 4993 (magit-add-section-hook 4994 'magit-status-sections-hook 4995 'forge-insert-assigned-issues nil t) 4996 (magit-add-section-hook 4997 'magit-status-sections-hook 4998 'forge-insert-assigned-pullreqs nil t))) 4999 #+end_src 5000 5001 *** Claude Code IDE Integration - Commit Helper 5002 Helper function for Claude Code IDE to start a magit commit with pre-filled message. 5003 5004 #+begin_src emacs-lisp 5005 (defun mrb/claude-magit-commit (message &optional directory) 5006 "Prepare and start a magit commit with pre-filled message. 5007 This is called by Claude Code IDE when committing changes. 5008 MESSAGE is the commit message text. 5009 DIRECTORY is the git repository path (optional; uses current directory if nil)." 5010 (let* ((repo-dir (file-name-as-directory 5011 (expand-file-name (or directory default-directory)))) 5012 (tmp-file (make-temp-file "claude-commit-" nil ".txt"))) 5013 (write-region message nil tmp-file) 5014 (with-temp-buffer 5015 (setq default-directory repo-dir) 5016 (magit-commit-create (list "-F" tmp-file "-e"))))) 5017 #+end_src 5018 5019 Most of the magit work is done through its status screen and I would like to see issues and todo's and fixme's and other work that needs to be done in that screen. The forge package does that from a remote forge, like github, but many tasks are hiddenn in the sources already by myself. The magit-todos package scans for those things and show them in the magit screen 5020 5021 #+begin_src emacs-lisp 5022 (use-package magit-todos 5023 :disabled nil 5024 :after (magit) 5025 :custom 5026 (magit-todos-insert-after '(bottom)) 5027 (magit-todos-exclude-globs '("org-config.org")) 5028 :config 5029 (magit-todos-mode 1)) 5030 #+end_src 5031 5032 *** Committing automatically 5033 I have lost a number of changes in the past because I reverted a file, made a mistake or whatever. Some of these mistakes can be reverted easily if saves are automatically committed 5034 5035 Rather than using an after save hook, there is a minor git-auto-commit mode package which does just what I need. 5036 5037 There is not much to configure for this minor mode. There are a couple of ways to enable it: 5038 1. file-local variable (put it in the file to be autocommitted) 5039 2. directory-local variable (make a =.dir-locals.el= file); this enables it for all files in the directory 5040 3. as a hook 5041 5042 I'm using the first method on relevant files. The disadvantage of this method is that you have to think about it for each file, so perhaps a =.dir-locals.el= is a better solution. 5043 5044 I am considering using a generic hook again to enable the method and either using =git commit --amend= and commit squashing if working on more structured commits. For files that I really do not want autocommit to run I can use a file local variable to disable the hook (or the minor mode) 5045 5046 #+begin_src emacs-lisp 5047 (use-package git-auto-commit-mode 5048 :defer t 5049 :hook ((org-journal-mode . git-auto-commit-mode)) ; journals are always autocommitted 5050 5051 :init 5052 (defun mrb/git-auto-commit-mode () 5053 "Enable git-auto-commit-mode unless editing the config file." 5054 (interactive) 5055 (unless (and (buffer-file-name) 5056 (string= (buffer-file-name) config-file)) 5057 (git-auto-commit-mode)))) 5058 #+end_src 5059 *** Miscellaneous 5060 A collection of loosely development related things. 5061 5062 **** Online pastebin services 5063 I use github gists sometimes, but rather have more open options as well. These are mostly use to communicate longer pieces of text in chat programs, notably IRC 5064 5065 #+begin_src emacs-lisp 5066 (use-package webpaste 5067 :custom 5068 (webpaste-provider-priority '("paste.rs" 5069 "dpaste.com" 5070 "paste.ubuntu.com" 5071 "bpa.st" 5072 "gist.github.com")) 5073 (webpaste-paste-confirmation t) 5074 (webpaste-open-in-browser t)) 5075 #+end_src 5076 5077 **** Grepping files 5078 Searching for terms in files is a hard problem, but the vast majority of my use-cases can just be solved by searching for terms in the current project root, almost always defined by having a .git directory in a parent directory. So, let's start there and see what else I'll need. 5079 5080 I've chosen =ag= as the engine for /grepping/ the files because it's very fast. I would like to have a better UI within Emacs though (consult maybe?) 5081 5082 #+begin_src emacs-lisp 5083 (use-package ag) 5084 #+end_src 5085 ** AI tools 5086 Tools to use "AI" in Emacs. 5087 5088 *** Claude code 5089 5090 Use of /claude code/ inside emacs is fairly trivial. Once the cli tool is installed Emacs integrates with it through =vterm=. 5091 5092 #+begin_src emacs-lisp 5093 (use-package claude-code-ide 5094 :after (vterm autorevert) 5095 :straight (:type git :host github :repo "manzaltu/claude-code-ide.el") 5096 :bind ("C-c C-'" . claude-code-ide-menu) ; menu keybinding 5097 :custom 5098 (claude-code-ide-use-ide-diff t) ; use ediff, sometimes, see below 5099 (claude-code-ide-focus-claude-after-ediff nil) ; keep focus in control ediff 5100 (claude-code-ide-use-side-window nil) ; disable side windows to allow C-x 1 5101 :config 5102 (claude-code-ide-emacs-tools-setup) 5103 5104 ;; Configuration variables and constants 5105 (defcustom mrb/auto-revert-during-claude-files 5106 (list (mrb/emacs-file "mrb.org") 5107 (mrb/emacs-file "custom.el")) 5108 "Files to auto-revert without prompting when Claude Code IDE session is active." 5109 :type '(repeat string) 5110 :group 'mrb) 5111 5112 (defcustom mrb/claude-code-ide-diff-threshold 5 5113 "Number of line changes at which to switch from auto-accept to ediff review. 5114 Changes with fewer lines than this threshold are auto-accepted without prompting. 5115 Changes with this many lines or more open ediff for interactive review. 5116 Note: New files (no old version) will have all lines counted as changes." 5117 :type 'integer 5118 :group 'claude-code-ide) 5119 5120 (defconst mrb/claude-code-ide-diff-accepted 5121 '((type . "text") (text . "FILE_SAVED")) 5122 "MCP response indicating diff was accepted and file should be saved.") 5123 5124 (defun mrb/org-reveal-context-on-open (result) 5125 "Show heading context around point when opening an org file via Claude IDE." 5126 (when (derived-mode-p 'org-mode) 5127 (org-show-context 'default)) 5128 result) 5129 5130 (defun mrb/org-fold-show-all-on-ediff-prepare () 5131 "Show all org-mode content when preparing buffer for ediff." 5132 (when (and (derived-mode-p 'org-mode) 5133 (fboundp 'org-fold-show-all)) 5134 (org-fold-show-all))) 5135 5136 (defun mrb/claude-session-active-p (&optional file-path) 5137 "Check if Claude Code IDE has an active session. 5138 If FILE-PATH is provided, check for session in that file's project. 5139 If FILE-PATH is nil, check if ANY session is active." 5140 (when (featurep 'claude-code-ide-mcp) 5141 (condition-case nil 5142 (if file-path 5143 (when-let* ((project-dir (expand-file-name (file-name-directory file-path))) 5144 (session (claude-code-ide-mcp--get-session-for-project project-dir))) 5145 t) 5146 (and (fboundp 'claude-code-ide-mcp--get-current-session) 5147 (claude-code-ide-mcp--get-current-session))) 5148 (error nil)))) 5149 5150 (defun mrb/silent-revert-p (&optional file-path) 5151 "Return t if FILE-PATH should auto-revert silently (whitelist + active session). 5152 If FILE-PATH is nil, use the current buffer's file." 5153 (let ((file-path (or file-path (buffer-file-name)))) 5154 (when file-path 5155 (let* ((file-canonical (file-truename file-path)) 5156 (whitelist-canonical (mapcar #'file-truename mrb/auto-revert-during-claude-files))) 5157 (and (member file-canonical whitelist-canonical) 5158 (mrb/claude-session-active-p file-path)))))) 5159 5160 (defun mrb/revert-with-claude-awareness (orig-func arg1 arg2) 5161 "Revert buffer silently if whitelisted AND Claude session active." 5162 (if (mrb/silent-revert-p) 5163 ;; arg2 is t => silent revert 5164 (funcall orig-func arg1 t) 5165 ;; normal revert behaviour (may prompt) 5166 (funcall orig-func arg1 arg2))) 5167 5168 ;; Wrap auto-revert behavior with our Claude-aware decision logic 5169 ;; Remove old advice if it exists, then add the corrected version 5170 (advice-remove 'revert-buffer--default #'mrb/revert-with-claude-awareness) 5171 (advice-add 'revert-buffer--default :around #'mrb/revert-with-claude-awareness) 5172 5173 (advice-add 'claude-code-ide-mcp-handle-open-file 5174 :filter-return #'mrb/org-reveal-context-on-open) 5175 5176 ;; Unfold org-mode buffers when preparing for ediff 5177 (with-eval-after-load 'ediff 5178 (add-hook 'ediff-prepare-buffer-hook #'mrb/org-fold-show-all-on-ediff-prepare)) 5179 5180 ;; Smart diff routing: y/n prompt for small changes, ediff for large changes 5181 (defun mrb/count-diff-lines (old-content new-content) 5182 "Count lines that differ between old and new content." 5183 (let ((old-lines (split-string old-content "\n")) 5184 (new-lines (split-string new-content "\n"))) 5185 (+ (length (seq-difference new-lines old-lines #'equal)) 5186 (length (seq-difference old-lines new-lines #'equal))))) 5187 5188 (defun mrb/claude-code-ide-diff-smart-router (orig-fn arguments) 5189 "Route diff review: auto-accept small changes, ediff for large changes. 5190 For small diffs (<threshold lines), auto-accepts and saves the file. 5191 For large diffs (>=threshold lines), opens ediff for interactive review. 5192 Falls back to ediff if session lookup fails." 5193 (condition-case err 5194 (let* ((old-file-path (alist-get 'old_file_path arguments)) 5195 (new-content (alist-get 'new_file_contents arguments)) 5196 (tab-name (alist-get 'tab_name arguments)) 5197 ;; Read old file if it exists (new files have no old content) 5198 (old-content (and old-file-path (file-exists-p old-file-path) 5199 (with-temp-buffer 5200 (insert-file-contents old-file-path) 5201 (buffer-string)))) 5202 ;; Count actual lines changed 5203 (num-changes (if old-content 5204 (mrb/count-diff-lines old-content new-content) 5205 (length (split-string new-content "\n")))) 5206 ;; Get session for sending MCP response (try both methods, prefer find-session-for-file) 5207 (session (or (and (fboundp 'claude-code-ide-mcp--find-session-for-file) 5208 (claude-code-ide-mcp--find-session-for-file old-file-path)) 5209 (and (fboundp 'claude-code-ide-mcp--get-current-session) 5210 (claude-code-ide-mcp--get-current-session))))) 5211 (message "claude-code-ide diff: %d line(s) changed %s" num-changes 5212 (if (>= num-changes mrb/claude-code-ide-diff-threshold) "(ediff)" "(auto-accept)")) 5213 (if (>= num-changes mrb/claude-code-ide-diff-threshold) 5214 ;; Large changes: open ediff via original handler 5215 (funcall orig-fn arguments) 5216 ;; Small changes: auto-accept and send MCP response 5217 (if session 5218 (claude-code-ide-mcp-complete-deferred 5219 session "openDiff" 5220 (list mrb/claude-code-ide-diff-accepted 5221 `((type . "text") (text . ,new-content))) 5222 tab-name) 5223 ;; Fallback if session lookup fails: use ediff instead 5224 (progn 5225 (message "claude-code-ide: session lookup failed, falling back to ediff") 5226 (funcall orig-fn arguments))))) 5227 (error 5228 ;; If anything goes wrong, fall back to ediff review 5229 (message "claude-code-ide: error in diff router (%s), falling back to ediff" (error-message-string err)) 5230 (funcall orig-fn arguments)))) 5231 5232 ;; Wire up the smart router 5233 (advice-remove 'claude-code-ide-mcp-handle-open-diff #'mrb/claude-code-ide-diff-smart-router) 5234 (advice-add 'claude-code-ide-mcp-handle-open-diff :around #'mrb/claude-code-ide-diff-smart-router)) 5235 5236 #+end_src 5237 ** Ansible 5238 Not really a development setting, but closely related to it, as eventually the programs written need to be deployed. Most of the time, while deploying, ansible plays a role in this and it's now worth having a dedicated configuration for editing the YAML files for it. 5239 5240 For now, just the ansible package, and some access to its documentation will suffice 5241 #+begin_src emacs-lisp 5242 (use-package ansible 5243 :after yaml-mode 5244 :init 5245 (use-package ansible-doc 5246 :hook (yaml-mode . ansible-doc-mode)) 5247 :hook (yaml-mode . (lambda () (ansible 1)))) 5248 #+end_src 5249 * Finale 5250 When we are all done with this, provide it. 5251 5252 #+begin_src emacs-lisp 5253 (provide 'mrb) 5254 ;;; mrb ends here 5255 #+end_src 5256 5257 # -*- eval: (git-auto-commit-mode 1) 5258 # Local Variables: 5259 # writefreely-post-id: "wf83bq5jwz" 5260 # writefreely-post-token: nil 5261 # End: