summary refs log tree commit diff stats
path: root/lisp/yoke.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/yoke.el')
-rw-r--r--lisp/yoke.el392
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.
51If 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.
68PACKAGE is either a symbol, in which case `yoke' expands to
69basically a named `progn' (good for grouping configuration), or a
70list of the form (NAME . ARGS), where ARGS can be one of the
71following:
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
92BODY is executed in a `condition-case' so that errors won't keep
93the rest of Emacs from initializing. BODY can also be prepended
94by 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
108Other 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.
212TRIES 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.
335If 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.
344If 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.
361FN 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.
368PREREQS can be a feature, a number, `:init', or a list of those.
369
370Features are used as arguments to `eval-after-load'. Numbers are
371used as arguments to `run-with-idle-timer'. `:init' will ensure BODY
372runs after Emacs's init time.
373
374When given a list of PREREQS, `eval-after' will nest each one
375from 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