(redirected from Homepage)

Strona domowa

Witam na mojej prywatnej stronie internetowej!

[If this is all Polish to you, click here: English]

Uwaga: z oczywistych powodów nie mogę zagwarantować swojej nieomylności, choć staram się o zgodność tego, co piszę, z Prawdą. Jest również oczywiste, że nie gwarantuję takiej zgodności w przypadku komentarzy. Umieszczenie linku do strony spoza niniejszego serwisu nie musi oznaczać, że podzielam poglądy autora tej strony, a jedynie, że uważam ją za wartościową z takich czy innych powodów.

Marcin ‘mbork’ Borkowski

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

More...