emacs/init.el

574 lines
17 KiB
EmacsLisp

;;; init.el -*- lexical-binding: t; coding: utf-8 -*-
;; Copyright (C) 2020-2021 Case Duckworth
;;
;; Author: Case Duckworth <acdw@acdw.net>
;; 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)
;; Load newer files first
(setq-default load-prefer-newer t)
;; Make C-z more useful
(defvar acdw/leader
(let ((map (make-sparse-keymap))
(c-z (global-key-binding "\C-z")))
(global-unset-key "\C-z")
(global-set-key "\C-z" map)
(define-key map "\C-z" c-z)
map)
"A leader key for apps and stuff.")
(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)))
;; 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
(use-package super-save
:defer 5 ; This package can wait
:init
(setq-default
auto-save-default nil ; Don't use `auto-save' system
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)
(use-package visual-fill-column
:init (setq-default visual-fill-column-center-text t)
:hook visual-fill-column-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))
(which-function-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 (("<backspace>" . 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-z g" . magit-status))
;; 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)
(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
("<down>" . icomplete-forward-completions)
("C-n" . icomplete-forward-completions)
("<up>" . 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) ; orig. repeat-complet-command
("C-x b" . consult-buffer) ; orig. switch-to-buffer
("C-x 4 b" . consult-buffer-other-window) ; orig. switch-to-buffer-other-window
("C-x 5 b" . consult-buffer-other-frame) ; orig. switch-to-buffer-other-frame
;; Custom M-# bindings for fast register access
("M-#" . consult-register-load)
("M-'" . consult-register-store) ; orig. abbrev-prefix-mark (unrelated)
("C-M-#" . consult-register)
;; Other custom bindings
("M-y" . consult-yank-pop) ; orig. yank-pop
("<help> a" . consult-apropos) ; orig. apropos-command
;; M-g bindings (goto-map)
("M-g e" . consult-compile-error)
("M-g g" . consult-goto-line) ; orig. goto-line
("M-g M-g" . consult-goto-line) ; orig. 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) ; orig. isearch-edit-string
("M-s e" . consult-isearch) ; orig. isearch-edit-string
("M-s l" . consult-line)) ; required by consult-line to detect isearch
: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 "<"))