;;; refresh.el --- Load Lisp files again as they change on disk -*- lexical-binding: t; -*- ;; Author: Pavel Popov ;; Keywords: lisp, tools ;;; Commentary: ;; Keeps a running Emacs up with Lisp files that change under it - a ;; copy synced from another computer, say. Watch a file: ;; ;; (refresh-watch (locate-library "emacs-web.el" t) #'emacs-web-start) ;; ;; and it is loaded again once it changes. A user option still holding ;; the default the file gave it takes the file's new default; one set ;; to anything else keeps its value. The function, when given, is ;; called after each load, for what loading alone does not redo - a ;; server to restart, say. ;;; Code: (require 'seq) (defgroup refresh nil "Load Lisp files again as they change on disk." :group 'lisp) (defcustom refresh-interval 60 "Seconds between checks of the watched files." :type 'number) (defvar refresh--watched nil "The watched files, each (FILE MODIFIED AFTER). MODIFIED is when FILE was modified as last loaded, AFTER the function to call after loading it, or nil.") (defvar refresh--timer nil "The timer checking the watched files, or nil.") (defun refresh--modified (file) "Return when FILE was last modified, or nil." (file-attribute-modification-time (file-attributes file))) ;;;###autoload (defun refresh-watch (file &optional after) "Load FILE again whenever it changes, then call AFTER if given. FILE as it is now counts as loaded. AFTER is called with no arguments." (let ((file (expand-file-name file))) (setf (alist-get file refresh--watched nil nil #'equal) (list (refresh--modified file) after))) (refresh--schedule)) (defun refresh-unwatch (file) "Stop watching FILE." (setf (alist-get (expand-file-name file) refresh--watched nil t #'equal) nil) (refresh--schedule)) (defun refresh--schedule () "Check the watched files every `refresh-interval' seconds, while there are any." (when refresh--timer (cancel-timer refresh--timer) (setq refresh--timer nil)) (when refresh--watched (setq refresh--timer (run-at-time refresh-interval refresh-interval #'refresh-check)))) (defun refresh-check () "Load each watched file that changed since it was last loaded." (interactive) (dolist (entry refresh--watched) (pcase-let ((`(,file ,modified ,after) entry)) (let ((now (refresh--modified file))) (when (and now (not (equal now modified))) ;; a file that fails to load is tried again on its next change (setf (nth 1 entry) now) (when (and (refresh-load file) after) (with-demoted-errors "Refresh: %S" (funcall after)))))))) (defun refresh--definitions (file) "Return what loading FILE defined, as `load-history' has it." (let ((base (file-name-sans-extension (file-truename file)))) (cdr (seq-find (lambda (entry) (and (stringp (car entry)) (equal (file-name-sans-extension (file-truename (car entry))) base))) load-history)))) (defun refresh--options-at-default (file) "Return the user options FILE defined that still hold their default." (seq-filter (lambda (s) (and (symbolp s) (boundp s) (get s 'standard-value) (ignore-errors (equal (symbol-value s) (eval (car (get s 'standard-value)) t))))) (refresh--definitions file))) (defun refresh-load (file) "Load FILE, its user options still at their defaults taking its new ones. Return non-nil if it loaded; if it did not, the options keep their values." (let ((options (mapcar (lambda (s) (cons s (symbol-value s))) (refresh--options-at-default file)))) (dolist (o options) (makunbound (car o))) (condition-case err (progn (load file nil t t) (message "Refreshed %s" file) t) (error (dolist (o options) (unless (boundp (car o)) (set-default (car o) (cdr o)))) (message "Refreshing %s failed: %s" file (error-message-string err)) nil)))) (provide 'refresh) ;;; refresh.el ends here