X-Git-Url: https://code.delx.au/gnu-emacs-elpa/blobdiff_plain/02112fa0eab2d9c17a692418f329e071aa53b239..f3b84896eee77611a0b0f487b6d4532111f775e7:/beacon.el diff --git a/beacon.el b/beacon.el index 39354765d..e237c69d1 100644 --- a/beacon.el +++ b/beacon.el @@ -1,12 +1,12 @@ -;;; beacon.el --- Highlight the cursor whenever it moves long distances -*- lexical-binding: t; -*- +;;; beacon.el --- Highlight the cursor whenever the window scrolls -*- 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")) +;; Version: 0.1.1 +;; 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 @@ -23,12 +23,21 @@ ;;; 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 a light will shine on top of your cursor so +;; you know where it is. +;; +;; That’s it. +;; +;; See the accompanying Readme.org for configuration details. ;;; Code: -(require 'cl-lib) +(require 'seq) (defgroup beacon nil "Customization group for beacon." @@ -37,7 +46,7 @@ (defvar beacon--timer nil) -(defcustom beacon-push-mark nil +(defcustom beacon-push-mark 35 "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 @@ -59,6 +68,10 @@ movement distance (in lines) that triggers a beacon blink." "Should the beacon blink when the window scrolls?" :type 'boolean) +(defcustom beacon-blink-when-window-changes t + "Should the beacon blink when the window changes?" + :type 'boolean) + (defcustom beacon-blink-duration 0.3 "Time, in seconds, that the blink should last." :type 'number) @@ -83,39 +96,88 @@ If it is a string, it is a color name or specification, e.g. \"#666600\"." :type '(choice number color)) +(defvar 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. + +For instance, if you want to disable beacon on buffers where +`hl-line-mode' is on, you can do: + + (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 '(magit-status-mode) + "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)) + +(defcustom beacon-dont-blink-commands '(recenter-top-bottom) + "A list of commands that should not make the beacon blink. +Use this for commands that scroll the window in very +predictable ways, when the blink would be more distracting +than helpful.." + :type '(repeat symbol)) + ;;; Overlays (defvar beacon--ovs nil) -(defun beacon--colored-overlay (color) +(defconst beacon-overlay-priority (/ most-positive-fixnum 2) + "Priotiy used on all of our overlays.") + +(defun beacon--make-overlay (length &rest properties) "Put an overlay at point with background COLOR." - (let ((ov (make-overlay (point) (1+ (point))))) - (overlay-put ov 'face (list :background color)) + (let ((ov (make-overlay (point) (+ length (point))))) (overlay-put ov 'beacon t) - (push ov beacon--ovs))) + ;; Our overlay is very temporary, so we take the liberty of giving + ;; it a high priority. + (overlay-put ov 'priority beacon-overlay-priority) + (overlay-put ov 'window (selected-window)) + (while properties + (overlay-put ov (pop properties) (pop properties))) + (push ov beacon--ovs) + ov)) + +(defun beacon--colored-overlay (color) + "Put an overlay at point with background COLOR." + (beacon--make-overlay 1 'face (list :background color))) (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." - (overlay-put overlay 'beacon-colors colors) - (overlay-put overlay 'after-string - (mapconcat (lambda (c) (propertize " " 'face (list :background c))) - colors - ""))) +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 '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)))) - (beacon--ov-put-after-string ov colors) - (overlay-put ov 'beacon t) - (push ov beacon--ovs))) + ;; The after-string must not be longer than the remaining columns + ;; from point to right window-end else it will be wrapped around. + (let ((colors (seq-take colors (- (window-width) (current-column))))) + (beacon--ov-put-after-string (beacon--make-overlay 0) colors))) (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." @@ -164,35 +226,49 @@ Only returns `beacon-size' elements." (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))) - (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)))))))) + (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 beacon-size) - #'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) + (memq (or this-command last-command) beacon-dont-blink-commands)) + (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-window-start nil) (defvar beacon--previous-mark-head nil) +(defvar beacon--previous-window nil) + +(defun beacon--pos-on-current-line-p (pos) + "Return non-nil if POS is on the current line." + (<= (save-excursion (beginning-of-line) (point)) + pos + (save-excursion (end-of-line) (point)))) (defun beacon--movement-> (delta) "Return non-nil if latest point movement is > DELTA. @@ -203,9 +279,21 @@ If DELTA is nil, return nil." (current-buffer)) (> (abs (- (point) beacon--previous-place)) delta) - (> (count-screen-lines (min (point) beacon--previous-place) - (max (point) beacon--previous-place)) - delta))) + ;; Check if the movement was larger than DELTA lines by testing if + ;; `point' is still on the same line or on any line DELTA lines up or + ;; down. This is much cheaper than computing the actual number of lines + ;; moved using `count-screen-lines'. + (let ((prev-pos (marker-position beacon--previous-place))) + (catch 'movement + (when (beacon--pos-on-current-line-p (point)) + (throw 'movement nil)) + (dolist (inc '(1 -1)) + (save-excursion + (dotimes (i delta) + (vertical-motion inc) + (when (beacon--pos-on-current-line-p (point)) + (throw 'movement nil))))) + (throw 'movement t))))) (defun beacon--maybe-push-mark () "Push mark if it seems to be safe." @@ -221,16 +309,14 @@ If DELTA is nil, return nil." (cond ((not (markerp beacon--previous-place)) (beacon--vanish)) - ;; Blink because we changed buffer. - ((not (equal (marker-buffer beacon--previous-place) - (current-buffer))) - (when beacon-blink-when-buffer-changes - (unless (window-minibuffer-p) - (beacon-blink)))) + ;; Blink for switching windows. + ((and beacon-blink-when-window-changes + (not (eq beacon--previous-window (selected-window)))) + (beacon-blink)) ;; Blink for scrolling. ((and beacon-blink-when-window-scrolls - (progn (redisplay) - (not (equal beacon--previous-window-start (window-start))))) + beacon--window-scrolled + (equal beacon--window-scrolled (selected-window))) (beacon-blink)) ;; Blink for movement ((beacon--movement-> beacon-blink-when-point-moves) @@ -238,17 +324,33 @@ If DELTA is nil, return nil." ;; Even if we don't blink, vanish any previous beacon. (t (beacon--vanish))) (beacon--maybe-push-mark) + (setq beacon--window-scrolled nil) (unless (window-minibuffer-p) - (setq beacon--previous-window-start (window-start)) (setq beacon--previous-mark-head (car mark-ring)) - (setq beacon--previous-place (point-marker)))) + (setq beacon--previous-place (point-marker)) + (setq beacon--previous-window (selected-window)))) + +(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) + (setq beacon--window-scrolled nil) + (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) @@ -257,8 +359,13 @@ If DELTA is nil, return nil." 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