Showing with 49 additions and 38 deletions.
  1. +27 −3 README.md
  2. +22 −35 topspace.el
30 changes: 27 additions & 3 deletions README.md
@@ -1,20 +1,44 @@
# TopSpace

[![MELPA](http://melpa.org/packages/topspace-badge.svg)](http://melpa.org/#/topspace)
[![MELPA Stable](http://stable.melpa.org/packages/topspace-badge.svg)](http://stable.melpa.org/#/topspace)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)

**Scroll above the top line**

![topspace](https://user-images.githubusercontent.com/12535207/154770200-0b3edcd8-8036-40c7-910f-d5b3a1c3b4df.gif)
![topspace](https://user-images.githubusercontent.com/12535207/155176914-87390537-10f0-4ee5-9b37-cd798f07df27.gif)

TopSpace is an Emacs minor mode that lets you scroll above the top line to vertically center top text or cursor with a scrollable top margin/padding. In particular, it is useful when using Emacs in full-screen/on large monitors.

TopSpace is an Emacs minor mode that lets you scroll above the top line to vertically center top text.
### Just install and go:

No new keybindings are required as `topspace` automatically works for any
commands or subsequent function calls which use `scroll-up`, `scroll-down`,
or `recenter` as the underlying primitives for scrolling. This includes all
scrolling commands/functions available in Emacs as far as the author is aware.

### How it works:

The top "margin" is created by drawing an [overlay](https://www.gnu.org/software/emacs/manual/html_node/elisp/Overlays.html) before window-start which contains nothing but newline characters. As you scroll, more newline characters are added or removed accordingly.

Bonus: If you use [`centered-cursor-mode`][1], this means that `topspace` will automatically let you center the cursor all the way to the top line!

# Customization
# Installation

TopSpace is available on [MELPA](http://melpa.org).
After [installing MELPA](https://melpa.org/#/getting-started) you can install TopSpace with the following command:

<kbd>M-x</kbd> `package-install` <kbd>[RET]</kbd> `topspace` <kbd>[RET]</kbd>

If `topspace` did not appear here in `package-install` try running <kbd>M-x</kbd> `package-refresh-contents` and repeating the above step. Then enable TopSpace locally with <kbd>M-x</kbd> `topspace-mode`, or globally with <kbd>M-x</kbd> `global-topspace-mode`.
Alternatively, add `(global-topspace-mode 1)` to your Emacs config to enable `topspace-mode` globally on startup.

# Customization
By default, small buffers will be vertically centered with top space when first opened. To disable this feature, simply add the following to your Emacs config:
```
(custom-set-variables '(topspace-autocenter-buffers nil))
```
More options/detail:
```
(defcustom topspace-autocenter-buffers
t
Expand Down
57 changes: 22 additions & 35 deletions topspace.el
@@ -1,11 +1,11 @@
;;; topspace.el --- Scroll above the top line to vertically center top text -*- lexical-binding: t -*-
;;; topspace.el --- Scroll above the top line to vertically center top text or cursor with a scrollable top margin/padding -*- lexical-binding: t -*-

;; Copyright (C) 2021-2022 Trevor Edwin Pogue

;; Author: Trevor Edwin Pogue <trevor.pogue@gmail.com>
;; Maintainer: Trevor Edwin Pogue <trevor.pogue@gmail.com>
;; URL: https://github.com/trevorpogue/topspace
;; Keywords: convenience, scrolling, center, margin, padding
;; Keywords: convenience, scrolling, center, cursor, margin, padding
;; Version: 0.1.0
;; Package-Requires: ((emacs "25.1"))

Expand All @@ -23,10 +23,10 @@
;; along with this program. If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:
;; Scroll above the top line to vertically center top text.
;; Gives the effect of having blank space/padding/margin being automatically
;; drawn above the top text line using overlays as you scroll above,
;; giving the equivalent effect of being able to scroll above the top line.
;; Scroll above the top line to vertically center top text or cursor with a
;; scrollable top margin/padding. An overlay is automatically drawn above the
;; top text line as you scroll above, giving the equivalent effect of being able
;; to scroll above the top line.

;; No new keybindings are required as topspace automatically works for any
;; commands or subsequent function calls which use `scroll-up', `scroll-down',
Expand Down Expand Up @@ -84,7 +84,6 @@ space should be reduced in size or not")
(defcustom topspace-autocenter-buffers
t
"Vertically center small buffers when first opened or window sizes change.
This is done by automatically calling `topspace-recenter-buffer',
which adds enough top space to center small buffers.
Top space will not be added if the number of text lines in the buffer is larger
Expand All @@ -97,20 +96,17 @@ Customize `topspace-center-position' to adjust the centering position."
0.5
"Target position when centering buffers as a ratio of frame height.
A value from 0 to 1 where lower values center buffers higher up in the screen.
Used in `topspace-recenter-buffer' when called or when opening/resizing buffers
if `topspace-autocenter-buffers' is non-nil."
:group 'topspace
:type 'float)

(defcustom topspace-mode-line " T"
"Mode line lighter for Topspace.
The value of this variable is a mode line template as in
`mode-line-format'. See Info Node `(elisp)Mode Line Format' for
more information. Note that it should contain a _single_ mode
line construct only.
Set this variable to nil to disable the mode line completely."
:group 'topspace
:type 'sexp)
Expand Down Expand Up @@ -155,15 +151,14 @@ TOTAL-LINES is used in the same way as in `scroll-up'."
(defun topspace--after-scroll (&optional total-lines)
"Run after `scroll-up'/`scroll-down' for scrolling above the top line.
TOTAL-LINES is used in the same way as in `scroll-down'.
This is needed when scrolling down (moving buffer text lower in the screen)
and no top space was present before scrolling but it should be after scrolling.
The reason this is needed is because `topspace--put' only draws the overlay when
`window-start` equals 1, which can only be true after the scroll command is run
in the described case above."
(setq total-lines topspace--total-lines-scrolling)
(when (and (> topspace--window-start-before-scroll 1) (= (window-start) 1))
(let ((lines-already-scrolled (topspace--count-lines
(let ((lines-already-scrolled (count-screen-lines
1 topspace--window-start-before-scroll)))
(setq total-lines (abs total-lines))
(set-window-start (selected-window) 1)
Expand All @@ -177,11 +172,11 @@ LINE-OFFSET and REDISPLAY are used in the same way as in `recenter'."
redisplay ; remove flycheck warning for unused argument (see above)
(when (= (window-start) 1)
(unless line-offset
(setq line-offset (round (/ (topspace--window-height) line-offset))))
(setq line-offset (round (/ (topspace--window-height) 2))))
(when (< line-offset 0)
(setq line-offset (- (topspace--window-height) line-offset)))
(topspace--put (- line-offset (topspace--count-lines (window-start)
(point))))))
(topspace--put (- line-offset (topspace--count-screen-lines (window-start)
(point))))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Top space line height calculation
Expand Down Expand Up @@ -210,7 +205,6 @@ If no previous value exists, return the appropriate value to

(defun topspace--correct-height (height)
"Return HEIGHT if a valid top space line height, else a valid value.
Valid top space line heights are:
- never negative,
- only positive when `window-start' equals 1,
Expand All @@ -232,12 +226,12 @@ Any value above 0 flags that the target TOPSPACE-HEIGHT is too large."
(defun topspace--current-line-plus-topspace (&optional topspace-height)
"Used when making sure top space height does not push cursor off-screen.
Return the current line plus the top space height TOPSPACE-HEIGHT."
(+ (topspace--count-lines (window-start) (point))
(+ (count-screen-lines (window-start) (point))
(or topspace-height (topspace--height))))

(defun topspace--height-to-make-buffer-centered ()
"Return the necessary top space height to center selected window's buffer."
(let ((buffer-height (topspace--count-lines (window-start) (window-end)))
(let ((buffer-height (count-screen-lines (window-start) (window-end)))
(result)
(window-height (topspace--window-height)))
(setq result (- (- (topspace--center-frame-line)
Expand All @@ -257,7 +251,6 @@ which must be accounted for in the calling functions."

(defun topspace--recenter-buffers-p ()
"Return non-nil if buffer is allowed to be auto-centered.
Buffers will not be auto-centered if `topspace-autocenter-buffers' is nil
or if the selected window is in a child-frame."
(and topspace-autocenter-buffers
Expand All @@ -270,14 +263,14 @@ or if the selected window is in a child-frame."
"Return the number of screen lines in the selected window rounded up."
(ceiling (window-screen-lines)))

(defun topspace--count-lines (start end)
(defun topspace--count-screen-lines (start end)
"Return screen lines between START and END.
Like `count-screen-lines' except `count-screen-lines' will
return unexpected value when END is in column 0. This fixes that issue."
(let ((adjustment 0) (column))
(let ((adjustment 0) (column 0))
(save-excursion
(goto-char end)
(setq column (mod (current-column) (window-text-width)))
(setq column (car (nth 6 (posn-at-point))))
(unless (= column 0) (setq adjustment -1)))
(+ (count-screen-lines start end) adjustment)))

Expand Down Expand Up @@ -366,10 +359,8 @@ return unexpected value when END is in column 0. This fixes that issue."
;;;###autoload
(defun topspace-recenter-buffer ()
"Add enough top space in the selected window to center small buffers.
Top space will not be added if the number of text lines in the buffer is larger
than or close to the selected window's height.
Customize `topspace-center-position' to adjust the centering position.
Customize `topspace-autocenter-buffers' to run this command automatically
after first opening buffers and after window sizes change."
Expand All @@ -384,9 +375,7 @@ after first opening buffers and after window sizes change."

(defun topspace--enable-p ()
"Return non-nil if buffer is allowed to enable `topspace-mode.'.
Topspace will not be enabled for:
- minibuffers
- ephemeral buffers (See Info node `(elisp)Buffer Names')
- if variable `topspace-mode' is already enabled"
Expand All @@ -403,7 +392,9 @@ Topspace will not be enabled for:
#'topspace--filter-args-scroll-down)
(advice-add #'scroll-up :after #'topspace--after-scroll)
(advice-add #'scroll-down :after #'topspace--after-scroll)
(advice-add #'recenter :after #'topspace--after-recenter)))
(advice-add #'recenter :after #'topspace--after-recenter)
(dolist (window (get-buffer-window-list))
(with-selected-window window (topspace--put)))))

(defun topspace--disable ()
"Disable variable `topspace-mode' if already enabled, else do nothing."
Expand All @@ -419,20 +410,16 @@ Topspace will not be enabled for:

;;;###autoload
(define-minor-mode topspace-mode
"Scroll above the top line to vertically center top text.
Gives the effect of having blank space/padding/margin being automatically
drawn above the top text line using overlays as you scroll above,
giving the equivalent effect of being able to scroll above the top line.
"Scroll above the top line to vertically center top text or cursor.
It is like having a scrollable top margin/padding.
An overlay is automatically drawn above the top text line as you scroll above,
giving the effect of being able to scroll above the top line.
No new keybindings are required as topspace automatically works for any
commands or subsequent function calls which use `scroll-up', `scroll-down',
or `recenter' as the underlying primitives for scrolling. This includes all
scrolling commands/functions available in Emacs as far as the author is aware.
When called interactively, toggle variable `topspace-mode'. With prefix
ARG, enable variable `topspace-mode' if ARG is positive, otherwise disable it.
When called from Lisp, enable variable `topspace-mode' if ARG is omitted,
nil or positive. If ARG is `toggle', toggle variable `topspace-mode'.
Otherwise behave as if called interactively."
Expand Down