branch: externals/timeout commit 16a9550aa64ab6d803a985720aa2ae84c2b69e4e Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com> Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
timeout: Add timeout.el and README --- README.org | 34 ++++++++++++++++++ timeout.el | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/README.org b/README.org new file mode 100644 index 0000000000..3b664aed0e --- /dev/null +++ b/README.org @@ -0,0 +1,34 @@ +* Timeout: Sometimes Emacs needs one + +=timeout= is a small library to help you throttle or debounce elisp function calls. See [[https://karthinks.com/software/cool-your-heels-emacs][this write-up]] for an introduction and potential uses. + +It's actually tiny, just a couple of functions. + +*** To use this library: + +You can throttle an elisp function =func= to run at most once every 2 seconds: +#+begin_src emacs-lisp +(timeout-throttle! 'func 2.0) +#+end_src + +To reset =func=: +#+begin_src emacs-lisp +(timeout-throttle! 'func 0.0) +#+end_src + +When the call is a noop, a throttled function will return the same result as the last successful run. + +You can debounce an elisp function =func= to run after an uninterrupted delay of 0.5 seconds: +#+begin_src emacs-lisp +(timeout-debounce! 'func 0.5) +#+end_src + +To reset =func=: +#+begin_src emacs-lisp +(timeout-debounce! 'func 0.0) +#+end_src + +By default a debounced function returns =nil= at call time. To change this, run: +#+begin_src emacs-lisp +(timeout-debounce! 'func 0.5 'some-return-value) +#+end_src diff --git a/timeout.el b/timeout.el new file mode 100644 index 0000000000..2a7c2348c0 --- /dev/null +++ b/timeout.el @@ -0,0 +1,117 @@ +;;; timeout.el --- throttle or debounce elisp functions -*- lexical-binding: t; -*- + +;; Copyright (C) 2023 Karthik Chikmagalur + +;; Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com> +;; Keywords: convenience, extensions + +;; 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 <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; timeout is a small elisp library that provides higher order functions to +;; throttle or debounce elisp functions. This is useful for corraling +;; over-eager code that: +;; (i) is slow and blocks Emacs, and +;; (ii) does not provide customization options to limit how often it runs, +;; +;; To throttle a function FUNC to run no more than once every 2 seconds, run +;; (timeout-throttle! func 2.0) +;; +;; To debounce a function FUNC to run after a delay of 0.3 seconds, run +;; (timeout-debounce! func 0.3) + +;;; Code: +(require 'nadvice) + +(defun timeout--throttle-advice (&optional timeout) + "Return a function that throttles its argument function. + +THROTTLE defaults to 1.0 seconds. This is intended for use as +function advice." + (let ((throttle-timer) + (timeout (or timeout 1.0)) + (result)) + (lambda (orig-fn &rest args) + "Throttle calls to this function." + (if (and throttle-timer (timerp throttle-timer)) + result + (prog1 + (setq result (apply orig-fn args)) + (setq throttle-timer + (run-with-timer + timeout nil + (lambda () + (cancel-timer throttle-timer) + (setq throttle-timer nil))))))))) + +(defun timeout--debounce-advice (&optional delay default) + "Return a function that debounces its argument function. + +DELAY defaults to 0.50 seconds. DEFAULT is the immediate return +value of the function when called. + +This is intended for use as function advice." + (let ((debounce-timer nil) + (delay (or delay 0.50))) + (lambda (orig-fn &rest args) + "Debounce calls to this function." + (when (and debounce-timer (timerp debounce-timer)) + (cancel-timer debounce-timer)) + (prog1 default + (setq debounce-timer + (run-with-idle-timer + delay nil + (lambda (buf) + (cancel-timer debounce-timer) + (setq debounce-timer nil) + (with-current-buffer buf + (setq result (apply orig-fn args)))) + (current-buffer))))))) + +;;;###autoload +(defun timeout-debounce! (func &optional delay default) + "Debounce FUNC by DELAY seconds. + +This advises FUNC, when called (interactively or from code), to +run after DELAY seconds. If FUNC is called again within this time, +the timer is reset. + +DELAY defaults to 0.5 seconds. Using a delay of 0 resets the +function. + +DEFAULT is the immediate return value of the function when called." + (if (= delay 0) + (advice-remove func 'debounce) + (advice-add func :around (timeout--debounce-advice delay default) + '((name . debounce) + (depth . -99))))) + +;;;###autoload +(defun timeout-throttle! (func &optional throttle) + "Throttle FUNC by THROTTLE seconds. + +This advises FUNC so that it can run no more than once every +THROTTLE seconds. + +THROTTLE defaults to 1.0 seconds. Using a throttle of 0 resets the +function." + (if (= throttle 0) + (advice-remove func 'throttle) + (advice-add func :around (timeout--throttle-advice throttle) + '((name . throttle) + (depth . -98))))) + +(provide 'timeout) +;;; timeout.el ends here