From 59a1f58695d09ab29ddf992b2c0711c94a4039ea Mon Sep 17 00:00:00 2001 From: Case Duckworth Date: Tue, 3 Jan 2023 23:03:03 -0600 Subject: Switch to use-package --- lisp/yoke.el | 664 +++++++++++++++++++++++++++++++---------------------------- 1 file changed, 352 insertions(+), 312 deletions(-) (limited to 'lisp/yoke.el') diff --git a/lisp/yoke.el b/lisp/yoke.el index 8ca94fd..ec84f56 100644 --- a/lisp/yoke.el +++ b/lisp/yoke.el @@ -1,271 +1,360 @@ -;;; yoke.el --- make your editor work for YOU -*- lexical-binding: t; -*- -;; Copyright (C) 2022 C. Duckworth +;;; yoke.el --- Yoke configuration into your config -*- lexical-binding: t; -*- + +;; Copyright (C) 2022 Case Duckworth + +;; Author: Case Duckworth +;; Keywords: convenience +;; Package-Version: 0.61803398875 +;; Homepage: https://junk.acdw.net/yoke.el +;; Package-Requires: ((emacs "28.1")) + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . ;;; Commentary: -;; What's the most basic functionality of a package manager? In my view, all a -;; package manager should do is fetch packages from wherever they are, and -;; provide the system with a method of accessing those packages' functionality. -;; In Emacs, this means downloading packages from the Internet and adding their -;; directories to `load-path'. That's what `yoke' tries to do. -;; -;; In fact, that's /all/ `yoke' tries to do, on the package front. It doesn't -;; automatically fetch dependencies. It doesnt' do much else of anything -;; --- hell, it doesn't have to generate autoloads or build the dang source -;; files if you don't want it to. /I/ have it do those things because I like a -;; few creature comforts, but you can turn 'em off. -;; -;; Instead of focusing too much on installing packages, `yoke' works harder to -;; group---to "yoke together," if you will---related configurations together, à -;; la `use-package' or `setup'. I used both of those packages before and found -;; each somewhat lacking, and what I really wanted was a fancy `progn' that I -;; could put whatever I want inside. So that's basically what `yoke' is. It's -;; a configuration macro that automatically fetches packages from their repos -;; and tells Emacs where they are, then executes its body in a `cl-block' for -;; ... reasons. That's it. +;; THIS IS A WORK IN PROGRESS. DON'T USE IT. ;;; Code: (require 'cl-lib) +(require 'package-vc) -;;; Customization options +;;; User options (defgroup yoke nil "Customizations for `yoke'." - :group 'applications + :group 'convenience :prefix "yoke-") -(defcustom yoke-dir (locate-user-emacs-file "yoke") +(defcustom yoke-directory package-user-dir "Where to put yoked packages." :type 'file) -(defcustom yoke-get-default-fn #'yoke-get-git - "Default function to get packages with." - :type 'function) +(defcustom yoke-cache-directory (locate-user-emacs-file "yoke-cache" + "~/.yoke-cache") + "Where to put cached yoke files, like downloaded HTTP packages." + :type 'file) + +(defcustom yoke-debug-on-error nil + "Whether to throw up the debugger on a yoke error. +If nil, errors will be inserted in the `yoke-message-buffer'.") + +;;; Variables + +(defvar yoke-message-buffer " *yoke*" + "The buffer used for yoke messages.") + +(defvar yoke-selected-packages nil + "List of packages managed by `yoke'.") + +(defvar yoke-backends '(file http package) + "Backends handled by `yoke'.") + +;;; Main functionality + +(defmacro yoke (package &rest body) + "Yoke a package into your Emacs session. +PACKAGE is either a symbol, in which case `yoke' expands to +basically a named `progn' (good for grouping configuration), or a +list of the form (NAME . ARGS), where ARGS can be one of the +following: + +- nil: install NAME using `package'. +- a property list describing a package specification. Valid + key/value pairs include + + `:backend' (symbol) + A symbol of the yoke backend to use for installing the + package. See `yoke-backends' for allowed backends. + + `:url' (string) + The URL of the package's repository or source. + + `:lisp-dir' (string) + The repository-relative name of the directory to use for + loading lisp sources. If not given, it defaults to the + repo's root directory. -(defvar yoke-buffer "*yoke*" - "Buffer to use for yoke process output.") + Other pairs may be valid for a given backend; see that + backend's `yoke-install' function for more details. -(defvar yoke-dirs nil - "List of directories managed by `yoke'.") +BODY is executed in a `condition-case' so that errors won't keep +the rest of Emacs from initializing. BODY can also be prepended +by the following keyword arguments: -;;; GET YOKED + `:after' (FEATURE...) -(defmacro yoke (package - &rest body) - "Yoke PACKAGE to work with your Emacs. -Execute BODY afterward. + `:require' (FEATURE...) -\(fn (PACKAGE [REPO REPO-KEYWORDS]) [BODY-KEYWORDS] BODY...)" + `:depends' (PACKAGE-SPEC...) + + `:build' (ACTION...) + + `:unless' (PREDICATE) + + `:when' (PREDICATE) + +Other keywords are ignored. + +\(fn (PACKAGE [SPEC]) [BODY-ARGS] BODY...)" (declare (indent 1)) - (let* (;; State - (pkg (cond ((consp package) (car package)) - (:else package))) - (url (cond ((consp package) (cdr package)) - (:else nil))) - (pname (intern (format "yoke:%s" pkg))) - (dirvar '$yoke-dir) - ;; Keyword args --- TODO: Naming could probably be better. + (let* ((name (or (car-safe package) package)) + (backend (yoke--pget package :backend)) + ;; Body keyword arguments (after (plist-get body :after)) (depends (plist-get body :depends)) + (req (plist-get body :require)) + (buildp (plist-member body :build)) + (build (plist-get body :build)) (whenp (plist-member body :when)) + (when (if whenp (plist-get body :when) t)) (unlessp (plist-member body :unless)) - (when (cond (whenp (plist-get body :when)) - (:else t))) - (unless (cond (unlessp (plist-get body :unless)) - (:else nil))) - (autoload (cond ((plist-member body :autoload) - (plist-get body :autoload)) - (:else t))) - (pre (plist-get body :pre)) + (unless (if unlessp (plist-get body :unless) nil)) ;; Body - (body (cl-loop for (this next) on body by #'cddr - unless (keywordp this) - append (list this next) into ret - finally return (cond ((eq (car (last ret)) nil) - (butlast ret)) - (:else ret)))) - (r (gensym))) - `(let ((,r (cl-block ,pname -(condition-case err - (progn - ;; Pass `:when' or `:unless' clauses - ,@(cond - ((and whenp unlessp) - `((when (or (not ,when) ,unless) - (cl-return-from ,pname - (format "%s (abort) :when %S :unless %S" - ',pname ',when ',unless))))) - (whenp - `((unless ,when (cl-return-from ,pname - (format "%s (abort) :when %S" - ',pname ',when))))) - (unlessp - `((when ,unless (cl-return-from ,pname - (format "%s (abort) :unless %S" - ',pname ',unless)))))) - ;; Evaluate `:pre' forms - ,@pre - ;; Get prerequisite packages - ,@(cl-loop - for (pkg* . yoke-get-args) in depends - collect `(or - (let* ((pkg-spec (yoke-get ,@yoke-get-args - :dir ,(format "%s" pkg*))) - (dir (expand-file-name (or (plist-get (cdr pkg-spec) :load) - "") - (car pkg-spec)))) - (and dir - ,@(if autoload - `((yoke-generate-autoloads ',pkg* dir)) - '(t)) - (add-to-list 'yoke-dirs dir nil #'string=))) - (cl-return-from ,pname - (format "Error fetching prerequiste: %s" - ',pkg*)))) - ;; Download the package, generate autoloads - ,@(when url - `((let* ((pkg-spec (yoke-get ,@url :dir ,(format "%s" pkg))) - (,dirvar (expand-file-name (or (plist-get (cdr pkg-spec) :load) - "") - (car pkg-spec)))) - ,@(when autoload - `((yoke-generate-autoloads ',pkg ,dirvar))) - (add-to-list 'yoke-dirs ,dirvar nil #'string=)))) - ;; Evaluate the body, optionally after the features in `:after' - ,@(cond (after - `((yoke-eval-after ,after ,@body))) - (:else body))) - (:success ',package) - (t (message "%s: %s (%s)" ',pname (car err) (cdr err)) - nil))))) - (when (stringp ,r) (message "%S" ,r)) - ,r))) - -(defun yoke-get (url &rest args) - "\"Get\" URL and and put it in DIR, then add DIR to `load-path'. -URL can be a string or a list of the form (TYPE URL). The -download will be dispatched to the TYPE, or to -`yoke-get-default-fn' if only a string is given. -ARGS is a plist with the following possible keys: - -:dir DIRECTORY --- the directory to put the URL. -:load DIRECTORY --- the directory (relative to the download path) - to add to `load-path'. -:type TYPE --- one of `http', `git', or `file' --- how to - download URL." - (let* ((dir (plist-get args :dir)) - (load (plist-get args :load)) - (type (or (plist-get args :type))) - (path (cond - ((eq type 'http) (yoke-get-http url dir)) - ((or (eq type 'git) - (string-match-p (rx bos "git:") url)) - (yoke-get-git url dir)) - ((or (eq type 'file) - (string-match-p (rx bos (or "file:" "~" "/")) url)) - (yoke-get-file url dir)) - ((stringp url) - (funcall yoke-get-default-fn url dir)) - (:else (error "Uknown URL type: %S" url))))) - (cond - ((file-exists-p path) - (add-to-list 'load-path (expand-file-name (or load "") path)) - (cons path args)) - (:else (error "Directory \"%s\" doesn't exist." path) - nil)))) - -(defun yoke-get--guess-directory (path &optional dir) - "Guess directory from PATH and DIR, and return it. -If DIR is present and relative, resolve it relative to -`yoke-dir', or if it's absolute, leave it as-is. If DIR is -absent, return the final component of PATH resolved relative to -`yoke-dir'." - (expand-file-name (or dir (file-name-nondirectory path)) - yoke-dir)) - -(defun yoke-get-http (url &optional dir) - "Download URL to DIR and return its directory. -If DIR isn't given, it's guessed from the final component of the -URL's path and placed under `yoke-dir'." - (let* ((dir (yoke-get--guess-directory url dir)) - (basename (file-name-nondirectory url)) - ;; XXX: Is this the best idea?? PROBABLY NOT!!! Ideally I'd have - ;; a parameter (either dynamic var or passed in) that would give the - ;; name of the downloaded file. But that would take a bit of - ;; re-engineering, I think. So for now, it stays thus. - (filename (expand-file-name - (replace-regexp-in-string - (rx "-" (+ digit) ; major version - (+ (group "." (+ digit))) ; following version numbers - (group "." (+ (not space)))) ; extension - "\\2" - basename) - dir))) - (cond ((file-exists-p filename) - dir) - (:else - (message "Downloading %s..." url) - (with-current-buffer (let ((url-debug t)) - (url-retrieve-synchronously url)) - (condition-case e - (progn - (goto-char (point-min)) - (delete-region (point) (+ 1 (re-search-forward "^$"))) - (make-directory dir :parents) - (write-file filename 1) - (message "Downloading %s... Done" url)) - (:success dir) - (t (signal (car e) (cdr e))))))))) - -(defun yoke-get-git (repo &optional dir) - "Clone REPO to DIR and return its directory. -If DIR isn't given, it's guessed from the repo's name and put -under `yoke-dir'. Return the cloned directory's name on success, -or nil on failure." - (let ((dir (yoke-get--guess-directory repo dir))) - (cond ((file-exists-p dir) - dir) - (:else - (message "Cloning %s..." repo) - (pcase (call-process "git" nil (get-buffer-create yoke-buffer) nil - "clone" repo dir) - (0 (message "Cloning %s... Done" repo) - dir) - (_ (message "Cloning %s... Error! See buffer %s for output." - repo yoke-buffer) - nil)))))) - -(defun yoke-get-file (file &optional _dir) - "Add FILE's directory to `load-dir'. -_DIR is ignored." - (file-name-directory file)) - -(defun yoke-generate-autoloads (package dir) - "Generate autoloads for PACKAGE in DIR." - ;; Shamelessly stolen from `straight'. - (eval-and-compile (require 'autoload)) - (let ((generated-autoload-file - (expand-file-name (format "%s-autoloads.el" package) dir)) - (backup-inhibited t) - (version-control 'never) - (message-log-max nil) - (inhibit-message t)) - (unless (file-exists-p generated-autoload-file) - (let ((find-file-hook nil) - (write-file-functions nil) - (debug-on-error nil) - (left-margin 0)) - (if (fboundp 'make-directory-autoloads) - (make-directory-autoloads dir generated-autoload-file) - (and (fboundp 'update-directory-autoloads) - (update-directory-autoloads dir))))) - (when-let ((buf (find-buffer-visiting generated-autoload-file))) - (kill-buffer buf)) - (load generated-autoload-file :noerror :nomessage) - t)) - -;;; Evaluating forms after features + (body (let ((b body) r) + (while (consp b) + (if (keywordp (car b)) + (setf b (cdr b)) + (setf r (cons (car b) r))) + (setf b (cdr b))) + (reverse r))) + (esym (make-symbol "yoke-error"))) + ;; Body modifiers. These are applied in reverse order (that is, the last + ;; one will be on the outside). + ;; --- Require the current package + (when req + (setf body + (append (let (reqs) + (dolist (r (ensure-list req) reqs) + (let* ((feat (if (eq r t) name r)) + (+feat (intern (format "+%s" feat)))) + (push `(require ',feat) reqs) + (push `(require ',+feat nil :noerror) reqs))) + (reverse reqs)) + body))) + ;; --- Install the package + (when (consp package) + (push `(yoke-install ',(car package) ,@(cdr package)) + body)) + ;; --- Dependencies + (when depends + (setf body + (append (cl-loop for dep in (ensure-list depends) + collect `(or (yoke-install ',@(ensure-list dep)) + (error "Dependency (%s): %S" + ',dep ',package))) + body))) + ;; --- Load after + (when after + (setf body `((yoke--eval-after ,(cl-subst name t after) ,@body)))) + ;; --- Conditional expansion + (when (or whenp unlessp) + (setf body + (append (cond + ((and whenp unlessp) + `((when (or (not ,when) ,unless) + (signal 'yoke-predicate + '(:when ,when :unless ,unless))))) + (whenp + `((unless ,when (signal 'yoke-predicate + '(:when ,when))))) + (unlessp + `((when ,unless (signal 'yoke-predicate + '(:unless ,unless)))))) + body))) + ;; Expansion + `(condition-case ,esym + (cl-letf (((symbol-function 'package--save-selected-packages) + #'ignore)) + ;; Body + ,@body) + (:success + ,(unless (atom package) + `(setf (alist-get ',name yoke-selected-packages) + (list ,@(cdr-safe package)))) + ',package) + (t ,(if yoke-debug-on-error + `(signal (car ,esym) (cdr ,esym)) + `(message "(yoke) %s: %s" (car ,esym) (cdr ,esym))))))) + +;;; Installing packages + +(defun yoke-install (name &rest args) + "Install package NAME, with ARGS." + (let ((custom-file null-device) + (inhibit-message (and (not (plist-member args :update)) + (not debug-on-error))) + (messages-buffer-name yoke-message-buffer)) + (funcall + (intern + (format "yoke-install-%s" + (or (plist-get args :backend) + (yoke--guess-backend (plist-get args :url)) + 'package))) + name args)) + (yoke--clean-load-path) + ;; Don't return nil + t) + +(defun yoke-install-package (name args &optional tries) + "Install package NAME with ARGS using `package' machinery. +TRIES is an internal variable." + (let ((package-user-dir yoke-directory) + (url (plist-get args :url)) + (update (plist-get args :update)) + (dirname (expand-file-name (format "%s" name) + yoke-directory)) + (tries (or tries 0)) + load-dir autoloads-file-name) + (unless (file-exists-p dirname) + (setq dirname (or (car-safe (file-expand-wildcards + (concat dirname "*"))) + dirname))) + (setq load-dir + (expand-file-name (or (plist-get args :lisp-dir) "") dirname) + generated-autoload-file + (expand-file-name (format "%s-autoloads.el" name) load-dir)) + (prog1 + (condition-case error + (cond + ;; -- Commented on 2022-12-21 + ;; ((and (file-exists-p dirname) + ;; (not update)) + ;; (add-to-list 'load-path + ;; (expand-file-name + ;; (or (plist-get args :lisp-dir) "") + ;; dirname) + ;; nil #'equal) + ;; (require (intern (format "%s-autoloads" name)))) + ((and url update) + (package-vc-update (cadr (assoc name package-alist)))) + (update + (package-update name)) + (url + ;; I'm going to be honest here, this is extremely cursed. But I + ;; don't want to get asked about installing the packages, and when + ;; the user answers 'no', the function errors. So.. this. + (cl-letf (((symbol-function 'yes-or-no-p) #'ignore)) + (ignore-errors (package-vc-install (cons name args))))) + (:else + (package-install name))) + (file-error (if (> tries 1) + (error "(yoke) Can't install `%s'" name) + (package-refresh-contents) + (yoke-install-package name args (1+ tries))))) + (add-to-list 'load-path load-dir nil #'equal) + (loaddefs-generate load-dir generated-autoload-file) + ;; Do it again, if it doesn't actually /generate/ anything + (when (eq 'provide + (with-current-buffer (find-file-noselect generated-autoload-file) + (read (buffer-substring (point-min) (point-max))))) + (loaddefs-generate load-dir generated-autoload-file nil nil nil + :generate-full)) + (load generated-autoload-file :noerror) + (kill-buffer (get-file-buffer generated-autoload-file)) + (package-activate name)))) + +(defun yoke-install-http (name args) + "Install a package NAME using ARGS from an http source." + (let* ((url (plist-get args :url)) + (cached (expand-file-name (file-name-nondirectory url) + yoke-cache-directory)) + (update (plist-get args :update))) + (unless url + (error "No URL for HTTP download: %S" (cons name args))) + (when (or (not (file-exists-p cached)) + update) + (make-directory yoke-cache-directory :parents) + (message "Downloading `%s'..." url) + (let* ((url-debug t) + (buf (url-retrieve-synchronously url))) + (with-current-buffer buf + (goto-char (point-min)) + (delete-region (point) (1+ (re-search-forward "^$"))) + (write-file cached 1) + (message "Downloading `%s'...Done." url)))) + (package-install-file cached))) + +(defun yoke-install-file (name args) + "Install package NAME using ARGS from a file on-disk." + (let ((url (plist-get args :url)) + (update (plist-get args :update)) + (dirname (expand-file-name (format "%s" name) yoke-directory))) + (if (file-exists-p url) + ;; This takes care of updating too. + (package-install-file url) + (error "(yoke) No such file: `%s'" url)))) + +;;; Other package transactions + +(defun yoke--choose-package () + "Choose a package from `yoke-selected-packages'." + (assoc (intern (completing-read "Package: " yoke-selected-packages)) + yoke-selected-packages)) + +(defun yoke-update (name &rest args) + (interactive (yoke--choose-package)) + (save-window-excursion + (apply #'yoke-install name (append '(:update t) + args)))) + +(defun yoke-update-all () + (interactive) + (dolist (pkg yoke-selected-packages) + (apply #'yoke-update pkg))) + +;;; Emacs integration + +(defun yoke-imenu-insinuate () + "Insinuate `yoke' forms for `imenu'." + (require 'imenu) + (setf (alist-get "Yoke" imenu-generic-expression nil nil #'equal) + (list "(yoke[[:space:]]*(?\\([^\t\n )]*\\)" + 1)) + (with-eval-after-load 'consult-imenu + (setf (alist-get ?y (plist-get (alist-get 'emacs-lisp-mode + consult-imenu-config) + :types)) + '("Yoke")))) + +;;; Utility functions + +(defun yoke--pget (spec prop &optional default) + "Get PROP's value from SPEC, a yoke specification. +If KEY doesn't exist, return DEFAULT." + (let ((pl (or (and (plistp spec) spec) + (cdr-safe spec)))) + (if (plist-member pl prop) + (plist-get pl prop) + default))) + +(defun yoke--guess-backend (url) + "Guess the backend to use from URL. +If inconclusive, return nil." + (cond + ((or (string-prefix-p "file:" url t) + (string-prefix-p "~" url) + (string-prefix-p "/" url)) + 'file) + (:else nil))) + +(defun yoke--clean-load-path () + (when-let ((first (string-remove-suffix "/" (car load-path))) + (second (string-remove-suffix "/" (cadr load-path))) + (_ (equal first second))) + (setf load-path (cdr load-path)) + (setf (car load-path) second))) (defun yoke--eval-after-init (fn) "Evaluate FN after inititation, or now if Emacs is initialized. @@ -274,79 +363,30 @@ FN is called with no arguments." (funcall fn) (add-hook 'after-init-hook fn))) -(defmacro yoke-eval-after (features &rest body) - "Evaluate BODY, but only after loading FEATURES. -FEATURES can be an atom or a list; as an atom it works like -`with-eval-after-load'. The special feature `init' will evaluate -BODY after Emacs is finished initializing." - (declare (indent 1) - (debug (form def-body))) - (unless (listp features) - (setf features (list features))) - (if (null features) - (macroexp-progn body) - (let* ((this (car features)) - (rest (cdr features))) - (cond ((eq this 'init) - `(yoke--eval-after-init - (lambda () (yoke-eval-after ,rest ,@body)))) - (:else - `(with-eval-after-load ',this - (yoke-eval-after ,rest ,@body))))))) +(defmacro yoke--eval-after (prereqs &rest body) + "Evaluate body after PREREQS. +PREREQS can be a feature, a number, `:init', or a list of those. -;;; Integration +Features are used as arguments to `eval-after-load'. Numbers are +used as arguments to `run-with-idle-timer'. `:init' will ensure BODY +runs after Emacs's init time. -(defun yoke-imenu-insinuate () - "Insinuate `yoke' forms for `imenu'." - (require 'imenu) - (setf (alist-get "Yoke" imenu-generic-expression nil nil #'equal) - (list (rx (: "(yoke" (+ space) (? "(") - (group (+ (not (or "(" " " "\t" "\n")))) - (* any))) - 1))) - -;;; Package maintenance - -(defvar yoke--all "*all*" - "Value that `yoke--prompt-for-package' uses for all packages.") - -(defun yoke--choose-packages (prompt &optional onep) - "Choose from all of yoke's installed packages." - (funcall (if onep #'completing-read #'completing-read-multiple) - prompt - (cons yoke--all yoke-dirs) - nil :require-match nil nil - (unless onep yoke--all))) - -(defun yoke--choices (&optional selections) - "Either the SELECTIONS given, or all of `yoke-dirs'. -If `yoke--all' is part of SELECTIONS, or if it's not given, -return the full list of `yoke-dirs'." - (cond ((or (null selections) - (member yoke--all selections)) - yoke-dirs) - (:else selections))) - -(defun yoke-compile (&rest packages) - "Compile all elisp files in `yoke-dirs'." - (interactive (yoke--choose-packages "Compile packages: ")) - (dolist (dir (yoke--choices packages)) - (byte-recompile-directory dir 0))) - -(defun yoke-update-autoloads (&rest packages) - "Update the autoloads in PACKAGES' directories." - (interactive (yoke--choose-packages "Generate autoloads for packages: ")) - (dolist (dir (yoke--choices packages)) - (message "Generating autoloads for %s..." dir) - (yoke-generate-autoloads (file-name-nondirectory dir) dir) - (message "Generating autoloads for %s... Done" dir))) - -(defun yoke-remove (dir) - "Remove DIR from `yoke-dir'." - (interactive - (list (completing-read "Remove: " yoke-dirs - nil :require-match))) - (delete-directory dir :recursive :trash)) +When given a list of PREREQS, `eval-after' will nest each one +from left to right." + (declare (indent 1) (debug (form def-body))) + (setf prereqs (ensure-list prereqs)) + (if (null prereqs) + (macroexp-progn body) + (let* ((this (car prereqs)) + (form `((lambda () (yoke--eval-after ,(cdr prereqs) ,@body))))) + (cond + ((eq this :init) + (append '(yoke--eval-after-init) form)) + ((numberp this) + (append `(run-with-idle-timer ,this nil) form)) + ((symbolp this) + (append `(eval-after-load ',this) form)) + (:else (user-error "Eval-after: Bad prereq: %S" this)))))) (provide 'yoke) ;;; yoke.el ends here -- cgit 1.4.1-21-gabe81