emacs/lisp/acdw.el

546 lines
20 KiB
EmacsLisp
Raw Normal View History

2021-03-29 22:58:01 +00:00
;;; acdw.el -*- lexical-binding: t; coding: utf-8-unix -*-
2021-05-25 17:28:32 +00:00
2021-03-08 04:14:38 +00:00
;; Author: Case Duckworth <acdw@acdw.net>
;; Created: Sometime during Covid-19, 2020
;; Keywords: configuration
;; URL: https://tildegit.org/acdw/emacs
2021-03-16 16:16:21 +00:00
2021-03-08 04:14:38 +00:00
;; This file is NOT part of GNU Emacs.
2021-03-16 16:16:21 +00:00
2021-03-08 04:14:38 +00:00
;;; 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.
2021-03-16 16:16:21 +00:00
2021-03-08 04:14:38 +00:00
;;; Commentary:
;; `acdw.el' contains `acdw/map', its mode, and assorted ease-of-life
;; functions for me, acdw.
2021-03-16 16:16:21 +00:00
2021-03-08 04:14:38 +00:00
;;; Code:
2021-04-06 22:59:45 +00:00
;;; Variables
2021-03-08 04:14:38 +00:00
(defconst acdw/system
(pcase system-type
('gnu/linux :home)
((or 'msdos 'windows-nt) :work)
(_ :other))
2021-04-06 22:59:45 +00:00
"Which computer system is currently being used.")
(defmacro acdw/system (&rest args)
"Convenience macro for interfacing with `acdw/system'.
When called without arguments, it returns `acdw/system'. When
called with one (symbol) argument, it returns (eq acdw/system
ARG). When called with multiple arguments or a list, it returns
`pcase' over each argument."
(cond
((null args) acdw/system)
((atom (car args))
`(when (eq acdw/system ,(car args))
,(car args)))
(t
`(pcase acdw/system
,@args))))
2021-04-06 22:59:45 +00:00
;;; Utility functions
;; I don't prefix these because ... reasons. Honestly I probably should prefix
;; them.
2021-04-06 22:59:45 +00:00
(defun dos2unix (buffer)
"Replace \r\n with \n in BUFFER."
(interactive "*b")
(save-excursion
(goto-char (point-min))
(while (search-forward (string ?\C-m ?\C-j) nil t)
(replace-match (string ?\C-j) nil t))))
(defun expand-file-name-exists-p (&rest expand-file-name-args)
"Call `expand-file-name' on EXPAND-FILE-NAME-ARGS, returning
its name if it exists, or NIL otherwise."
(let ((file (apply #'expand-file-name expand-file-name-args)))
(if (file-exists-p file)
file
nil)))
2021-04-06 22:59:45 +00:00
;; (defmacro hook-defun (name hooks &rest forms)
;; "Define a function NAME that executes FORMS, and add it to
;; each hook in HOOKS."
;; (declare (indent 2))
;; (let ((func-name (intern (concat "hook-defun-" (symbol-name name))))
;; (hook-list (if (consp hooks) hooks (list hooks)))
;; (hook-defun-add-hook-list))
;; `(progn
;; (defun ,func-name () "Defined by `hook-defun'." ,@forms)
;; ,@(dolist (hook hook-list hook-defun-add-hook-list)
;; (push `(add-hook ',hook #',func-name) hook-defun-add-hook-list)))))
2021-04-06 22:59:45 +00:00
(defun kill-region-or-backward-word (arg)
"Kill region if active, or backward word if not."
(interactive "p")
(if (region-active-p)
(kill-region (region-beginning) (region-end))
(backward-kill-word arg)))
(defmacro when-unfocused (name &rest forms)
"Define a function NAME, executing FORMS, that fires when Emacs
is unfocused."
(declare (indent 1))
(let ((func-name (intern (concat "when-unfocused-" (symbol-name name)))))
`(progn
(defun ,func-name () "Defined by `when-unfocused'."
(when (seq-every-p #'null
(mapcar #'frame-focus-state (frame-list)))
,@forms))
(add-function :after after-focus-change-function #',func-name))))
(defmacro with-message (message &rest body)
"Execute BODY, messaging 'MESSAGE...' before and 'MESSAGE... Done.' after."
(declare (indent 1))
;; Wrap a progn inside a prog1 to return the return value of the body.
`(prog1
(progn (message "%s..." ,message)
,@body)
(message "%s... Done." ,message)))
;;; Comment-or-uncomment-sexp
;; from https://endlessparentheses.com/a-comment-or-uncomment-sexp-command.html
(defun uncomment-sexp (&optional n)
"Uncomment a sexp around point."
(interactive "P")
(let* ((initial-point (point-marker))
(inhibit-field-text-motion t)
(p)
(end (save-excursion
(when (elt (syntax-ppss) 4)
(re-search-backward comment-start-skip
(line-beginning-position)
t))
(setq p (point-marker))
(comment-forward (point-max))
(point-marker)))
(beg (save-excursion
(forward-line 0)
(while (and (not (bobp))
(= end (save-excursion
(comment-forward (point-max))
(point))))
(forward-line -1))
(goto-char (line-end-position))
(re-search-backward comment-start-skip
(line-beginning-position)
t)
(ignore-errors
(while (looking-at-p comment-start-skip)
(forward-char -1)))
(point-marker))))
(unless (= beg end)
(uncomment-region beg end)
(goto-char p)
;; Indentify the "top-level" sexp inside the comment.
(while (and (ignore-errors (backward-up-list) t)
(>= (point) beg))
(skip-chars-backward (rx (syntax expression-prefix)))
(setq p (point-marker)))
;; Re-comment everything before it.
(ignore-errors
(comment-region beg p))
;; And everything after it.
(goto-char p)
(forward-sexp (or n 1))
(skip-chars-forward "\r\n[:blank:]")
(if (< (point) end)
(ignore-errors
(comment-region (point) end))
;; If this is a closing delimiter, pull it up.
(goto-char end)
(skip-chars-forward "\r\n[:blank:]")
(when (eq 5 (car (syntax-after (point))))
(delete-indentation))))
;; Without a prefix, it's more useful to leave point where
;; it was.
(unless n
(goto-char initial-point))))
(defun comment-sexp--raw ()
"Comment the sexp at point or ahead of point."
(pcase (or (bounds-of-thing-at-point 'sexp)
(save-excursion
(skip-chars-forward "\r\n[:blank:]")
(bounds-of-thing-at-point 'sexp)))
(`(,l . ,r)
(goto-char r)
(skip-chars-forward "\r\n[:blank:]")
(save-excursion
(comment-region l r))
(skip-chars-forward "\r\n[:blank:]"))))
(defun comment-or-uncomment-sexp (&optional n)
"Comment the sexp at point and move past it.
If already inside (or before) a comment, uncomment instead.
With a prefix argument N, (un)comment that many sexps."
(interactive "P")
(if (or (elt (syntax-ppss) 4)
(< (save-excursion
(skip-chars-forward "\r\n[:blank:]")
(point))
(save-excursion
(comment-forward 1)
(point))))
(uncomment-sexp n)
(dotimes (_ (or n 1))
(comment-sexp--raw))))
2021-05-22 21:40:18 +00:00
;;; Sort sexps
;; from https://github.com/alphapapa/unpackaged.el#sort-sexps
(defun sort-sexps (beg end)
"Sort sexps in region.
Comments stay with the code below."
(interactive "r")
(cl-flet ((skip-whitespace () (while (looking-at (rx (1+ (or space "\n"))))
(goto-char (match-end 0))))
(skip-both () (while (cond ((or (nth 4 (syntax-ppss))
(ignore-errors
(save-excursion
(forward-char 1)
(nth 4 (syntax-ppss)))))
(forward-line 1))
((looking-at (rx (1+ (or space "\n"))))
(goto-char (match-end 0)))))))
(save-excursion
(save-restriction
(narrow-to-region beg end)
(goto-char beg)
(skip-both)
(cl-destructuring-bind (sexps markers)
(cl-loop do (skip-whitespace)
for start = (point-marker)
for sexp = (ignore-errors
(read (current-buffer)))
for end = (point-marker)
while sexp
;; Collect the real string, then one used for sorting.
collect (cons (buffer-substring (marker-position start) (marker-position end))
(save-excursion
(goto-char (marker-position start))
(skip-both)
(buffer-substring (point) (marker-position end))))
into sexps
collect (cons start end)
into markers
finally return (list sexps markers))
(setq sexps (sort sexps (lambda (a b)
(string< (cdr a) (cdr b)))))
(cl-loop for (real . sort) in sexps
for (start . end) in markers
do (progn
(goto-char (marker-position start))
(insert-before-markers real)
(delete-region (point) (marker-position end)))))))))
;;; Emacs configuration functions
(defun emacs-git-pull-config (&optional remote branch)
"`git-pull' emacs configuration from REMOTE and BRANCH.
REMOTE defaults to 'origin', BRANCH to 'main'."
(let ((remote (or remote "origin"))
(branch (or branch "main")))
(with-message (format "Pulling Emacs's configuration from %s" branch)
(shell-command (concat "git -C "
"\"" (expand-file-name user-emacs-directory) "\""
" pull " remote " " branch)
(get-buffer-create "*emacs-git-pull-config-output*")
(get-buffer-create "*emacs-git-pull-config-error*")))))
(defun emacs-reload (&optional git-pull-first)
"Reload Emacs's configuration files.
With a prefix argument, run git pull on the repo first."
(interactive "P")
(when git-pull-first
(emacs-git-pull-config))
(let ((init-files (append
;; Load lisp libraries first, in case their functionality
;; is used by {early-,}init.el
(let* ((dir (expand-file-name "lisp/"
user-emacs-directory))
(full-name (lambda (f)
(concat
(file-name-as-directory dir) f))))
(mapcar full-name (directory-files dir nil "\\.el\\'")))
;; Load regular init files
(list (locate-user-emacs-file "early-init.el")
(locate-user-emacs-file "init.el" ".emacs"))))
(debug-on-error t))
(with-message "Saving init files"
(save-some-buffers :no-confirm (lambda () (member (buffer-file-name)
init-files))))
(dolist (file init-files)
(with-message (format "Loading %s" file)
(when (file-exists-p file)
(load-file file))))))
2021-04-06 22:59:45 +00:00
;;; Specialized functions
2021-04-06 22:59:45 +00:00
(defun acdw/dir (&optional file make-directory)
"Place Emacs files in one place.
If called without parameters, `acdw/dir' expands to
~/.emacs.d/var or similar. If called with FILE, `acdw/dir'
expands FILE to ~/.emacs.d/var, optionally making its directory
if MAKE-DIRECTORY is non-nil."
(let ((dir (expand-file-name (convert-standard-filename "var/")
user-emacs-directory)))
(if file
(let ((file-name (expand-file-name (convert-standard-filename file)
2021-04-07 05:14:08 +00:00
dir)))
2021-04-06 22:59:45 +00:00
(when make-directory
(make-directory (file-name-directory file-name) 'parents))
file-name)
dir)))
2021-04-02 18:07:24 +00:00
2021-04-07 17:56:14 +00:00
(defun acdw/find-emacs-dotfiles ()
"Finds lisp files in `user-emacs-directory' and passes them to
`completing-read'."
(interactive)
(find-file (completing-read ".emacs: "
(directory-files-recursively
user-emacs-directory "\.el$"))))
(defun acdw/find-emacs-source ()
"Find where Emacs keeps its source tree."
(acdw/system
(:work (expand-file-name
(concat "~/src/emacs-" emacs-version "/src")))
(:home (expand-file-name "~/src/pkg/emacs/src/emacs-git/src"))
(:other nil)))
(defun acdw/gc-disable ()
"Functionally disable the Garbage collector."
(setq gc-cons-threshold most-positive-fixnum
gc-cons-percentage 0.8))
(defun acdw/gc-enable ()
"Enable the Garbage collector."
(setq gc-cons-threshold (* 800 1024 1024)
gc-cons-percentage 0.1))
(defun acdw/insert-iso-date (with-time)
"Insert the ISO-8601-formatted date, with optional time."
(interactive "P")
(let ((format (if with-time "%FT%T%z" "%F")))
(insert (format-time-string format (current-time)))))
(defun acdw/kill-a-buffer (&optional prefix)
"Kill a buffer based on the following rules:
C-x k => Kill CURRENT buffer and window
C-u C-x k => Kill OTHER buffer and window
C-u C-u C-x k => Kill ALL OTHER buffers and windows
Prompt only if there are unsaved changes."
(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)))
2021-04-21 14:38:08 +00:00
(16 (mapc 'kill-buffer (delq (current-buffer) (buffer-list)))
(delete-other-windows))))
(defun acdw/sunrise-sunset (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 "12:00am" (* 60 60 24) sunset-command)))
(defun acdw/setup-fringes ()
"Set up fringes how I likes 'em."
(define-fringe-bitmap 'left-curly-arrow
[#b01100000
#b00110000
#b00011000
#b00001100]
4 8 'center)
(define-fringe-bitmap 'right-curly-arrow
[#b00000011
#b00000110
#b00001100
#b00011000]
4 8 'center)
(define-fringe-bitmap 'left-arrow
[#b01100000
#b01010000]
2 8 '(top t))
(define-fringe-bitmap 'right-arrow
[#b00000011
#b00000101]
2 8 '(top t))
(setq-local indicate-empty-lines nil
indicate-buffer-boundaries '((top . right)
(bottom . right)))
(custom-set-faces '(fringe
((t (:foreground "dim gray"))))))
;;; URL regexp
;; really, I just want to add gemini:// protocol, but I'm going to do some
;; reverse-engineering here.
(defvar acdw/button-protocols '("http"
"https"
"shttp"
"shttps"
"ftp"
"file"
"gopher"
"nntp"
"news"
"telnet"
"wais"
"mailto"
"info")
"The list of protocols to splice into `browse-url-button-regexp'.")
(defun acdw/build-button-url-regexp ()
"Build `browse-url-button-regexp' from `acdw/button-protocols'.
I used `xr' (not included in Emacs) to get the RX form of the
default, so I can easily splice the list into it. THIS IS
BRITTLE AF!!!"
(rx-to-string ; thanks wgreenhouse!
`(seq word-boundary
(group
(group
(or "www."
(seq
(group (or ,@acdw/button-protocols))
":")))
(opt
(group "//"
(one-or-more
(any "0-9a-z" "._-"))
":"
(zero-or-more
(any "0-9"))))
(or
(seq
(one-or-more
(any "0-9a-z" "!#$%&*+,./:;=?@\\_~-" word))
"("
(one-or-more
(any "0-9a-z" "!#$%&*+,./:;=?@\\_~-" word))
(zero-or-more
(any "0-9a-z" "#$%&*+/=@\\_~-" word))
")"
(opt
(one-or-more
(any "0-9a-z" "!#$%&*+,./:;=?@\\_~-" word))
(any "0-9a-z" "#$%&*+/=@\\_~-" word)))
(seq
(one-or-more
(any "0-9a-z" "!#$%&*+,./:;=?@\\_~-" word))
(any "0-9a-z" "#$%&*+/=@\\_~-" word)))))))
(defun acdw/add-button-url-regexp-protocol (proto)
"Add PROTO to `browse-url-button-regexp'
First, add PROTO to `acdw/button-protocols'.
Then, build `browse-url-button-regexp' with the new protocol."
(add-to-list 'acdw/button-protocols proto)
(setq-default browse-url-button-regexp (acdw/build-button-url-regexp)))
;;; Recentf renaming with dired
;; from ... somewhere. 'rjs', apparently?
;; I'm throwing these here because they look better here than in init.el.
;; Comments are "rjs"'s.
;; Magic advice to rename entries in recentf when moving files in
;; dired.
(defun rjs/recentf-rename-notify (oldname newname &rest args)
(if (file-directory-p newname)
(rjs/recentf-rename-directory oldname newname)
(rjs/recentf-rename-file oldname newname)))
(defun rjs/recentf-rename-file (oldname newname)
(setq recentf-list
(mapcar (lambda (name)
(if (string-equal name oldname)
newname
oldname))
recentf-list)))
(defun rjs/recentf-rename-directory (oldname newname)
;; oldname, newname and all entries of recentf-list should already
;; be absolute and normalised so I think this can just test whether
;; oldname is a prefix of the element.
(setq recentf-list
(mapcar (lambda (name)
(if (string-prefix-p oldname name)
(concat newname (substring name (length oldname)))
name))
recentf-list)))
2021-04-07 05:14:08 +00:00
;;; Minor modes
2021-04-07 05:14:08 +00:00
(define-minor-mode acdw/reading-mode
"A mode for reading."
:init-value nil
:lighter " Read"
(if acdw/reading-mode
(progn ;; turn on
;; settings
(setq-local orig-indicate-empty-lines indicate-empty-lines
2021-04-07 05:14:08 +00:00
indicate-empty-lines nil
orig-indicate-buffer-boundaries indicate-buffer-boundaries
indicate-buffer-boundaries nil)
;; disable modes
(dolist (mode '(display-fill-column-indicator-mode))
(when (fboundp mode)
(funcall mode -1)))
;; enable modes
(dolist (mode '(iscroll-mode olivetti-mode))
(when (fboundp mode)
(funcall mode +1))))
;; turn off
;; settings
(setq-local indicate-empty-lines orig-indicate-empty-lines
indicate-buffer-boundaries orig-indicate-buffer-boundaries)
;; enable modes
(dolist (mode '(display-fill-column-indicator-mode))
(when (fboundp mode)
(funcall mode +1)))
;; disable modes
(dolist (mode '(olivetti-mode iscroll-mode))
(when (fboundp mode)
(funcall mode -1)))))
2021-03-08 04:14:38 +00:00
(provide 'acdw)
;;; acdw.el ends here