Blog

2026-10-05 Emacs Lock mode

Today I only have a short tip. Sometimes you may have an important Emacs buffer you don’t want to kill accidentally. (For example, it might be an Eshell buffer with a long-running process, or a scratch buffer not associated with any file where you have some notes – although that is obviously asking for trouble.) Well, you can say M-x emacs-lock-mode, and Emacs won’t let you kill that buffer (nor quit). You can also provide a prefix argument and decide whether you want to lock killing the buffer, quitting Emacs or both.

And now the fun part. You might think that since you’ve never heard about it (or did you?), it’s some kind of a new feature. Well… no. The kill-or-quit-lock feature was introduced in 2011, and the first, very crude version of the “buffer locking” feature (which only prevented you from accidentally quitting Emacs if at least one buffer is locked) was committed by RMS himself in 1994! I wonder how many more hidden gems like this there are in Emacs…

CategoryEnglish, CategoryBlog, CategoryEmacs

Comments on this page

2026-09-28 How many screens left

One of the things I do in Emacs is reading. Sometimes it is an article copied from the internet using org-web-tools, sometimes it is a long answer to my question to an LLM, and sometimes it is an ebook I’m reading using nov.el. Either way, I often like to know how much I have left. In stock Emacs, there are two indicators of how far I am in the buffer: the scrollbar and the pos string in the modeline. However, both are rather crude. I don’t know exactly how the scrollbar position is computed, but in my Emacs at least it’s perfectly possible to see the bottom of the buffer (which is indicated by the Bot in the modeline) while the scrollbar is not at the bottom at all. On the other hand, the pos string by design tells you what percentage of the buffer is above the first line. If only the last line of your buffer is below the bottom, it can show Top if your buffer has H+1 lines (where H is the window height), 2% if it has H+2 and 49% if it is H*2. (Depending on the actual window heights, these numbers may be different, of course, but they are close to 0 and 50 percent in those cases.) What I need is not “how much of the text I’ve read” (that is, how much is above the top of the window), but rather “how much of the text I haven’t yet read” (that is, how much is below the bottom) – and not as a percentage, but in terms of screenfuls. (If I wanted a percentage, I could use the %P construct, but I would have to subtract it from 100 in my head – not optimal at all.)

Well, Emacs Lisp to the rescue. First of all, how do we know how many lines are below the bottom? Here is my first attempt.

(- (line-number-at-pos (point-max))
   (line-number-at-pos
    (save-excursion
      (move-to-window-line -1)
      (point))))

This is simple, but has one serious drawback: it counts logical lines, not visual lines. Not good. However, there is a function which does what I need - count-screen-lines.

(max 0
     (1- (count-screen-lines
          (save-excursion
            (move-to-window-line -1)
            (point))
          (point-max))))

Let’s unpack it slowly. First, count-screen-lines counts the lines from the last line fully visible on the screen to the end of the buffer – including that last line. This means that we need to subtract 1 to get the number of lines below the bottom. However, if the bottom of the buffer is visible, then it returns 0 – hence the (max 0 ...) part.

Now to count how many screenfuls it is, we need to know how many lines fit on the screen. This is easy – you just call (window-body-height). However, what I actually prefer is (- (window-body-height) next-screen-context-lines). The reason is that what I really want to know is how many times I need to press C-v to get all the way down the buffer.

Last but not least, I need to divide the above two numbers. There is a catch here: / with two (or more) integer arguments is an integer division in Elisp, and it rounds towards zero. What I need is rounding in the opposite direction – after all, if there is two and a half screenfuls below the bottom of the window, I need to press C-v three times to get to the bottom. This means I need a floating point division and a ceiling function.

(ceiling
 (/ (max 0
         (1- (count-screen-lines
              (save-excursion
                (move-to-window-line -1)
                (point))
              (point-max))))
    (- (window-body-height) next-screen-context-lines)
    1.0))

So now that we know how to count the number we need, the only question that remains is how to display it. The easy way is to write a command for this.

(defun count-screenfuls-below-window-bottom (print-message)
  "Count how many presses of \\[scroll-up-command] move to the buffer end."
  (interactive "p")
  (let ((result (ceiling
                 (/ (max 0
                         (1- (count-screen-lines
                              (save-excursion
                                (move-to-window-line -1)
                                (point))
                              (point-max))))
                    (- (window-body-height) next-screen-context-lines)
                    1.0))))
    (if print-message
        (message
         (substitute-command-keys
          "Press \\[scroll-up-command] %s time(s) to move to the buffer end.")
         result)
      result)))

It would be nicer, however, to have this displayed in the modeline. This is slightly risky, though: count-screen-lines can be slow. For example, in the Org buffer I’m writing this blog post now it takes about 0.03 seconds. Not bad if you run it interactively, but very slow if you want it to run on every redisplay (or in post-command-hook). At first I thought I could get away with it – as the manual says:
For efficiency, Emacs does not continuously recompute each window’s mode line and header line. It does so when circumstances appear to call for it--for instance, if you change the window configuration, switch buffers, narrow or widen the buffer, scroll, or modify the buffer.

So, I defined a minor mode which displayed my precious number in the mode line.

;; This code is potentially slowing down Emacs, don't use it!
(defun count-screenfuls--format-mode-line-info ()
  "Format the number of screenfuls left to read."
  (format "[screens left: %s] "
          (count-screenfuls-below-window-bottom nil)))

