Blog

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

2026-09-12 Duplicating lines and other Emacs miscellanea

A few weeks ago I wrote about how I remapped yank to my command which allows to transform the yanked text. The whole setup – where I press C-y twice to trigger the transformation transient – has one drawback: I sometimes legitimately want to call yank twice in a row. This happens when I need to copy the current line and modify the copy so that I have two similar lines in a row. (I often need this when editing Ledger transactions, for example.) What I sometimes do in such a situation is basically a dance of C-a C-1 C-k C-y C-y.

It turns out I don’t need to. There is a duplicate-line command which does exactly what is says on the tin. The only issue is that it is not bound to any key by default. There is, however, one key which is ideally suited for it - C-x C-d. It is normally bound to list-directory, which I find completely useless given that Dired (C-x d) exists.

Before binding duplicate-line to C-x C-d, let’s poke around a bit. This command is almost completely superseded by its cousin duplicate-dwim, which duplicates the current line if the region is inactive and the current region otherwise. Also, there are two options which allow to tweak both commands’ behavior. If you set duplicate-line-final-position to 0 (the default), duplicating the line leaves the point where it was. Setting it to +1 or -1 moves it to the first and last duplicated line, respectively (of course duplicate-line and duplicate-dwim accept a prefix argument!). The duplicate-region-final-position option works in a similar way.

(setq duplicate-line-final-position 1)
(setq duplicate-region-final-position 1)
(bind-key "C-x C-d" #'duplicate-dwim)

That’s not the end of the story, though. While looking around in the file where these commands are defined (lisp/misc.el), I found a few more interesting ones. One of them is copy-from-above-command. I’m not sure how useful it is (and what to bind it to in case I find a use for it), but it’s there if you need it. As its docstring says, it copies characters from the line above, starting above point and until the end of that line. (A prefix argument limits the maximum number of characters copied.) There is zap-up-to-char which works like zap-to-char but does not delete the character it zaps to. There are forward-to-word and backward-to-word, which work very similarly to forward-word and backward-word, but leave the point at the opposite end of the words they move to (so for example forward-to-word moves the point forward until it gets at the beginning of a word). There are a few more, but these I find the most interesting. Maybe I’ll bind them some day – but even if not, they might come in handy with M-x or in Elisp code one day.

That’s it for this time, see you next week!

CategoryEnglish, CategoryBlog, CategoryEmacs

Comments on this page

More...

CategoryBlog