diff options
Diffstat (limited to 'lisp/yoke.el')
-rw-r--r-- | lisp/yoke.el | 392 |
1 files changed, 0 insertions, 392 deletions
diff --git a/lisp/yoke.el b/lisp/yoke.el deleted file mode 100644 index ec84f56..0000000 --- a/lisp/yoke.el +++ /dev/null | |||
@@ -1,392 +0,0 @@ | |||
1 | ;;; yoke.el --- Yoke configuration into your config -*- lexical-binding: t; -*- | ||
2 | |||
3 | ;; Copyright (C) 2022 Case Duckworth | ||
4 | |||
5 | ;; Author: Case Duckworth <case@bob> | ||
6 | ;; Keywords: convenience | ||
7 | ;; Package-Version: 0.61803398875 | ||
8 | ;; Homepage: https://junk.acdw.net/yoke.el | ||
9 | ;; Package-Requires: ((emacs "28.1")) | ||
10 | |||
11 | ;; This program is free software; you can redistribute it and/or modify | ||
12 | ;; it under the terms of the GNU General Public License as published by | ||
13 | ;; the Free Software Foundation, either version 3 of the License, or | ||
14 | ;; (at your option) any later version. | ||
15 | |||
16 | ;; This program is distributed in the hope that it will be useful, | ||
17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
19 | ;; GNU General Public License for more details. | ||
20 | |||
21 | ;; You should have received a copy of the GNU General Public License | ||
22 | ;; along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
23 | |||
24 | ;;; Commentary: | ||
25 | |||
26 | ;; THIS IS A WORK IN PROGRESS. DON'T USE IT. | ||
27 | |||
28 | ;;; Code: | ||
29 | |||
30 | (require 'cl-lib) | ||
31 | (require 'package-vc) | ||
32 | |||
33 | ;;; User options | ||
34 | |||
35 | (defgroup yoke nil | ||
36 | "Customizations for `yoke'." | ||
37 | :group 'convenience | ||
38 | :prefix "yoke-") | ||
39 | |||
40 | (defcustom yoke-directory package-user-dir | ||
41 | "Where to put yoked packages." | ||
42 | :type 'file) | ||
43 | |||
44 | (defcustom yoke-cache-directory (locate-user-emacs-file "yoke-cache" | ||
45 | "~/.yoke-cache") | ||
46 | "Where to put cached yoke files, like downloaded HTTP packages." | ||
47 | :type 'file) | ||
48 | |||
49 | (defcustom yoke-debug-on-error nil | ||
50 | "Whether to throw up the debugger on a yoke error. | ||
51 | If nil, errors will be inserted in the `yoke-message-buffer'.") | ||
52 | |||
53 | ;;; Variables | ||
54 | |||
55 | (defvar yoke-message-buffer " *yoke*" | ||
56 | "The buffer used for yoke messages.") | ||
57 | |||
58 | (defvar yoke-selected-packages nil | ||
59 | "List of packages managed by `yoke'.") | ||
60 | |||
61 | (defvar yoke-backends '(file http package) | ||
62 | "Backends handled by `yoke'.") | ||
63 | |||
64 | ;;; Main functionality | ||
65 | |||
66 | (defmacro yoke (package &rest body) | ||
67 | "Yoke a package into your Emacs session. | ||
68 | PACKAGE is either a symbol, in which case `yoke' expands to | ||
69 | basically a named `progn' (good for grouping configuration), or a | ||
70 | list of the form (NAME . ARGS), where ARGS can be one of the | ||
71 | following: | ||
72 | |||
73 | - nil: install NAME using `package'. | ||
74 | - a property list describing a package specification. Valid | ||
75 | key/value pairs include | ||
76 | |||
77 | `:backend' (symbol) | ||
78 | A symbol of the yoke backend to use for installing the | ||
79 | package. See `yoke-backends' for allowed backends. | ||
80 | |||
81 | `:url' (string) | ||
82 | The URL of the package's repository or source. | ||
83 | |||
84 | `:lisp-dir' (string) | ||
85 | The repository-relative name of the directory to use for | ||
86 | loading lisp sources. If not given, it defaults to the | ||
87 | repo's root directory. | ||
88 | |||
89 | Other pairs may be valid for a given backend; see that | ||
90 | backend's `yoke-install' function for more details. | ||
91 | |||
92 | BODY is executed in a `condition-case' so that errors won't keep | ||
93 | the rest of Emacs from initializing. BODY can also be prepended | ||
94 | by the following keyword arguments: | ||
95 | |||
96 | `:after' (FEATURE...) | ||
97 | |||
98 | `:require' (FEATURE...) | ||
99 | |||
100 | `:depends' (PACKAGE-SPEC...) | ||
101 | |||
102 | `:build' (ACTION...) | ||
103 | |||
104 | `:unless' (PREDICATE) | ||
105 | |||
106 | `:when' (PREDICATE) | ||
107 | |||
108 | Other keywords are ignored. | ||
109 | |||
110 | \(fn (PACKAGE [SPEC]) [BODY-ARGS] BODY...)" | ||
111 | (declare (indent 1)) | ||
112 | (let* ((name (or (car-safe package) package)) | ||
113 | (backend (yoke--pget package :backend)) | ||
114 | ;; Body keyword arguments | ||
115 | (after (plist-get body :after)) | ||
116 | (depends (plist-get body :depends)) | ||
117 | (req (plist-get body :require)) | ||
118 | (buildp (plist-member body :build)) | ||
119 | (build (plist-get body :build)) | ||
120 | (whenp (plist-member body :when)) | ||
121 | (when (if whenp (plist-get body :when) t)) | ||
122 | (unlessp (plist-member body :unless)) | ||
123 | (unless (if unlessp (plist-get body :unless) nil)) | ||
124 | ;; Body | ||
125 | (body (let ((b body) r) | ||
126 | (while (consp b) | ||
127 | (if (keywordp (car b)) | ||
128 | (setf b (cdr b)) | ||
129 | (setf r (cons (car b) r))) | ||
130 | (setf b (cdr b))) | ||
131 | (reverse r))) | ||
132 | (esym (make-symbol "yoke-error"))) | ||
133 | ;; Body modifiers. These are applied in reverse order (that is, the last | ||
134 | ;; one will be on the outside). | ||
135 | ;; --- Require the current package | ||
136 | (when req | ||
137 | (setf body | ||
138 | (append (let (reqs) | ||
139 | (dolist (r (ensure-list req) reqs) | ||
140 | (let* ((feat (if (eq r t) name r)) | ||
141 | (+feat (intern (format "+%s" feat)))) | ||
142 | (push `(require ',feat) reqs) | ||
143 | (push `(require ',+feat nil :noerror) reqs))) | ||
144 | (reverse reqs)) | ||
145 | body))) | ||
146 | ;; --- Install the package | ||
147 | (when (consp package) | ||
148 | (push `(yoke-install ',(car package) ,@(cdr package)) | ||
149 | body)) | ||
150 | ;; --- Dependencies | ||
151 | (when depends | ||
152 | (setf body | ||
153 | (append (cl-loop for dep in (ensure-list depends) | ||
154 | collect `(or (yoke-install ',@(ensure-list dep)) | ||
155 | (error "Dependency (%s): %S" | ||
156 | ',dep ',package))) | ||
157 | body))) | ||
158 | ;; --- Load after | ||
159 | (when after | ||
160 | (setf body `((yoke--eval-after ,(cl-subst name t after) ,@body)))) | ||
161 | ;; --- Conditional expansion | ||
162 | (when (or whenp unlessp) | ||
163 | (setf body | ||
164 | (append (cond | ||
165 | ((and whenp unlessp) | ||
166 | `((when (or (not ,when) ,unless) | ||
167 | (signal 'yoke-predicate | ||
168 | '(:when ,when :unless ,unless))))) | ||
169 | (whenp | ||
170 | `((unless ,when (signal 'yoke-predicate | ||
171 | '(:when ,when))))) | ||
172 | (unlessp | ||
173 | `((when ,unless (signal 'yoke-predicate | ||
174 | '(:unless ,unless)))))) | ||
175 | body))) | ||
176 | ;; Expansion | ||
177 | `(condition-case ,esym | ||
178 | (cl-letf (((symbol-function 'package--save-selected-packages) | ||
179 | #'ignore)) | ||
180 | ;; Body | ||
181 | ,@body) | ||
182 | (:success | ||
183 | ,(unless (atom package) | ||
184 | `(setf (alist-get ',name yoke-selected-packages) | ||
185 | (list ,@(cdr-safe package)))) | ||
186 | ',package) | ||
187 | (t ,(if yoke-debug-on-error | ||
188 | `(signal (car ,esym) (cdr ,esym)) | ||
189 | `(message "(yoke) %s: %s" (car ,esym) (cdr ,esym))))))) | ||
190 | |||
191 | ;;; Installing packages | ||
192 | |||
193 | (defun yoke-install (name &rest args) | ||
194 | "Install package NAME, with ARGS." | ||
195 | (let ((custom-file null-device) | ||
196 | (inhibit-message (and (not (plist-member args :update)) | ||
197 | (not debug-on-error))) | ||
198 | (messages-buffer-name yoke-message-buffer)) | ||
199 | (funcall | ||
200 | (intern | ||
201 | (format "yoke-install-%s" | ||
202 | (or (plist-get args :backend) | ||
203 | (yoke--guess-backend (plist-get args :url)) | ||
204 | 'package))) | ||
205 | name args)) | ||
206 | (yoke--clean-load-path) | ||
207 | ;; Don't return nil | ||
208 | t) | ||
209 | |||
210 | (defun yoke-install-package (name args &optional tries) | ||
211 | "Install package NAME with ARGS using `package' machinery. | ||
212 | TRIES is an internal variable." | ||
213 | (let ((package-user-dir yoke-directory) | ||
214 | (url (plist-get args :url)) | ||
215 | (update (plist-get args :update)) | ||
216 | (dirname (expand-file-name (format "%s" name) | ||
217 | yoke-directory)) | ||
218 | (tries (or tries 0)) | ||
219 | load-dir autoloads-file-name) | ||
220 | (unless (file-exists-p dirname) | ||
221 | (setq dirname (or (car-safe (file-expand-wildcards | ||
222 | (concat dirname "*"))) | ||
223 | dirname))) | ||
224 | (setq load-dir | ||
225 | (expand-file-name (or (plist-get args :lisp-dir) "") dirname) | ||
226 | generated-autoload-file | ||
227 | (expand-file-name (format "%s-autoloads.el" name) load-dir)) | ||
228 | (prog1 | ||
229 | (condition-case error | ||
230 | (cond | ||
231 | ;; -- Commented on 2022-12-21 | ||
232 | ;; ((and (file-exists-p dirname) | ||
233 | ;; (not update)) | ||
234 | ;; (add-to-list 'load-path | ||
235 | ;; (expand-file-name | ||
236 | ;; (or (plist-get args :lisp-dir) "") | ||
237 | ;; dirname) | ||
238 | ;; nil #'equal) | ||
239 | ;; (require (intern (format "%s-autoloads" name)))) | ||
240 | ((and url update) | ||
241 | (package-vc-update (cadr (assoc name package-alist)))) | ||
242 | (update | ||
243 | (package-update name)) | ||
244 | (url | ||
245 | ;; I'm going to be honest here, this is extremely cursed. But I | ||
246 | ;; don't want to get asked about installing the packages, and when | ||
247 | ;; the user answers 'no', the function errors. So.. this. | ||
248 | (cl-letf (((symbol-function 'yes-or-no-p) #'ignore)) | ||
249 | (ignore-errors (package-vc-install (cons name args))))) | ||
250 | (:else | ||
251 | (package-install name))) | ||
252 | (file-error (if (> tries 1) | ||
253 | (error "(yoke) Can't install `%s'" name) | ||
254 | (package-refresh-contents) | ||
255 | (yoke-install-package name args (1+ tries))))) | ||
256 | (add-to-list 'load-path load-dir nil #'equal) | ||
257 | (loaddefs-generate load-dir generated-autoload-file) | ||
258 | ;; Do it again, if it doesn't actually /generate/ anything | ||
259 | (when (eq 'provide | ||
260 | (with-current-buffer (find-file-noselect generated-autoload-file) | ||
261 | (read (buffer-substring (point-min) (point-max))))) | ||
262 | (loaddefs-generate load-dir generated-autoload-file nil nil nil | ||
263 | :generate-full)) | ||
264 | (load generated-autoload-file :noerror) | ||
265 | (kill-buffer (get-file-buffer generated-autoload-file)) | ||
266 | (package-activate name)))) | ||
267 | |||
268 | (defun yoke-install-http (name args) | ||
269 | "Install a package NAME using ARGS from an http source." | ||
270 | (let* ((url (plist-get args :url)) | ||
271 | (cached (expand-file-name (file-name-nondirectory url) | ||
272 | yoke-cache-directory)) | ||
273 | (update (plist-get args :update))) | ||
274 | (unless url | ||
275 | (error "No URL for HTTP download: %S" (cons name args))) | ||
276 | (when (or (not (file-exists-p cached)) | ||
277 | update) | ||
278 | (make-directory yoke-cache-directory :parents) | ||
279 | (message "Downloading `%s'..." url) | ||
280 | (let* ((url-debug t) | ||
281 | (buf (url-retrieve-synchronously url))) | ||
282 | (with-current-buffer buf | ||
283 | (goto-char (point-min)) | ||
284 | (delete-region (point) (1+ (re-search-forward "^$"))) | ||
285 | (write-file cached 1) | ||
286 | (message "Downloading `%s'...Done." url)))) | ||
287 | (package-install-file cached))) | ||
288 | |||
289 | (defun yoke-install-file (name args) | ||
290 | "Install package NAME using ARGS from a file on-disk." | ||
291 | (let ((url (plist-get args :url)) | ||
292 | (update (plist-get args :update)) | ||
293 | (dirname (expand-file-name (format "%s" name) yoke-directory))) | ||
294 | (if (file-exists-p url) | ||
295 | ;; This takes care of updating too. | ||
296 | (package-install-file url) | ||
297 | (error "(yoke) No such file: `%s'" url)))) | ||
298 | |||
299 | ;;; Other package transactions | ||
300 | |||
301 | (defun yoke--choose-package () | ||
302 | "Choose a package from `yoke-selected-packages'." | ||
303 | (assoc (intern (completing-read "Package: " yoke-selected-packages)) | ||
304 | yoke-selected-packages)) | ||
305 | |||
306 | (defun yoke-update (name &rest args) | ||
307 | (interactive (yoke--choose-package)) | ||
308 | (save-window-excursion | ||
309 | (apply #'yoke-install name (append '(:update t) | ||
310 | args)))) | ||
311 | |||
312 | (defun yoke-update-all () | ||
313 | (interactive) | ||
314 | (dolist (pkg yoke-selected-packages) | ||
315 | (apply #'yoke-update pkg))) | ||
316 | |||
317 | ;;; Emacs integration | ||
318 | |||
319 | (defun yoke-imenu-insinuate () | ||
320 | "Insinuate `yoke' forms for `imenu'." | ||
321 | (require 'imenu) | ||
322 | (setf (alist-get "Yoke" imenu-generic-expression nil nil #'equal) | ||
323 | (list "(yoke[[:space:]]*(?\\([^\t\n )]*\\)" | ||
324 | 1)) | ||
325 | (with-eval-after-load 'consult-imenu | ||
326 | (setf (alist-get ?y (plist-get (alist-get 'emacs-lisp-mode | ||
327 | consult-imenu-config) | ||
328 | :types)) | ||
329 | '("Yoke")))) | ||
330 | |||
331 | ;;; Utility functions | ||
332 | |||
333 | (defun yoke--pget (spec prop &optional default) | ||
334 | "Get PROP's value from SPEC, a yoke specification. | ||
335 | If KEY doesn't exist, return DEFAULT." | ||
336 | (let ((pl (or (and (plistp spec) spec) | ||
337 | (cdr-safe spec)))) | ||
338 | (if (plist-member pl prop) | ||
339 | (plist-get pl prop) | ||
340 | default))) | ||
341 | |||
342 | (defun yoke--guess-backend (url) | ||
343 | "Guess the backend to use from URL. | ||
344 | If inconclusive, return nil." | ||
345 | (cond | ||
346 | ((or (string-prefix-p "file:" url t) | ||
347 | (string-prefix-p "~" url) | ||
348 | (string-prefix-p "/" url)) | ||
349 | 'file) | ||
350 | (:else nil))) | ||
351 | |||
352 | (defun yoke--clean-load-path () | ||
353 | (when-let ((first (string-remove-suffix "/" (car load-path))) | ||
354 | (second (string-remove-suffix "/" (cadr load-path))) | ||
355 | (_ (equal first second))) | ||
356 | (setf load-path (cdr load-path)) | ||
357 | (setf (car load-path) second))) | ||
358 | |||
359 | (defun yoke--eval-after-init (fn) | ||
360 | "Evaluate FN after inititation, or now if Emacs is initialized. | ||
361 | FN is called with no arguments." | ||
362 | (if after-init-time | ||
363 | (funcall fn) | ||
364 | (add-hook 'after-init-hook fn))) | ||
365 | |||
366 | (defmacro yoke--eval-after (prereqs &rest body) | ||
367 | "Evaluate body after PREREQS. | ||
368 | PREREQS can be a feature, a number, `:init', or a list of those. | ||
369 | |||
370 | Features are used as arguments to `eval-after-load'. Numbers are | ||
371 | used as arguments to `run-with-idle-timer'. `:init' will ensure BODY | ||
372 | runs after Emacs's init time. | ||
373 | |||
374 | When given a list of PREREQS, `eval-after' will nest each one | ||
375 | from left to right." | ||
376 | (declare (indent 1) (debug (form def-body))) | ||
377 | (setf prereqs (ensure-list prereqs)) | ||
378 | (if (null prereqs) | ||
379 | (macroexp-progn body) | ||
380 | (let* ((this (car prereqs)) | ||
381 | (form `((lambda () (yoke--eval-after ,(cdr prereqs) ,@body))))) | ||
382 | (cond | ||
383 | ((eq this :init) | ||
384 | (append '(yoke--eval-after-init) form)) | ||
385 | ((numberp this) | ||
386 | (append `(run-with-idle-timer ,this nil) form)) | ||
387 | ((symbolp this) | ||
388 | (append `(eval-after-load ',this) form)) | ||
389 | (:else (user-error "Eval-after: Bad prereq: %S" this)))))) | ||
390 | |||
391 | (provide 'yoke) | ||
392 | ;;; yoke.el ends here | ||