(define-minor-mode count-screenfuls-mode
  "Toggle a minor mode for counting screenfuls left to read."
  :init-value nil
  :lighter nil
  (if count-screenfuls-mode
      (progn
        (make-local-variable 'mode-line-misc-info)
        (add-to-list
         'mode-line-misc-info
         '(t (count-screenfuls--format-mode-line-info))))
    (setq mode-line-misc-info
          (delete '(t (count-screenfuls--format-mode-line-info))
                  mode-line-misc-info))))

Note that what I needed to add to mode-line-misc=info was a (t ...) structure – it was not obvious to me at first why it is needed, but it is actually a (or maybe even the) correct way.

This worked, but – contrary to the quote above – seemed to be called every time I pressed any key. Let’s make it so the number is only updated after every scrolling command.

;; Warning: This code doesn't update the mode line when
;; a non-scrolling command triggers a scroll
(defvar count-screenfuls--cache nil
  "Formatted number of screenfuls left to read.")

(defun count-screenfuls--update-cache ()
  "Update `count-screenfuls--cache' after scrolling commands."
  (when (get this-command 'scroll-command)
    (message "%s" (car (current-time)))
    (setq count-screenfuls--cache
          (format "[screens left: %s] "
                  (count-screenfuls-below-window-bottom nil)))))

(define-minor-mode count-screenfuls-mode
  "Toggle a minor mode for counting screenfuls left to read."
  :init-value nil
  :lighter nil
  (if count-screenfuls-mode
      (progn
        (count-screenfuls--update-cache)
        (make-local-variable 'mode-line-misc-info)
        (add-to-list
         'mode-line-misc-info
         '(t count-screenfuls--cache))
        (add-hook 'post-command-hook #'count-screenfuls--update-cache nil t))
    (setq mode-line-misc-info
          (delete '(t count-screenfuls--cache)
                  mode-line-misc-info))
    (remove-hook 'post-command-hook #'count-screenfuls--update-cache t)))

As I mentioned in the comment above, this has one serious drawback: if a window was scrolled by a non-scrolling command (for example, C-n when the point was at the bottom of the screen), the update isn’t triggered. Back to the drawing board…

Here is a better way. Let’s record the value of window-start every time we update the counter, and if it changed, let’s perform the new update. I think this should work reliably, although I’m not sure if pixel-scroll-precision-mode won’t mess with it.

(defvar count-screenfuls--cache nil
  "Formatted number of screenfuls left to read.")

(defvar-local count-screenfuls--previous-window-start 0
  "The previous value of `window-start'.")

(defun count-screenfuls--update-cache (_window window-start)
  "Update `count-screenfuls--cache' after a scroll."
  (when (/= window-start count-screenfuls--previous-window-start)
    (setq count-screenfuls--previous-window-start window-start)
    (setq count-screenfuls--cache
          (format "[screens left: %s] "
                  (count-screenfuls-below-window-bottom nil)))))

(define-minor-mode count-screenfuls-mode
  "Toggle a minor mode for counting screenfuls left to read."
  :init-value nil
  :lighter nil
  (if count-screenfuls-mode
      (progn
        (count-screenfuls--update-cache nil (window-start))
        (make-local-variable 'mode-line-misc-info)
        (add-to-list
         'mode-line-misc-info
         '(t count-screenfuls--cache))
        (add-hook 'window-scroll-functions #'count-screenfuls--update-cache nil t))
    (setq mode-line-misc-info
          (delete '(t count-screenfuls--cache)
                  mode-line-misc-info))
    (remove-hook 'window-scroll-functions #'count-screenfuls--update-cache t)))

I’ve been testing this code for some time now, and it seems to work reliably. I’m almost sure it breaks in some weird corner case, but I haven’t been able to break it so far!

That’s it for today, see you next week!

CategoryEnglish, CategoryBlog, CategoryEmacs

Comments on this page

2026-09-21 forward-paragraph in Magit

As I’ve mentioned many times, I am a heavy Magit user. Pretty often I do code reviews from within Magit. There was one thing that has bothered me for a long time.

While I know that Emacs has good support for moving by code units (like forward-sexp and backward-sexp), I am a lazy person and I often use just C-<down> and C-<up> (forward-paragraph and backward-paragraph). Functions I write (or read) are always separated by at least one empty line (and I consider it a bug when they aren’t), and long functions have parts separated by empty lines, too, so this is surprisingly useful. However, it doesn’t work in Magit when I look at diffs, for an obvious reason: most lines in the diff begin with a + or - and so they are considered one giant paragraph.

At first I thought that I could define my own versions of forward-paragraph and backward-paragraph, just for the Magit buffers which can show diffs. But then it occurred to me that I don’t have to – the only thing I have to do is to set the variables paragraph-start and paragraph-separate to suitable values. It’s still not that simple, though. For starters, the default value of paragraph-separate is huge and complicated regex I’d prefer not to analyze. That is easy, though – I can just say

(setq-local paragraph-separate (format "[+-]?\\(?:%s\\)" paragraph-separate))
(setq-local paragraph-start (format "[+-]?\\(?:%s\\)" paragraph-start))

The second problem I had to solve is that I need to so that only in the Magit buffers which can show a diff. Since I use use-package, this is easy:

(defun mbork-set-up-diff-aware-paragraphs ()
  "Make paragraph-moving commands work in a diff."
  (setq-local paragraph-separate (format "[+-]?\\(?:%s\\)" paragraph-separate))
  (setq-local paragraph-start (format "[+-]?\\(?:%s\\)" paragraph-start)))

(use-package magit
  ;; ...
  :hook (magit-mode . mbork-set-up-diff-aware-paragraphs))

And that’s pretty much it! It’s still not ideal – for example, it jumps over @@ lines which start hunks – but things like that are either easy to solve or not worth solving. And from now on, Magit is even more useful to me!

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryGit

Comments on this page

More...

CategoryBlog