#+TITLE: Emacs, emacs, emacs #+AUTHOR: Case Duckworth #+PROPERTY: header-args :tangle config.el :comments both :mkdirp yes #+EXPORT_FILE_NAME: README.md #+OPTIONS: toc:nil #+BANKRUPTCY_COUNT: 3 #+Time-stamp: <2020-12-23 20:27:53 acdw> Let’s configure Emacs using Org mode, they said. It’ll be fun, they said. * Pave the way ** Correct =exec-path= #+begin_src emacs-lisp (let ((win-downloads "c:/Users/aduckworth/Downloads")) (dolist (path (list ;; Linux (expand-file-name "bin" user-emacs-directory) (expand-file-name "~/bin") (expand-file-name "~/.local/bin") (expand-file-name "~/Scripts") ;; Windows (expand-file-name "emacs/bin" win-downloads) (expand-file-name "m/usr/bin" win-downloads) (expand-file-name "m/mingw64/bin" win-downloads) (expand-file-name "PortableGit/bin" win-downloads) (expand-file-name "PortableGit/usr/bin" win-downloads))) (when (file-exists-p path) (add-to-list 'exec-path path)))) #+end_src ** Package management *** Straight.el Since for whatever reason, Straight can't bootstrap itself on Windows -- I've wrapped it in a function here and added the direct git command when it errors. #+begin_src emacs-lisp (defun acdw/bootstrap-straight () (defvar bootstrap-version) (let ((bootstrap-file (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory)) (bootstrap-version 5)) (unless (file-exists-p bootstrap-file) (with-current-buffer (url-retrieve-synchronously (concat "https://raw.githubusercontent.com/" "raxod502/straight.el/develop/install.el") 'silent 'inhibit-cookies) (goto-char (point-max)) (eval-print-last-sexp))) (load bootstrap-file nil 'nomessage))) (unless (ignore-errors (acdw/bootstrap-straight)) (message "Straight.el didn't bootstrap correctly. Cloning directly...") (call-process "git" nil (get-buffer-create "*bootstrap-straight-messages*") nil "clone" "https://github.com/raxod502/straight.el" (expand-file-name "straight/repos/straight.el" user-emacs-directory)) (acdw/bootstrap-straight)) #+end_src ** Customize variables *** Put customizations in a separate file #+begin_src emacs-lisp (setq custom-file (expand-file-name "custom.el" user-emacs-directory)) #+end_src *** A macro for ease of customization #+begin_src emacs-lisp (defmacro cuss (var val &optional docstring) "Basically `:custom' from `use-package', broken out." (declare (indent 2) (doc-string 3)) `(funcall (or (get ',var 'custom-set) #'set-default) ',var ,val)) #+end_src ** Keep a tidy =~/.emacs= #+begin_src emacs-lisp (straight-use-package 'no-littering) (cuss backup-directory-alist `((".*" . ,(no-littering-expand-var-file-name "backup/"))) "Where to store backup files.") (cuss auto-save-file-name-transforms `((".*" ,(no-littering-expand-var-file-name "autosaves/") t)) "Where to store auto-save files.") (cuss save-place-file (no-littering-expand-var-file-name "places") "Where to store place files.") (cuss undo-fu-session-directory (no-littering-expand-var-file-name "undos/") "Where to store undo information.") (cuss elpher-certificate-directory (no-littering-expand-var-file-name "elpher-certificates/") "Where to store elpher client certificates.") ;; Make all directories defined above (dolist (dir '("backup" "autosaves" "undos" "elpher-certificates")) (make-directory (no-littering-expand-var-file-name dir) 'parents)) #+end_src ** About me #+begin_src emacs-lisp (setq user-full-name "Case Duckworth" user-mail-address "acdw@acdw.net") #+end_src * Look and Feel ** Simplify the UI *** Tool bars and menu bars #+begin_src emacs-lisp (cuss default-frame-alist '((tool-bar-lines . 0) (menu-bar-lines . 0)) "On a default frame, show no tool bars or menu bars.") (menu-bar-mode -1) (tool-bar-mode -1) #+end_src *** Scroll bars #+begin_src emacs-lisp (add-to-list 'default-frame-alist '(vertical-scroll-bars . nil)) (scroll-bar-mode -1) (add-to-list 'default-frame-alist '(horizontal-scroll-bars . nil)) (horizontal-scroll-bar-mode -1) #+end_src *** Dialog boxen #+begin_src emacs-lisp (cuss use-dialog-box nil "Don't show dialog boxes.") #+end_src *** Shorten confirmations #+begin_src emacs-lisp (fset 'yes-or-no-p #'y-or-n-p) #+end_src *** Remove the bell #+begin_src emacs-lisp ;(cuss visible-bell ; (not (string= (system-name) "larry")) ; "Only show a visible bell when on 'larry'.") (defun acdw/ring-bell-function () "Custom bell-ringing function." (let ((orig-face (face-foreground 'mode-line))) (set-face-foreground 'modeline "#F2804F") (run-with-idle-timer 0.1 nil (lambda (fg) (set-face-foreground 'mode-line fg)) orig-face))) (cuss ring-bell-function #'acdw/ring-bell-function) #+end_src *** Tell Ediff to setup windows better #+begin_src emacs-lisp (declare-function ediff-setup-windows-plain "ediff-wind.el") (cuss ediff-window-setup-function #'ediff-setup-windows-plain) #+end_src ** Tweak the remaining UI *** Fringes #+begin_src emacs-lisp (add-to-list 'default-frame-alist '(left-fringe-width . 2)) (add-to-list 'default-frame-alist '(right-fringe-width . 2)) #+end_src *** Minibuffer **** Setup the minibuffer frame #+begin_src emacs-lisp (cuss minibuffer-frame-alist '((width . 80) (height . 2) (vertical-scrollbars . nil)) "Set up the minibuffer frame.") (set-window-scroll-bars (minibuffer-window) nil nil) #+end_src **** Keep the cursor from going into the prompt #+begin_src emacs-lisp (cuss minibuffer-prompt-properties '(read-only t cursor-intangible t face minibuffer-prompt) "Disable moving the cursor into the minibuffer prompt.") #+end_src *** Tabs **** Show the tabs as current buffer, plus window count #+begin_src emacs-lisp (cuss tab-bar-tab-name-function #'tab-bar-tab-name-current-with-count) #+end_src **** Only show the tab bar when there's more than one tab #+begin_src emacs-lisp (cuss tab-bar-show 1 "Show the tab bar only when there's more than 1 tab.") #+end_src *** Cursor #+begin_src emacs-lisp (cuss cursor-type 'bar "Show a vertical bar for the cursor.") (cuss cursor-in-non-selected-windows 'hollow "In inactive windows, make the cursor an empty box.") (blink-cursor-mode 0) #+end_src *** Buffer names #+begin_src emacs-lisp (require 'uniquify) (cuss uniquify-buffer-name-style 'forward) #+end_src *** Buffer boundaries #+begin_src emacs-lisp (cuss indicate-buffer-boundaries '((up . right) (down . right) (t . nil)) "Show arrows on the right when there's more to the buffer up or down.") (cuss indicate-empty-lines t "Show a bitmap on the left for empty lines after the end of a buffer.") #+end_src ** Windows *** Winner mode #+begin_src emacs-lisp (when (fboundp 'winner-mode) (winner-mode +1)) #+end_src *** Windmove #+begin_src emacs-lisp (cuss windmove-create-window t "Create windows in a direction if they don't exist.") (cuss windomove-wrap-around t "Wrap window movements around frame edges.") (windmove-default-keybindings) #+end_src *** Pop some buffers up in the same window from [[https://github.com/link0ff/emacs-init][link0ff]]. #+begin_src emacs-lisp (push `(,(rx bos "*" (or "Help" "Apropos" "Colors" "Buffer List" "Command History" "Dictionary" "Locate" "Messages" "Proced" "eww" "snd" (and "gud-" (+ (any "a-z0-9"))) "compilation" "grep" "erlang" "haskell" ;; Handle both "*shell*" and e.g. "*emacs-shell*" ;; generated by `project-shell': (and (? (* nonl) "-") "shell") "Shell Command Output" (and "SQL: " (+ (any "A-za-z"))) "Diff" "vc-dir" "vc-log" "vc-search-log") "*" ;; Uniquifed buffer name with optional suffix in angle brackets (? (and "<" (+ (not (any ">"))) ">")) eos) display-buffer-same-window (inhibit-same-window . nil)) display-buffer-alist) (defun display-buffer-from-help-p (_buffer-name _action) (unless current-prefix-arg (with-current-buffer (window-buffer) (eq major-mode 'help-mode)))) (push '(display-buffer-from-help-p display-buffer-same-window) display-buffer-alist) #+end_src ** Startup #+begin_src emacs-lisp (cuss inhibit-startup-screen t "Don't show Emacs' startup buffer.") (cuss initial-buffer-choice t "Start at *scratch*.") (cuss initial-scratch-message "" "Empty *scratch*.") #+end_src ** Theme #+begin_src emacs-lisp (straight-use-package '(modus-themes :host gitlab :repo "protesilaos/modus-themes" :branch "main")) (cuss modus-themes-slanted-constructs t) (cuss modus-themes-bold-constructs t) (cuss modus-themes-fringes nil) (cuss modus-themes-mode-line '3d) (cuss modus-themes-syntax 'yellow-comments) (cuss modus-themes-intense-hl-line nil) (cuss modus-themes-paren-match 'intense-bold) (cuss modus-themes-links nil) (cuss modus-themes-no-mixed-fonts nil) (cuss modus-themes-prompts nil) (cuss modus-themes-completions nil) (cuss modus-themes-diffs nil) (cuss modus-themes-org-blocks 'grayscale) (cuss modus-themes-headings '((1 . line) (t . t))) (cuss modus-themes-variable-pitch-headings t) (cuss modus-themes-scale-headings t) (cuss modus-themes-scale-1 1.1) (cuss modus-themes-scale-2 1.15) (cuss modus-themes-scale-3 1.21) (cuss modus-themes-scale-4 1.27) (cuss modus-themes-scale-5 1.33) ;; :custom-face (custom-set-faces `(font-lock-comment-face ((t (:inherit (custom-comment italic variable-pitch)))))) (load-theme 'modus-operandi t) #+end_src *** Change theme based on time of day #+begin_src emacs-lisp (cuss calendar-latitude 30.4515) (cuss calendar-longitude -91.1871) (straight-use-package 'circadian) (cuss circadian-themes '((:sunrise . modus-operandi) (:sunset . modus-vivendi))) (circadian-setup) #+end_src *** Modeline #+begin_src emacs-lisp (straight-use-package 'smart-mode-line) (cuss sml/no-confirm-load-theme t) (sml/setup) #+end_src **** Rich minority Since this /comes/ with smart mode line, I’m just going to use it, instead of =diminish= or another package. I do have to write this helper function, though, to add things to the whitelist. #+begin_src emacs-lisp (defun rm/whitelist-add (regexp) "Add a REGEXP to the whitelist for `rich-minority'." (if (listp 'rm--whitelist-regexps) (add-to-list 'rm--whitelist-regexps regexp) (setq rm--whitelist-regexps `(,regexp))) (setq rm-whitelist (mapconcat 'identity rm--whitelist-regexps "\\|"))) (straight-use-package 'rich-minority) (rm/whitelist-add "^$") #+end_src *** Fonts **** Define fonts #+begin_src emacs-lisp (defun set-face-from-alternatives (face fonts) (catch :return (dolist (font fonts) (when (find-font (font-spec :family (car font))) (apply #'set-face-attribute `(,face nil :family ,(car font) ,@(cdr font))) (throw :return font))))) (defun acdw/setup-fonts () "Setup fonts. This has to happen after the frame is setup for the first time, so it should be added to `window-setup-hook'. It removes itself from that hook." (interactive) (when (display-graphic-p) (set-face-from-alternatives 'default '(("Libertinus Mono" :height 110) ("Linux Libertine Mono O" :height 110) ("Go Mono" :height 100) ("Consolas" :height 100))) (set-face-from-alternatives 'fixed-pitch '(("Libertinus Mono" :height 110) ("Linux Libertine Mono O" :height 110) ("Go Mono" :height 100) ("Consolas" :height 100))) (set-face-from-alternatives 'variable-pitch '(("Libertinus Serif" :height 120) ("Linux Libertine O" :height 120) ("Georgia" :height 110))) (remove-function after-focus-change-function #'acdw/setup-fonts))) (add-function :before after-focus-change-function #'acdw/setup-fonts) #+end_src **** Variable-pitch in text modes #+begin_src emacs-lisp (add-hook 'text-mode-hook #'variable-pitch-mode) #+end_src **** Line spacing #+begin_src emacs-lisp (cuss line-spacing 0.1) #+end_src **** Unicode fonts #+begin_src emacs-lisp (straight-use-package 'unicode-fonts) (with-eval-after-load 'unicode-fonts (unicode-fonts-setup)) #+end_src * Interactivity ** Async #+begin_src emacs-lisp (straight-use-package 'async) (autoload 'dired-async-mode "dired-async.el" nil t) (dired-async-mode +1) (async-bytecomp-package-mode +1) #+end_src ** Completing-read *** Shadow file names #+begin_src emacs-lisp (cuss file-name-shadow-properties '(invisible t)) (file-name-shadow-mode +1) #+end_src *** Selectrum #+begin_src emacs-lisp (straight-use-package 'selectrum) (require 'selectrum) (selectrum-mode +1) #+end_src *** Prescient #+begin_src emacs-lisp (straight-use-package 'prescient) (require 'prescient) (prescient-persist-mode +1) (straight-use-package 'selectrum-prescient) (with-eval-after-load 'prescient (with-eval-after-load 'selectrum (selectrum-prescient-mode +1))) #+end_src *** Consult #+begin_src emacs-lisp (straight-use-package '(consult :host github :repo "minad/consult")) (require 'consult) (define-key ctl-x-map "b" #'consult-buffer) (define-key ctl-x-map "4b" #'consult-buffer-other-window) (define-key ctl-x-map "5b" #'consult-buffer-other-frame) (define-key goto-map "o" #'consult-outline) (define-key goto-map "l" #'consult-line) (global-set-key (kbd "M-y") #'consult-yank-pop) (define-key help-map "a" #'consult-apropos) (fset 'multi-occur #'consult-multi-occur) (straight-use-package '(consult-selectrum :host github :repo "minad/consult")) #+end_src *** Marginalia #+begin_src emacs-lisp (straight-use-package '(marginalia :host github :repo "minad/marginalia" :branch "main")) (cuss marginalia-annotators (if (eq system-type 'windows-nt) '(marginalia-annotators-light marginalia-annotators-heavy) '(marginalia-annotators-heavy marginalia-annotators-light))) (marginalia-mode +1) #+end_src ** Ignore case #+begin_src emacs-lisp (cuss completion-ignore-case t) (cuss read-buffer-completion-ignore-case t) (cuss read-file-name-completion-ignore-case t) #+end_src ** Search #+begin_src emacs-lisp (straight-use-package 'ctrlf) (require 'ctrlf) (cuss ctrlf-show-match-count-at-eol nil) (cuss ctrlf-mode-bindings '(("C-s" . ctrlf-forward-regexp) ("C-r" . ctrlf-backward-regexp) ("C-M-s" . ctrlf-forward-literal) ("C-M-r" . ctrlf-backward-literal) ("M-s _" . ctrlf-forward-symbol) ("M-s ." . ctrlf-forward-symbol-at-point))) (ctrlf-mode +1) #+end_src ** Replace #+begin_src emacs-lisp (straight-use-package 'anzu) (global-anzu-mode +1) (cuss anzu-replace-to-string-separator " → ") (global-set-key (kbd "M-%") #'anzu-query-replace-regexp) (global-set-key (kbd "M-C-%") #'anzu-query-replace) #+end_src ** Mouse *** Fix scrolling in margins This is not /quite/ correct yet. For example, scrolling in the margins with a trackpad isn’t picked up (a trackpad sends different mouse events). #+begin_src emacs-lisp (dolist (vec '([left-margin wheel-down] [right-margin wheel-down] [left-margin wheel-up] [right-margin wheel-up])) (global-set-key vec #'mwheel-scroll)) #+end_src ** Keyboard *** Use =ESC= as a cancel key From [[https://github.com/link0ff/emacs-init][link0ff]]. I thought they made a great point that =ESC= isn’t necessary to copy the =META= key on window-systems, which is where I use Emacs, anyway. #+begin_src emacs-lisp (when window-system (global-set-key [escape] 'keyboard-escape-quit) (define-key isearch-mode-map [escape] 'isearch-cancel)) #+end_src *** Make =C-z= more useful as a prefix key Also from link0ff. See the above for a link. #+begin_src emacs-lisp (defvar acdw/map (let ((map (make-sparse-keymap)) (c-z (global-key-binding "\C-z"))) (global-unset-key "\C-z") (define-key global-map "\C-z" map) (define-key map "\C-z" c-z) map)) (run-hooks 'acdw/map-defined-hook) #+end_src *** Which-key #+begin_src emacs-lisp (straight-use-package 'which-key) (which-key-mode +1) #+end_src *** Bindings **** Switch to another window #+begin_src emacs-lisp (global-set-key (kbd "M-o") #'other-window) #+end_src * Persistence ** Save history #+begin_src emacs-lisp (require 'savehist) (cuss savehist-additional-variables '(kill-ring search-ring regexp-search-ring)) (cuss savehist-save-minibuffer-history t) (cuss history-length t) (cuss history-delete-duplicates t) (savehist-mode +1) #+end_src ** Save places in files #+begin_src emacs-lisp (require 'saveplace) (cuss save-place-forget-unreadable-files (not (eq system-type 'windows-nt))) (save-place-mode 1) #+end_src ** Recent files #+begin_src emacs-lisp (require 'recentf) (cuss recentf-max-menu-items 100) (cuss recentf-max-saved-items 100) (with-eval-after-load 'no-littering (add-to-list 'recentf-exclude no-littering-var-directory) (add-to-list 'recentf-exclude no-littering-etc-directory)) (recentf-mode 1) #+end_src *** Easily navigate recent files #+begin_src emacs-lisp (defun recentf-find-file () "Find a recent file using `completing-read'." (interactive) (let ((file (completing-read "Recent file: " recentf-list nil t))) (when file (find-file file)))) (global-set-key (kbd "C-x C-r") #'recentf-find-file) #+end_src ** Undo #+begin_src emacs-lisp (straight-use-package 'undo-fu) (require 'undo-fu) (global-set-key (kbd "C-/") #'undo-fu-only-undo) (global-set-key (kbd "C-?") #'undo-fu-only-redo) (straight-use-package 'undo-fu-session) (require 'undo-fu-session) (cuss undo-fu-session-incompatible-files '("/COMMIT_EDITMSG\\'" "/git-rebase-todo\\'")) (global-undo-fu-session-mode +1) #+end_src * Editing ** Operate visually on lines #+begin_src emacs-lisp (global-visual-line-mode +1) #+end_src ** Require a final newline #+begin_src emacs-lisp (cuss require-final-newline t) #+end_src ** Killing & Yanking *** Replace selection when typing #+begin_src emacs-lisp (delete-selection-mode +1) #+end_src *** Save existing clipboard text into kill ring before replacing it #+begin_src emacs-lisp (cuss save-interprogram-paste-before-kill t) #+end_src *** Sync the system clipboard and the kill ring #+begin_src emacs-lisp (cuss yank-pop-change-selection t) #+end_src ** So long mode #+begin_src emacs-lisp (when (fboundp 'global-so-long-mode) (global-so-long-mode)) #+end_src ** Multiple cursors #+begin_src emacs-lisp (straight-use-package 'multiple-cursors) (global-set-key (kbd "C->") #'mc/mark-next-like-this) (global-set-key (kbd "C-<") #'mc/mark-previous-like-this) (global-set-key (kbd "C-c C-<") #'mc/mark-all-like-this) #+end_src ** Expand region #+begin_src emacs-lisp (straight-use-package 'expand-region) (global-set-key (kbd "C-=") #'er/expand-region) (global-set-key (kbd "C-+") #'er/contract-region) #+end_src ** Highlight modified regions #+begin_src emacs-lisp (straight-use-package 'goggles) (cuss goggles-pulse nil) (goggles-mode +1) #+end_src * Files ** Encoding *** UTF-8 #+begin_src emacs-lisp (set-language-environment "UTF-8") (set-terminal-coding-system 'utf-8) (cuss locale-coding-system 'utf-8) (set-default-coding-systems 'utf-8) (set-selection-coding-system 'utf-8) (prefer-coding-system 'utf-8) #+end_src *** Convert all files to UNIX-style line endings from [[https://www.emacswiki.org/emacs/EndOfLineTips][Emacs Wiki]]. #+begin_src emacs-lisp (defun ewiki/no-junk-please-were-unixish () "Convert line endings to UNIX, dammit." (let ((coding-str (symbol-name buffer-file-coding-system))) (when (string-match "-\\(?:dos\\|mac\\)$" coding-str) (set-buffer-file-coding-system 'unix)))) #+end_src I add it to the ~find-file-hook~ /and/ ~before-save-hook~ because I don't want to ever work with anything other than UNIX line endings ever again. I just don't care. Even Microsoft Notepad can handle UNIX line endings, so I don't want to hear it. #+begin_src emacs-lisp (add-hook 'find-file-hook #'ewiki/no-junk-please-were-unixish) (add-hook 'before-save-hook #'ewiki/no-junk-please-were-unixish) #+end_src ** Backups #+begin_src emacs-lisp (cuss backup-by-copying 1) (cuss delete-old-versions -1) (cuss version-control t) (cuss vc-make-backup-files t) #+end_src ** Auto-saves #+begin_src emacs-lisp (auto-save-visited-mode 1) #+end_src ** Revert files #+begin_src emacs-lisp (cuss auto-revert-verbose nil) (global-auto-revert-mode +1) #+end_src ** Add a timestamp to files #+begin_src emacs-lisp (add-hook 'before-save-hook #'time-stamp) #+end_src * Programming ** Which function are we in? #+begin_src emacs-lisp (which-function-mode +1) #+end_src ** Parentheses *** Show parentheses #+begin_src emacs-lisp (cuss show-paren-delay 0 "Show matching parens immediately.") (cuss show-paren-style 'mixed "Show parenthesis, or whole expression, depending on visibility.") (cuss show-paren-when-point-in-periphery t "Show paren when point is near-to paren.") (cuss show-paren-when-point-inside-paren t "Show surrounding parens.") (add-hook 'prog-mode-hook #'show-paren-mode) #+end_src *** Smart parentheses #+begin_src emacs-lisp (straight-use-package 'smartparens) (require 'smartparens-config) (show-smartparens-global-mode +1) (add-to-list 'sp-ignore-modes-list 'org-mode) (add-hook 'prog-mode-hook #'smartparens-strict-mode) #+end_src ** COMMENT Line numbers #+begin_src emacs-lisp (defun acdw/enable-line-numbers () "Enable line numbers, through either `display-line-numbers-mode' or through `linum-mode'." (if (and (fboundp 'display-line-numbers-mode) (display-graphic-p)) (display-line-numbers-mode +1) (linum-mode +1))) (cuss display-line-numbers-width 2 "Always have at least 2 digits for line numbers.") (add-hook 'prog-mode-hook #'acdw/enable-line-numbers) #+end_src ** Indenting #+begin_src emacs-lisp (straight-use-package 'aggressive-indent) (global-aggressive-indent-mode +1) #+end_src ** Completion #+begin_src emacs-lisp (unless (eq system-type 'windows-nt) (straight-use-package 'company) (cuss company-idle-delay 0.1) (cuss company-minimum-prefix-length 2) (add-hook 'prog-mode-hook #'company-mode) (straight-use-package 'company-prescient) (add-hook 'company-mode-hook #'company-prescient-mode) (straight-use-package 'company-posframe) (with-eval-after-load 'company (company-posframe-mode +1) (define-key company-active-map (kbd "C-n") (lambda () (interactive) (company-complete-common-or-cycle +1))) (define-key company-active-map (kbd "C-p") (lambda () (interactive) (company-complete-common-or-cycle -1))))) #+end_src ** Languages *** Lua #+begin_src emacs-lisp (straight-use-package 'lua-mode) (add-to-list 'auto-mode-alist '("\\.lua\\'" . lua-mode)) (add-to-list 'interpreter-mode-alist '("lua" . lua-mode)) #+end_src *** Fennel #+begin_src emacs-lisp (straight-use-package 'fennel-mode) (add-to-list 'auto-mode-alist '("\\.fnl\\'" . fennel-mode)) #+end_src *** Emacs lisp #+begin_src emacs-lisp (cuss eval-expression-print-length nil "Don't truncate printed expressions by length.") (cuss eval-expression-print-level nil "Don't truncate printed expressions by level.") #+end_src * Writing ** Visual Fill Column #+begin_src emacs-lisp (straight-use-package 'visual-fill-column) (cuss split-window-preferred-function 'visual-fill-column-split-window-sensibly) (cuss visual-fill-column-center-text t) (cuss fill-column 80) (advice-add 'text-scale-adjust :after #'visual-fill-column-adjust) (add-hook 'text-mode-hook #'visual-fill-column-mode) #+end_src *** COMMENT Split windows /more/ sensibly from [[https://stackoverflow.com/questions/23659909/reverse-evaluation-order-of-split-height-threshold-and-split-width-threshold-in][Stack Overflow]]. #+begin_src emacs-lisp (defun my-split-window-sensibly (&optional window) (let ((window (or window (selected-window)))) (or (and (window-splittable-p window t) ;; Split window horizontally. (with-selected-window window (split-window-right))) (and (window-splittable-p window) ;; Split window vertically. (with-selected-window window (split-window-below))) (and (eq window (frame-root-window (window-frame window))) (not (window-minibuffer-p window)) ;; If WINDOW is the only window on its frame and is not the ;; minibuffer window, try to split it horizontally disregarding ;; the value of `split-width-threshold'. (let ((split-width-threshold 0)) (when (window-splittable-p window t) (with-selected-window window (split-window-right)))))))) (setq split-window-preferred-function #'my-split-window-sensibly) #+end_src ** Type nice-looking quote-type marks #+begin_src emacs-lisp (straight-use-package 'typo) (add-hook 'text-mode-hook #'typo-mode) #+end_src ** Insert /kaomoji/ #+begin_src emacs-lisp (straight-use-package 'insert-kaomoji) (global-set-key (kbd "C-x 8 k") #'insert-kaomoji) #+end_src * Applications ** Magit #+begin_src emacs-lisp (straight-use-package 'magit) (define-key acdw/map "g" #'magit-status) #+end_src ** Org mode I’ve put org mode under Applications, as opposed to Writing, because it’s more generally-applicable than that. #+begin_src emacs-lisp (straight-use-package 'org) (with-eval-after-load 'org (require 'org-tempo) (require 'ox-md) (define-key org-mode-map (kbd "M-n") #'outline-next-visible-heading) (define-key org-mode-map (kbd "M-p") #'outline-previous-visible-heading)) (cuss org-hide-emphasis-markers t) (cuss org-fontify-done-headline t) (cuss org-fontify-whole-heading-line t) (cuss org-fontify-quote-and-verse-blocks t) (cuss org-pretty-entities t) (cuss org-num-mode +1) (cuss org-src-tab-acts-natively t) (cuss org-src-fontify-natively t) (cuss org-src-window-setup 'current-window) (cuss org-confirm-babel-evaluate nil) (cuss org-directory "~/Org") #+end_src *** Org Agenda #+begin_src emacs-lisp (cuss org-agenda-files (no-littering-expand-etc-file-name "agenda-files")) (if (and (stringp org-agenda-files) (not (file-exists-p org-agenda-files))) (with-temp-buffer (write-file org-agenda-files))) (define-key acdw/map (kbd "C-a") #'org-agenda) #+end_src *** COMMENT Make bullets look like bullets #+begin_src emacs-lisp (font-lock-add-keywords 'org-mode '(("^ *\\([-+]\\) " (0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "•")))))) #+end_src *** [[http://kitchingroup.cheme.cmu.edu/blog/2017/04/09/A-better-return-in-org-mode/][A better return in Org mode]] #+begin_src emacs-lisp (require 'org-inlinetask) (defun scimax/org-return (&optional ignore) "Add new list item, heading or table row with RET. A double return on an empty element deletes it. Use a prefix arg to get regular RET." (interactive "P") (if ignore (org-return) (cond ((eq 'line-break (car (org-element-context))) (org-return t)) ;; Open links like usual, unless point is at the end of a line. ;; and if at beginning of line, just press enter. ((or (and (eq 'link (car (org-element-context))) (not (eolp))) (bolp)) (org-return)) ;; It doesn't make sense to add headings in inline tasks. Thanks Anders ;; Johansson! ((org-inlinetask-in-task-p) (org-return)) ;; checkboxes too ((org-at-item-checkbox-p) (org-insert-todo-heading nil)) ;; lists end with two blank lines, so we need to make sure we are also not ;; at the beginning of a line to avoid a loop where a new entry gets ;; created with only one blank line. ((org-in-item-p) (if (save-excursion (beginning-of-line) (org-element-property :contents-begin (org-element-context))) (org-insert-heading) (beginning-of-line) (delete-region (line-beginning-position) (line-end-position)) (org-return))) ;; org-heading ((org-at-heading-p) (if (not (string= "" (org-element-property :title (org-element-context)))) (progn (org-end-of-meta-data) (org-insert-heading-respect-content) (outline-show-entry)) (beginning-of-line) (setf (buffer-substring (line-beginning-position) (line-end-position)) ""))) ;; tables ((org-at-table-p) (if (-any? (lambda (x) (not (string= "" x))) (nth (- (org-table-current-dline) 1) (org-table-to-lisp))) (org-return) ;; empty row (beginning-of-line) (setf (buffer-substring (line-beginning-position) (line-end-position)) "") (org-return))) ;; fall-through case (t (org-return))))) (define-key org-mode-map (kbd "RET") 'scimax/org-return) #+end_src *** Insert blank lines from [[https://github.com/alphapapa/unpackaged.el#ensure-blank-lines-between-headings-and-before-contents][unpackaged.el]]. #+begin_src emacs-lisp ;;;###autoload (defun unpackaged/org-fix-blank-lines (&optional prefix) "Ensure that blank lines exist between headings and between headings and their contents. With prefix, operate on whole buffer. Ensures that blank lines exist after each headings's drawers." (interactive "P") (org-map-entries (lambda () (org-with-wide-buffer ;; `org-map-entries' narrows the buffer, which prevents us ;; from seeing newlines before the current heading, so we ;; do this part widened. (while (not (looking-back "\n\n" nil)) ;; Insert blank lines before heading. (insert "\n"))) (let ((end (org-entry-end-position))) ;; Insert blank lines before entry content (forward-line) (while (and (org-at-planning-p) (< (point) (point-max))) ;; Skip planning lines (forward-line)) (while (re-search-forward org-drawer-regexp end t) ;; Skip drawers. You might think that `org-at-drawer-p' ;; would suffice, but for some reason it doesn't work ;; correctly when operating on hidden text. This ;; works, taken from `org-agenda-get-some-entry-text'. (re-search-forward "^[ \t]*:END:.*\n?" end t) (goto-char (match-end 0))) (unless (or (= (point) (point-max)) (org-at-heading-p) (looking-at-p "\n")) (insert "\n")))) t (if prefix nil 'tree))) #+end_src **** Add a before-save-hook #+begin_src emacs-lisp (defun cribbed/org-mode-fix-blank-lines () (when (eq major-mode 'org-mode) (let ((current-prefix-arg 4)) ; Emulate C-u (call-interactively 'unpackaged/org-fix-blank-lines)))) (add-hook 'before-save-hook #'cribbed/org-mode-fix-blank-lines) #+end_src ** Elpher #+begin_src emacs-lisp (straight-use-package '(elpher :repo "git://thelambdalab.xyz/elpher.git" :branch "patch_multiple_buffers")) (cuss elpher-ipv4-always t) (custom-set-faces `(elpher-gemini-heading1 ((t (:inherit (modus-theme-heading1))))) `(elpher-gemini-heading2 ((t (:inherit (modus-theme-heading2))))) `(elpher-gemini-heading3 ((t (:inherit (modus-theme-heading3)))))) (defun elpher:eww-browse-url (original url &optional new-window) "Handle gemini/gopher links with eww." (cond ((string-match-p "\\`\\(gemini\\|gopher\\)://" url) (require 'elpher) (elpher-go url)) (t (funcall original url new-window)))) (advice-add 'eww-browse-url :around 'elpher:eww-browse-url) (unless (fboundp 'elpher-bookmarks) (autoload #'elpher-bookmarks "elpher" nil t)) (define-key acdw/map "e" #'elpher-bookmarks) (with-eval-after-load 'elpher (dolist (key '(("n" . elpher-next-link) ("p" . elpher-prev-link) ("o" . elpher-follow-current-link) ("G" . elpher-go-current))) (define-key elpher-mode-map (car key) (cdr key)))) (add-hook 'elpher-mode-hook #'visual-fill-column-mode) #+end_src *** Gemini mode #+begin_src emacs-lisp (straight-use-package '(gemini-mode :repo "https://git.carcosa.net/jmcbray/gemini.el.git")) (add-to-list 'auto-mode-alist '("\\.\\(gemini\\|gmi\\)\\'" . gemini-mode)) (defun acdw/setup-gemini-mode () (visual-fill-column-mode +1) (variable-pitch-mode -1)) (add-hook 'gemini-mode-hook #'acdw/setup-gemini-mode) #+end_src *** Gemini write #+begin_src emacs-lisp (straight-use-package '(gemini-write :repo "https://alexschroeder.ch/cgit/gemini-write")) #+end_src *** Ox-gemini #+begin_src emacs-lisp (straight-use-package '(ox-gemini :repo "https://git.sr.ht/~abrahms/ox-gemini" :branch "main")) #+end_src ** Pastebin #+begin_src emacs-lisp (straight-use-package '0x0) (cuss 0x0-default-service 'ttm) #+end_src ** RSS #+begin_src emacs-lisp (cuss newsticker-url-list ;; LABEL URL [START-TIME] [INERVAL] [WGET-ARGUMENTS] '(("wsinatra" "http://lambdacreate.com/static/feed.rss") ("elioat" "https://eli.li/feed.rss") ("ACDW" "https://www.acdw.net/atom.xml") ("june" "https://text.causal.agency/feed.atom") ("kylie - notes" "https://www.somas.is/notes.atom") ("kylie - rhizome" "https://www.somas.is/rhizome.atom") ("brennan" "https://p1k3.com/all.xml") ("Planet Emacs" "https://planet.emacslife.com/atom.xml") ("nullprogram, Chris Wellons" "https://nullprogram.com/feed/") ("Malleable Systems" "https://malleable.systems/blog/index.xml")) ) (add-hook 'newsticker-treeview-item-mode-hook #'visual-fill-column-mode) #+end_src ** Web browsing *** Open youtube links in mpv from [[https://karthinks.com/software/more-batteries-included-with-emacs/#regexp-builder--m-x-re-builder][karthinks]]. #+begin_src emacs-lisp (require 'browse-url) (when (executable-find "mpv") (defun browse-url-mpv (url &optional single) (start-process "mpv" nil (if single "mpv" "umpv") (shell-quote-wildcard-pattern url))) (defun browse-url-at-point-mpv (&optional single) "Open a link in mpv." (interactive "P") (let ((browse-url-browser-function (if single (lambda (url &optional _new-window) (browse-url-mpv url t)) #'browse-url-mpv))) (browse-url-at-point))) (cuss browse-url-browser-function '(("https?:\\/\\/www\\.youtu\\.*be." . browse-url-mpv) ("." . browse-url-generic)))) #+end_src ** Reading e-books #+begin_src emacs-lisp (straight-use-package 'nov) (add-to-list 'auto-mode-alist '("\\.epub\\'" . nov-mode)) (defun acdw/setup-nov-mode () (visual-line-mode +1) (visual-fill-column-mode +1) (variable-pitch-mode +1) (setq cursor-type nil)) (cuss nov-text-width t) (add-hook 'nov-mode-hook #'acdw/setup-nov-mode) #+end_src ** Eshell #+begin_src emacs-lisp (when (executable-find "bash") (straight-use-package 'bash-completion)) (when (executable-find "fish") (straight-use-package 'fish-completion) (require 'fish-completion) (cuss fish-completion-fallback-on-bash-p (executable-find "bash")) (global-fish-completion-mode +1) (straight-use-package 'fish-mode) (add-to-list 'auto-mode-alist '("\\.fish\\'" . fish-mode))) #+end_src * Appendices ** Emacs' files *** init.el :PROPERTIES: :header-args: :tangle init.el :END: #+begin_src emacs-lisp :comments no ;; init.el -*- lexical-binding: t -*- #+end_src **** Speed up init #+begin_src emacs-lisp (setq gc-cons-threshold most-positive-fixnum) (defvar old-file-name-handler file-name-handler-alist) (setq file-name-handler-alist nil) #+end_src **** Load config inspired by [[https://protesilaos.com/dotemacs/#h:584c3604-55a1-49d0-9c31-abe46cb1f028][Protesilaos Stavrou]]. #+begin_src emacs-lisp (let* ((conf (expand-file-name "config" user-emacs-directory)) (conf-el (concat conf ".el")) (conf-org (concat conf ".org"))) (unless (and (file-newer-than-file-p conf-el conf-org) (load conf 'no-error)) (require 'org) (org-babel-load-file conf-org))) #+end_src **** Reset for normal operation #+begin_src emacs-lisp (setq gc-cons-threshold 16777216 ; 16mb gc-cons-percentage 0.1 file-name-handler-alist old-file-name-handler) #+end_src *** early-init.el :PROPERTIES: :header-args: :tangle early-init.el :END: #+begin_src emacs-lisp :comments no ;; early-init.el -*- lexical-binding: t; no-byte-compile: t; -*- (setq load-prefer-newer t) (setq frame-inhibit-implied-resize t) #+end_src ** Ease tangling and loading of Emacs' init #+begin_src emacs-lisp (defun refresh-emacs (&optional disable-load) "Tangle `config.org', then byte-compile the resulting files. Then, load the byte-compilations unless passed with a prefix argument." (interactive "P") (let ((config (expand-file-name "config.org" user-emacs-directory))) (save-mark-and-excursion (with-current-buffer (find-file config) (let ((prog-mode-hook nil)) ;; generate the readme (when (file-newer-than-file-p config (expand-file-name "README.md" user-emacs-directory)) (require 'ox-md) (org-md-export-to-markdown)) ;; tangle config.org (when (file-newer-than-file-p config (expand-file-name "config.el" user-emacs-directory)) (require 'org) (let ((inits (org-babel-tangle))) ;; byte-compile resulting files (dolist (f inits) (when (string-match "\\.el\\'" f) (byte-compile-file f (not disable-load))))))))))) #+end_src *** Add a hook to tangle when quitting #+begin_src emacs-lisp (defun acdw/refresh-emacs-no-load () (refresh-emacs 'disable-load)) (add-hook 'kill-emacs-hook #'acdw/refresh-emacs-no-load) #+end_src ** Ancillary scripts *** emacsdc Here’s a wrapper script that’ll start =emacs –daemon= if there isn’t one, and then launch =emacsclient= with the arguments. I’d recommend installing with either ~ln -s bin/emacsdc $HOME/.local/bin/~, or adding =$HOME/.local/bin= to your =$PATH=. #+begin_src sh :tangle bin/emacsdc :mkdirp yes :shebang "#!/bin/sh" if ! emacsclient -nc "$@" 2>/dev/null; then emacs --daemon emacsclient -nc "$@" fi #+end_src ** License :PROPERTIES: :header-args: :tangle LICENSE :comments no :END: Copyright © 2020 Case Duckworth This work is free. You can redistribute it and/or modify it under the terms of the Do What the Fuck You Want To Public License, Version 2, as published by Sam Hocevar. See the =LICENSE= file, tangled from the following source block, for details. #+begin_src text DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 Copyright (C) 2004 Sam Hocevar Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. You just DO WHAT THE FUCK YOU WANT TO. #+end_src *** Note on the license It's highly likely that the WTFPL is completely incompatible with the GPL, for what should be fairly obvious reasons. To that, I say: *SUE ME, RMS!*