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

Reply via email to