summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKarthik Chikmagalur <karthikchikmagalur@gmail.com>2023-08-14 17:46:36 -0700
committerKarthik Chikmagalur <karthikchikmagalur@gmail.com>2023-08-14 18:53:04 -0700
commit16a9550aa64ab6d803a985720aa2ae84c2b69e4e (patch)
tree1e87b1fe927dac14d85cacb9c087d1999748d67a
timeout: Add timeout.el and README
-rw-r--r--README.org34
-rw-r--r--timeout.el117
2 files changed, 151 insertions, 0 deletions
diff --git a/README.org b/README.org
new file mode 100644
index 0000000..3b664ae
--- /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 0000000..2a7c234
--- /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 <karthikchikmagalur@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