;;; init.el -*- lexical-binding: t; coding: utf-8 -*- ;; Copyright (C) 2020-2021 Case Duckworth ;; ;; Author: Case Duckworth ;; Created: Sometime during Covid-19, 2020 ;; Keywords: configuration ;; URL https://tildegit.org/acdw/emacs ;; ;; This file is NOT part of GNU Emacs. ;; ;;; License: ;; ;; Everyone is permitted to do whatever with this software, without ;; limitation. This software comes without any warranty whatsoever, ;; but with two pieces of advice: ;; - Don't hurt yourself. ;; - Make good choices. ;; ;;; Comentary: ;; ;;; Code: ;; User information (setq user-full-name "Case Duckworth" user-mail-address "acdw@acdw.net" calendar-location-name "Baton Rouge, LA" calendar-latitude 30.4 calendar-longitude -91.1 calendar-date-style 'iso custom-file (expand-file-name "custom.el" acdw/etc-dir)) ;; Load newer files first (setq-default load-prefer-newer t) ;; No littering (use-package no-littering :demand :init (setq no-littering-etc-directory acdw/etc-dir no-littering-var-directory acdw/var-dir)) (defun when-unfocused (func &rest args) "Run FUNC with ARGS iff all frames are out of focus." (when (seq-every-p #'null (mapcar #'frame-focus-state (frame-list))) (apply func args))) (define-minor-mode acdw/reading-mode "Make reading comfier." :lighter " Read" (if acdw/reading-mode (progn ;; turn on (text-scale-increase +1) (display-fill-column-indicator-mode -1) (dolist (func '(visual-fill-column-mode iscroll-mode)) (when (fboundp func) (funcall func +1)))) (progn ;; turn off (text-scale-increase 0) (display-fill-column-indicator-mode +1) (dolist (func '(visual-fill-column-mode iscroll-mode)) (when (fboundp func) (funcall func -1)))))) ;; Dialogs & alerts (setq-default use-dialog-box nil) ; Don't use a dialog box (fset 'yes-or-no-p #'y-or-n-p) (defun flash-mode-line () (ding) (invert-face 'mode-line) (run-with-timer 0.2 nil #'invert-face 'mode-line)) (setq-default visible-bell nil ; Don't use a visible bell ring-bell-function #'flash-mode-line) (defun hook--gc-when-unfocused () (when-unfocused #'garbage-collect)) (add-function :after after-focus-change-function #'hook--gc-when-unfocused) ;; Minibuffer (setq-default minibuffer-prompt-properties '(read-only t cursor-intangible t face minibuffer-prompt) enable-recursive-minibuffers t file-name-shadow-properties '(invisible t)) (file-name-shadow-mode +1) (minibuffer-depth-indicate-mode +1) (use-package savehist :straight nil :init (setq-default savehist-file (expand-file-name "history" acdw/var-dir) savehist-additional-variables '(kill-ring search-ring regexp-search-ring) history-length t history-delete-duplicates t savehist-autosave-interval 60) :config (savehist-mode +1)) ;; Backups (setq-default backup-by-copying t delete-old-versions -1 ; Don't delete old versions version-control t ; Make numeric backups vc-make-backup-files t ; Backup version-controlled files ) (let ((dir (expand-file-name "backup" acdw/var-dir))) (make-directory dir 'parents) (setq-default backup-directory-alist `((".*" . ,dir)))) ;; Lockfiles (setq-default create-lockfiles nil) ; Are these necessary? ;; Autosaves (setq auto-save-default nil ; Don't use `auto-save' system ) (use-package super-save :defer 5 ; This package can wait :init (setq-default super-save-remote-files nil ; Don't save remote files super-save-exclude '(".gpg") ; Wouldn't work anyway super-save-auto-save-when-idle t) :config (super-save-mode +1)) ;; Auto-revert (global-auto-revert-mode +1) ; Automatically revert a file ; to its on-disk contents (use-package saveplace :straight nil :init (setq-default save-place-file (expand-file-name "places" acdw/var-dir) save-place-forget-unreadable-files (eq acdw/system :home)) :config (save-place-mode +1)) (use-package recentf :straight nil :init (setq recentf-save-file (expand-file-name "recentf" acdw/var-dir) recentf-max-menu-items 100 recentf-max-saved-items nil recentf-auto-cleanup 'never) (defun maybe-save-recentf () "Save `recentf-file' every five minutes, but only when out of focus." (defvar recentf--last-save (time-convert nil 'integer) "When we last saved the `recentf-save-list'.") (when (> (time-convert (time-since recentf--last-save) 'integer) (* 60 5)) (setq-default recentf--last-save (time-convert nil 'integer)) (when-unfocused #'recentf-save-list))) :config (recentf-mode +1) (add-to-list 'recentf-exclude acdw/var-dir) (add-to-list 'recentf-exclude acdw/etc-dir) (add-function :after after-focus-change-function #'maybe-save-recentf)) ;; Uniquify (use-package uniquify :straight nil :init (setq-default uniquify-buffer-name-style 'forward ; bubble 'up' the directory tree uniquify-separator "/" ; separate path elements uniquify-after-kill-buffer-p t ; hook into buffer kills uniquify-ignore-buffers-re "^\\*" ; don't worry about special buffers )) ;; Scratch (setq-default inhibit-startup-screen t ; Don't show the splash screen initial-buffer-choice t ; Start on *scratch* initial-scratch-message (concat ";; Howdy, " (nth 0 (split-string user-full-name)) "!" " Welcome to GNU Emacs.\n\n")) (defun immortal-scratch () "Don't kill *scratch* when asked to by `kill-buffer'." (if (not (eq (current-buffer) (get-buffer "*scratch*"))) t (bury-buffer) nil)) (add-hook 'kill-buffer-query-functions #'immortal-scratch) ;; Easier buffer-killing (defun kill-a-buffer (&optional prefix) "Kill a buffer and its window, prompting only on unsaved changes. `kill-a-buffer' uses the PREFIX argument to determine which buffer(s) to kill: 0 => Kill THIS buffer & window 4 (C-u) => Kill OTHER buffer & window 16 (C-u C-u) => Run the default `kill-buffer'." (interactive "P") (pcase (or (car prefix) 0) (0 (kill-current-buffer) (unless (one-window-p) (delete-window))) (4 (other-window 1) (kill-current-buffer) (unless (one-window-p) (delete-window))) (16 (let ((current-prefix-arg nil)) (kill-buffer))))) (bind-key "C-x k" #'kill-a-buffer) ;; UTF-8 with LF line endings (set-charset-priority 'unicode) (set-language-environment "UTF-8") (prefer-coding-system 'utf-8-unix) (set-default-coding-systems 'utf-8-unix) (set-terminal-coding-system 'utf-8-unix) (set-keyboard-coding-system 'utf-8-unix) (set-selection-coding-system 'utf-8-unix) (setq-default locale-coding-system 'utf-8-unix coding-system-for-read 'utf-8-unix coding-system-for-write 'utf-8-unix buffer-file-coding-system 'utf-8-unix org-export-coding-system 'utf-8-unix org-html-coding-system 'utf-8-unix ; doesn't take from above default-process-coding-system '(utf-8-unix . utf-8-unix) x-select-request-type '(UTF8_STRING COMPOUND_TEXT TEXT STRING)) (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)))) (add-hook 'find-file-hook #'ewiki/no-junk-please-were-unixish) (add-hook 'before-save-hook #'ewiki/no-junk-please-were-unixish) ;; Cursor (setq-default cursor-type 'bar cursor-in-non-selected-windows nil) (blink-cursor-mode 0) ;; Filling text (setq-default fill-column 80) (global-display-fill-column-indicator-mode +1) (bind-key "C-x f" #'find-file) ; I don't set `fill-column', ever (setq-default comment-auto-fill-only-comments t) ;; Enable `auto-fill-mode' everywhere (add-hook 'text-mode-hook #'auto-fill-mode) (add-hook 'prog-mode-hook #'auto-fill-mode) ;; Also enable `visual-line-mode' everywhere (global-visual-line-mode +1) ;; "Fix" `visual-line-mode' in `org-mode' (defun hook--visual-line-fix-org-keys () (when (derived-mode-p 'org-mode) (local-set-key (kbd "C-a") #'org-beginning-of-line) (local-set-key (kbd "C-e") #'org-end-of-line) (local-set-key (kbd "C-k") #'org-kill-line))) (add-hook 'visual-line-mode-hook #'hook--visual-line-fix-org-keys) (dolist (margin '(right-margin left-margin)) (dolist (button '(mouse-1 mouse-2 mouse-3)) (global-set-key (vector margin button) (global-key-binding (vector button))))) (mouse-wheel-mode +1) (when (bound-and-true-p mouse-wheel-mode) (dolist (margin '(right-margin left-margin)) (dolist (event '(mouse-wheel-down-event mouse-wheel-up-event wheel-down wheel-up mouse-4 mouse-5)) (global-set-key (vector margin event) #'mwheel-scroll)))) (use-package visual-fill-column :init (setq-default visual-fill-column-center-text t) (add-hook 'visual-fill-column-mode-hook #'visual-line-mode) :config (advice-add 'text-scale-adjust :after #'visual-fill-column-adjust)) (when (fboundp 'global-so-long-mode) (global-so-long-mode +1)) ;; Whitespace (setq-default whitespace-style '(empty ; remove blank lines at buffer edges indentation ; clean up indentation ;; mixed tabs & spaces space-before-tab space-after-tab)) (add-hook 'before-save-hook #'whitespace-cleanup) (setq-default indent-tabs-mode t tab-width 8) (use-package smart-tabs-mode :config (smart-tabs-insinuate 'c 'c++ 'java 'javascript 'cperl 'python 'ruby 'nxml)) ;; Window layouts (setq-default split-width-threshold 100 ; minimum width for window splits split-height-threshold 50 ; minimum height for window splits display-buffer-alist ; how to display buffers '((".*" . (display-buffer-reuse-window display-buffer-same-window))) display-buffer-reuse-frames t ; allow reuse of frames even-window-sizes nil ; avoid resizing windows to even them help-window-select t ; select *Help* window when opened ) (defun vsplit-other-window () "Split the window vertically and switch to the new window." (interactive) (split-window-vertically) (other-window 1 nil)) (defun hsplit-other-window () "Split the window horizontally and switch to the new window." (interactive) (split-window-horizontally) (other-window 1 nil)) (bind-key "C-x 2" #'vsplit-other-window) (bind-key "C-x 3" #'hsplit-other-window) ;; Theming (use-package form-feed :config (global-form-feed-mode +1)) (use-package modus-themes :straight (:host gitlab :repo "protesilaos/modus-themes") :demand :init (setq-default modus-themes-slanted-constructs t modus-themes-bold-constructs t modus-themes-region 'bg-only modus-themes-org-blocks 'grayscale modus-themes-headings '((1 . section) (t . no-color)) modus-themes-scale-headings nil modus-themes-mode-line nil) :custom-face (modus-theme-heading-1 ((t (:inherit (modus-theme-heading-1 fixed-pitch bold))))) (modus-theme-heading-2 ((t (:inherit (modus-theme-heading-2 fixed-pitch bold))))) (modus-theme-heading-3 ((t (:inherit (modus-theme-heading-3 fixed-pitch bold))))) (modus-theme-heading-4 ((t (:inherit (modus-theme-heading-4 fixed-pitch bold))))) (modus-theme-heading-5 ((t (:inherit (modus-theme-heading-5 fixed-pitch bold))))) (modus-theme-heading-6 ((t (:inherit (modus-theme-heading-6 fixed-pitch bold))))) (modus-theme-heading-7 ((t (:inherit (modus-theme-heading-7 fixed-pitch bold))))) (modus-theme-heading-8 ((t (:inherit (modus-theme-heading-8 fixed-pitch bold)))))) ;; Change themes based on time of day (defun acdw/run-with-sun (sunrise-command sunset-command) "Run commands at sunrise and sunset." (let* ((times-regex (rx (* nonl) (: (any ?s ?S) "unrise") " " (group (repeat 1 2 digit) ":" (repeat 1 2 digit) (: (any ?a ?A ?p ?P) (any ?m ?M))) (* nonl) (: (any ?s ?S) "unset") " " (group (repeat 1 2 digit) ":" (repeat 1 2 digit) (: (any ?a ?A ?p ?P) (any ?m ?M))) (* nonl))) (ss (sunrise-sunset)) (_m (string-match times-regex ss)) (sunrise-time (match-string 1 ss)) (sunset-time (match-string 2 ss))) (run-at-time sunrise-time (* 60 60 24) sunrise-command) (run-at-time sunset-time (* 60 60 24) sunset-command) (run-at-time "0:00" (* 60 60 24) sunset-command))) (acdw/run-with-sun #'modus-themes-load-operandi #'modus-themes-load-vivendi) (use-package minions :config (minions-mode +1)) (use-package which-key :config (which-key-mode +1)) (delete-selection-mode +1) (setq-default save-interprogram-paste-before-kill t ; save existing text before replacing yank-pop-change-selection t ; update X selection when rotating ring x-select-enable-clipboard t ; Enable X clipboards x-select-enable-primary t mouse-drag-copy-region t ; Copy a region when mouse-selected kill-do-not-save-duplicates t ; Don't append the same thing twice ) (use-package smartscan :config (global-smartscan-mode +1)) (when (fboundp 'global-goto-address-mode) (global-goto-address-mode +1)) (use-package flyspell :init (setenv "LANG" "en_US") (setq-default ispell-program-name "hunspell" ispell-dictionary "en_US" ispell-personal-dictionary "~/.hunspell_personal") :hook (text-mode . flyspell-mode) (prog-mode . flyspell-prog-mode) :config (ispell-set-spellchecker-params) (unless (file-exists-p ispell-personal-dictionary) (write-region "" nil ispell-personal-dictionary nil 0))) (use-package flyspell-correct :bind ("C-;" . flyspell-correct-wrapper)) (setq-default show-paren-delay 0 show-paren-style 'mixed show-paren-when-point-inside-paren t show-paren-when-point-in-periphery t) (show-paren-mode +1) (add-hook 'prog-mode-hook #'electric-pair-local-mode) (setq-default prettify-symbols-unprettify-at-point 'right-edge) (add-hook 'prog-mode-hook #'prettify-symbols-mode) (add-hook 'after-save-hook #'executable-make-buffer-file-executable-if-script-p) (setq-default compilation-ask-about-save nil ; just save the buffer compilation-always-kill t ; kill the processes without asking compilation-scroll-output 'first-error) (use-package reformatter :demand) ;; Shell scripts (setq-default sh-basic-offset 8 smie-indent-basic 8) (use-package flymake-shellcheck :when (executable-find "shellcheck") :hook sh-mode) (when (executable-find "shfmt") (reformatter-define sh-format :program "shfmt" :lighter "Shfmt") (add-hook 'sh-mode-hook #'sh-format-on-save-mode)) (bind-key "M-/" #'hippie-expand) ;; Tabs (setq-default tab-bar-show 1 ; show the tab bar when more than one tab-bar-new-tab-choice "*scratch*" ; what to show on a new tab tab-bar-tab-name-function ; how to name a new tab #'tab-bar-tab-name-current-with-count tab-bar-history-limit 25 ; how many tabs to save in history ) (tab-bar-history-mode +1) ;; Smart hungry delete (use-package smart-hungry-delete :defer nil :bind (("" . smart-hungry-delete-backward-char) ("C-d" . smart-hungry-delete-forward-char)) :config (smart-hungry-delete-add-default-hooks)) ;; Enable all commands (setq-default disabled-command-function nil) ;; Magit (use-package magit :bind ("C-x g" . magit-status) :init (when (eq acdw/system :work) (setenv "GIT_ASKPASS" "git-gui--askpass"))) (use-package forge :after magit) (use-package gitattributes-mode :mode "\\.gitattributes\\'") (use-package gitconfig-mode :mode "\\.gitconfig\\'") (use-package gitignore-mode :mode "\\.gitignore\\'") ;; crux (use-package crux :straight (:host github :repo "bbatsov/crux") :bind ("M-o" . crux-other-window-or-switch-buffer) :config (crux-with-region-or-line kill-ring-save) (crux-with-region-or-line kill-region) (crux-with-region-or-line comment-or-uncomment-region)) ;; Completion and... stuff (setq-default completion-ignore-case t read-buffer-completion-ignore-case t read-file-name-completion-ignore-case t minibuffer-eldef-shorten-default t) (minibuffer-electric-default-mode +1) (use-package icomplete-vertical :demand :init (setq-default icomplete-delay-completions-threshold 0 icomplete-max-delay-chars 0 icomplete-compute-delay 0 icomplete-show-matches-on-no-input t icomplete-hide-common-prefix nil icomplete-with-completion-tables t icomplete-in-buffer t) :bind (:map icomplete-minibuffer-map ("" . icomplete-forward-completions) ("C-n" . icomplete-forward-completions) ("" . icomplete-backward-completions) ("C-p" . icomplete-backward-completions) ("C-v" . icomplete-vertical-toggle)) :config (fido-mode -1) (icomplete-mode +1) (icomplete-vertical-mode +1)) (use-package orderless :after icomplete :init (setq-default completion-styles '(orderless))) (use-package marginalia :after icomplete :init (setq-default marginalia-annotators '(marginalia-annotators-heavy marginalia-annotators-light)) :config (marginalia-mode +1)) (use-package consult :after icomplete :bind (;; C-c bindings (mode-specific-map) ("C-c h" . consult-history) ("C-c m" . consult-mode-command) ("C-c b" . consult-bookmark) ("C-c k" . consult-kmacro) ;; C-x bindings (ctl-x-map) ("C-x M-:" . consult-complex-command) ("C-x b" . consult-buffer) ("C-x 4 b" . consult-buffer-other-window) ("C-x 5 b" . consult-buffer-other-frame) ;; Custom M-# bindings for fast register access ("M-#" . consult-register-load) ("M-'" . consult-register-store) ("C-M-#" . consult-register) ;; Other custom bindings ("M-y" . consult-yank-pop) (" a" . consult-apropos) ;; M-g bindings (goto-map) ("M-g e" . consult-compile-error) ("M-g g" . consult-goto-line) ("M-g M-g" . consult-goto-line) ("M-g o" . consult-outline) ("M-g m" . consult-mark) ("M-g k" . consult-global-mark) ("M-g i" . consult-imenu) ("M-g I" . consult-project-imenu) ;; M-s bindings (search-map) ("M-s f" . consult-find) ("M-s L" . consult-locate) ("M-s g" . consult-grep) ("M-s G" . consult-git-grep) ("M-s r" . consult-ripgrep) ("M-s l" . consult-line) ("M-s m" . consult-multi-occur) ("M-s k" . consult-keep-lines) ("M-s u" . consult-focus-lines) ;; Isearch integration ("M-s e" . consult-isearch) :map isearch-mode-map ("M-e" . consult-isearch) ("M-s e" . consult-isearch) ("M-s l" . consult-line)) :init (setq register-preview-delay 0 register-preview-function #'consult-register-format) (advice-add #'register-preview :override #'consult-register-window) (setq xref-show-xrefs-function #'consult-xref xref-show-definitions-function #'consult-xref) :config ;; (setq consult-preview-key 'any) ;; (setq consult-preview-key (kbd "M-p")) (setq consult-narrow-key "<")) ;; Language: Emacs-Lisp (setq-default eval-expression-print-length nil ; don't limit print length eval-expression-print-level nil ) ;; indent like common lisp (require 'cl-lib) (setq-default lisp-indent-function #'common-lisp-indent-function) (put 'cl-flet 'common-lisp-indent-function (get 'flet 'common-lisp-indent-function)) (put 'cl-labels 'common-lisp-indent-function (get 'labels 'common-lisp-indent-function)) (put 'if 'common-lisp-indent-function 2) (put 'dotimes-protect 'common-lisp-indent-function (get 'when 'common-lisp-indent-function))