X-Git-Url: https://code.delx.au/gnu-emacs-elpa/blobdiff_plain/03d12b4089ec51b73b0f2a3dedecbccbee11d04f..5622af463cacdeae3ddaf9be7c9decf8723eb675:/beacon.el diff --git a/beacon.el b/beacon.el index a652e6701..8e2412008 100644 --- a/beacon.el +++ b/beacon.el @@ -1,11 +1,12 @@ ;;; beacon.el --- Highlight the cursor whenever it moves long distances -*- lexical-binding: t; -*- -;; Copyright (C) 2015 Artur Malabarba +;; Copyright (C) 2015 Free Software Foundation, Inc. ;; Author: Artur Malabarba +;; URL: https://github.com/Malabarba/beacon ;; Keywords: convenience ;; Version: 0.1 -;; Package-Requires: ((cl-lib "0.5")) +;; Package-Requires: ((seq "1.9")) ;; 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 @@ -22,12 +23,34 @@ ;;; Commentary: -;; This is a global minor-mode. Turn it on everywhere with -;; (beacon-mode 1) +;; This is a global minor-mode. Turn it on everywhere with: +;; ┌──── +;; │ (beacon-mode 1) +;; └──── +;; +;; Whenever the window scrolls or the buffer changes a light will shine on +;; top of your cursor so you know where it is. +;; +;; That’s it. +;; +;; +;; 1 Customizations +;; ════════════════ +;; +;; • To customize the appearance of the beacon, configure `beacon-size' +;; and `beacon-color'. +;; +;; • To customize how long it lasts, configure `beacon-blink-duration' +;; and `beacon-blink-delay'. +;; +;; • To customize /when/ it is used at all, configure +;; `beacon-blink-when-window-scrolls', +;; `beacon-blink-when-buffer-changes', and +;; `beacon-blink-when-point-moves'. ;;; Code: -(require 'cl-lib) +(require 'seq) (defgroup beacon nil "Customization group for beacon." @@ -36,18 +59,28 @@ (defvar beacon--timer nil) -(defcustom beacon-minimum-distance 15 - "Minimum movement distance in lines to blink the beacon." - :type 'integer) - (defcustom beacon-push-mark nil - "Should the mark be pushed before long movements?" - :type 'boolean) + "Should the mark be pushed before long movements? +If nil, `beacon' will not push the mark. +Otherwise this should be a number, and `beacon' will push the +mark whenever point moves more than that many lines." + :type '(choice integer (const nil))) + +(defcustom beacon-blink-when-point-moves nil + "Should the beacon blink when moving a long distance? +If nil, don't blink due to plain movement. +If non-nil, this should be an integer, which is the minimum +movement distance (in lines) that triggers a beacon blink." + :type '(choice integer (const nil))) (defcustom beacon-blink-when-buffer-changes t "Should the beacon blink when changing buffer?" :type 'boolean) +(defcustom beacon-blink-when-window-scrolls t + "Should the beacon blink when the window scrolls?" + :type 'boolean) + (defcustom beacon-blink-duration 0.3 "Time, in seconds, that the blink should last." :type 'number) @@ -56,13 +89,38 @@ "Time, in seconds, before starting to fade the beacon." :type 'number) -(defcustom beacon-size 30 +(defcustom beacon-size 40 "Size of the beacon in characters." :type 'number) -(defcustom beacon-brightness 0.5 - "Brightness as a float between 0 and 1." - :type 'number) +(defcustom beacon-color 0.5 + "Color of the beacon. +This can be a string or a number. + +If it is a number, the color is taken to be white or +black (depending on the current theme's background) and this +number is a float between 0 and 1 specifing the brightness. + +If it is a string, it is a color name or specification, +e.g. \"#666600\"." + :type '(choice number color)) + +(defcustom beacon-dont-blink-predicates nil + "A list of predicates that prevent the beacon blink. +These predicate functions are called in order, with no +arguments, before blinking the beacon. If any returns +non-nil, the beacon will not blink." + :type 'hook) + +(add-hook 'beacon-dont-blink-predicates (lambda () (bound-and-true-p hl-line-mode))) +(add-hook 'beacon-dont-blink-predicates #'window-minibuffer-p) + +(defcustom beacon-dont-blink-major-modes nil + "A list of major-modes where the beacon won't blink. +Whenever the current buffer satisfies `derived-mode-p' for +one of the major-modes on this list, the beacon will not +blink." + :type '(repeat symbol)) ;;; Overlays @@ -75,9 +133,42 @@ (overlay-put ov 'beacon t) (push ov beacon--ovs))) +(defun beacon--ov-put-after-string (overlay colors) + "Add an after-string property to OVERLAY. +The property's value is a string of spaces with background +COLORS applied to each one. +If COLORS is nil, OVERLAY is deleted!" + (if (not colors) + (when (overlayp overlay) + (delete-overlay overlay)) + (overlay-put overlay 'beacon-colors colors) + (overlay-put overlay 'priority most-positive-fixnum) + (overlay-put overlay 'after-string + (propertize + (mapconcat (lambda (c) (propertize " " 'face (list :background c))) + colors + "") + 'cursor 1000)))) + +(defun beacon--after-string-overlay (colors) + "Put an overlay at point with an after-string property. +The property's value is a string of spaces with background +COLORS applied to each one." + (let ((ov (make-overlay (point) (point))) + ;; The after-string must not be longer than the remaining columns from + ;; point to right window-end else it will be wrapped around (assuming + ;; truncate-lines is nil) introducing an ugly wrap-around for a + ;; fraction of a second. + (colors (seq-take colors (- (window-width) (current-column))))) + (beacon--ov-put-after-string ov colors) + (overlay-put ov 'beacon t) + (push ov beacon--ovs))) + (defun beacon--ov-at-point () - (car (cl-member-if (lambda (o) (overlay-get o 'beacon)) - (overlays-at (point))))) + (car (or (seq-filter (lambda (o) (overlay-get o 'beacon)) + (overlays-in (point) (point))) + (seq-filter (lambda (o) (overlay-get o 'beacon)) + (overlays-at (point)))))) (defun beacon--vanish () "Turn off the beacon." @@ -99,12 +190,16 @@ Only returns `beacon-size' elements." (defun beacon--color-range () "Return a list of background colors for the beacon." - (let ((bg (color-values (face-attribute 'default :background)))) - (apply #'cl-mapcar (lambda (r g b) (format "#%04x%04x%04x" r g b)) - (if (< (color-distance "black" bg) + (let* ((bg (color-values (face-attribute 'default :background))) + (fg (cond + ((stringp beacon-color) (color-values beacon-color)) + ((< (color-distance "black" bg) (color-distance "white" bg)) - (mapcar (lambda (n) (butlast (beacon--int-range (* beacon-brightness 65535) n))) bg) - (mapcar (lambda (n) (cdr (beacon--int-range (* (- 1 beacon-brightness) 65535) n))) bg))))) + (make-list 3 (* beacon-color 65535))) + (t (make-list 3 (* (- 1 beacon-color) 65535)))))) + (apply #'cl-mapcar (lambda (r g b) (format "#%04x%04x%04x" r g b)) + (mapcar (lambda (n) (butlast (beacon--int-range (elt fg n) (elt bg n)))) + [0 1 2])))) ;;; Blinking @@ -115,41 +210,66 @@ Only returns `beacon-size' elements." (while colors (if (looking-at "$") (progn - ;; (beacon--after-string) + (beacon--after-string-overlay colors) (setq colors nil)) (beacon--colored-overlay (pop colors)) (forward-char 1)))))) (defun beacon--dec () "Decrease the beacon brightness by one." - (let ((o (beacon--ov-at-point))) - (if (not o) - (beacon--vanish) - (delete-overlay o) - (save-excursion - (while (progn (forward-char 1) - (setq o (beacon--ov-at-point))) - (move-overlay o (1- (point)) (point))))))) + (pcase (beacon--ov-at-point) + (`nil (beacon--vanish)) + ((and o (let c (overlay-get o 'beacon-colors)) (guard c)) + (beacon--ov-put-after-string o (cdr c))) + (o + (delete-overlay o) + (save-excursion + (while (progn (forward-char 1) + (setq o (beacon--ov-at-point))) + (let ((colors (overlay-get o 'beacon-colors))) + (if (not colors) + (move-overlay o (1- (point)) (point)) + (forward-char -1) + (beacon--colored-overlay (pop colors)) + (beacon--ov-put-after-string o colors) + (forward-char 1)))))))) (defun beacon-blink () "Blink the beacon at the position of the cursor." (interactive) (beacon--vanish) - (beacon--shine) - (setq beacon--timer - (run-at-time beacon-blink-delay - (/ beacon-blink-duration 1.0 (length beacon--colors)) - #'beacon--dec))) + (unless (or (not beacon-mode) + (run-hook-with-args-until-success 'beacon-dont-blink-predicates) + (seq-find #'derived-mode-p beacon-dont-blink-major-modes)) + (beacon--shine) + (setq beacon--timer + (run-at-time beacon-blink-delay + (/ beacon-blink-duration 1.0 beacon-size) + #'beacon--dec)))) ;;; Movement detection +(defvar beacon--window-scrolled nil) (defvar beacon--previous-place nil) (defvar beacon--previous-mark-head nil) +(defun beacon--movement-> (delta) + "Return non-nil if latest point movement is > DELTA. +If DELTA is nil, return nil." + (and delta + (markerp beacon--previous-place) + (equal (marker-buffer beacon--previous-place) + (current-buffer)) + (> (abs (- (point) beacon--previous-place)) + delta) + (> (count-screen-lines (min (point) beacon--previous-place) + (max (point) beacon--previous-place)) + delta))) + (defun beacon--maybe-push-mark () "Push mark if it seems to be safe." - (when (and beacon-push-mark - (not mark-active)) + (when (and (not mark-active) + (beacon--movement-> beacon-push-mark)) (let ((head (car mark-ring))) (when (and (eq beacon--previous-mark-head head) (not (equal head beacon--previous-place))) @@ -164,28 +284,43 @@ Only returns `beacon-size' elements." ((not (equal (marker-buffer beacon--previous-place) (current-buffer))) (when beacon-blink-when-buffer-changes - (unless (window-minibuffer-p) - (beacon-blink)))) - ;; Blink for distance movement - ((and (> (abs (- (point) beacon--previous-place)) - beacon-minimum-distance) - (> (count-screen-lines (min (point) beacon--previous-place) - (max (point) beacon--previous-place)) - beacon-minimum-distance)) - (beacon--maybe-push-mark) + (beacon-blink))) + ;; Blink for scrolling. + ((and beacon-blink-when-window-scrolls + beacon--window-scrolled + (equal beacon--window-scrolled (selected-window))) + (beacon-blink) + (setq beacon--window-scrolled nil)) + ;; Blink for movement + ((beacon--movement-> beacon-blink-when-point-moves) (beacon-blink)) ;; Even if we don't blink, vanish any previous beacon. (t (beacon--vanish))) + (beacon--maybe-push-mark) (unless (window-minibuffer-p) (setq beacon--previous-mark-head (car mark-ring)) (setq beacon--previous-place (point-marker)))) +(defun beacon--window-scroll-function (win _start-pos) + "Blink the beacon or record that window has been scrolled. +If invoked during the command loop, record the current window so +that it may be blinked on post-command. This is because the +scrolled window might not be active, but we only know that at +`post-command-hook'. + +If invoked outside the command loop, `post-command-hook' would be +unreliable, so just blink immediately." + (if this-command + (setq beacon--window-scrolled win) + (beacon-blink))) + ;;; Minor-mode -(defcustom beacon-lighter (cond - ((char-displayable-p ?💡) "💡") - ((char-displayable-p ?Λ) "Λ") - (t "*")) +(defcustom beacon-lighter + (cond + ((char-displayable-p ?💡) " 💡") + ((char-displayable-p ?Λ) " Λ") + (t " *")) "Lighter string used on the mode-line." :type 'string) @@ -194,8 +329,13 @@ Only returns `beacon-size' elements." nil nil beacon-lighter nil :global t (if beacon-mode - (add-hook 'post-command-hook #'beacon--post-command) - (remove-hook 'post-command-hook #'beacon--post-command))) + (progn + (add-hook 'window-scroll-functions #'beacon--window-scroll-function) + (add-hook 'post-command-hook #'beacon--post-command) + (add-hook 'pre-command-hook #'beacon--vanish)) + (remove-hook 'window-scroll-functions #'beacon--window-scroll-function) + (remove-hook 'post-command-hook #'beacon--post-command) + (remove-hook 'pre-command-hook #'beacon--vanish))) (provide 'beacon) ;;; beacon.el ends here