Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- New `ghostel-line-spacing` customization pads terminal rows with extra
vertical space (integer pixels or a float fraction of the line height).
The terminal grid and reported cell pixel sizes follow the taller rows;
changing it live resizes open terminals. Note that Emacs draws no content
into the spacing strip, so non-zero values leave gaps in box-drawing
borders, block characters, and between kitty-graphics image rows.

## [0.43.0] — 2026-07-11

### Added
Expand Down
21 changes: 11 additions & 10 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -1285,16 +1285,17 @@ tables below group them by area; defaults shown are the out-of-the-box values.

** Rendering and performance

| Variable | Default | Description |
|-----------------------------------+---------+--------------------------------------------------------------------------------------------------------------------------------------|
| =ghostel-max-scrollback= | 5 MB | Max scrollback in bytes (materialized into the buffer; ~5,000 rows at 80 cols). |
| =ghostel-timer-delay= | =0.033= | Base redraw delay in seconds (~30 fps). |
| =ghostel-adaptive-fps= | =t= | Adaptive frame rate (shorter delay after idle, stop timer when idle). |
| =ghostel-immediate-redraw-interval= | =0.05= | Max seconds since last keystroke for immediate redraw. |
| =ghostel-inhibit-redraw-functions= | =nil= | Abnormal hook (each fn called with the buffer); if any returns non-nil the redraw is deferred. Used by add-ons such as =ghostel-ime=. |
| =ghostel-inhibit-anchor-functions= | =nil= | Abnormal hook (each fn called with WINDOW and FORCE); non-nil skips anchoring that window. Used by add-ons such as =evil-ghostel=. |
| =ghostel-cell-pixel-scale= | =auto= | Physical:logical pixel ratio for cell-size reporting (=auto= derives from DPI). |
| =ghostel-glyph-scale-floor= | =0.0= | Minimum scale for glyphs that don't fit the cell (=0.0= preserves grid alignment; =1.0= renders CJK at natural size). |
| Variable | Default | Description |
|-----------------------------------+---------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| =ghostel-max-scrollback= | 5 MB | Max scrollback in bytes (materialized into the buffer; ~5,000 rows at 80 cols). |
| =ghostel-timer-delay= | =0.033= | Base redraw delay in seconds (~30 fps). |
| =ghostel-adaptive-fps= | =t= | Adaptive frame rate (shorter delay after idle, stop timer when idle). |
| =ghostel-immediate-redraw-interval= | =0.05= | Max seconds since last keystroke for immediate redraw. |
| =ghostel-inhibit-redraw-functions= | =nil= | Abnormal hook (each fn called with the buffer); if any returns non-nil the redraw is deferred. Used by add-ons such as =ghostel-ime=. |
| =ghostel-inhibit-anchor-functions= | =nil= | Abnormal hook (each fn called with WINDOW and FORCE); non-nil skips anchoring that window. Used by add-ons such as =evil-ghostel=. |
| =ghostel-cell-pixel-scale= | =auto= | Physical:logical pixel ratio for cell-size reporting (=auto= derives from DPI). |
| =ghostel-glyph-scale-floor= | =0.0= | Minimum scale for glyphs that don't fit the cell (=0.0= preserves grid alignment; =1.0= renders CJK at natural size). |
| =ghostel-line-spacing= | =0= | Extra vertical space below each row (int = pixels, float = fraction of line height). Non-zero leaves gaps in box-drawing and block glyphs and between kitty image rows. |

** Images

Expand Down
32 changes: 30 additions & 2 deletions lisp/ghostel.el
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,24 @@ natural size, potentially making rows slightly taller and cells slightly wider."
:type '(float 0.0 1.0)
:local t)

(defcustom ghostel-line-spacing 0
"Additional vertical space below each terminal row.
An integer is extra pixels; a float is a fraction of the default line height.
Applied buffer-locally as `line-spacing'. The cons form of `line-spacing' is
not supported. Non-zero values leave gaps in everything that tiles vertically:
box-drawing borders, block characters and kitty graphics image rows."
:type '(choice (integer :tag "Extra pixels below each line")
(float :tag "Fraction of default line height"))
:initialize #'custom-initialize-default
:set (lambda (sym val)
(set-default sym val)
(dolist (buf (buffer-list))
(with-current-buffer buf
(when (derived-mode-p 'ghostel-mode)
(setq-local line-spacing val)
(dolist (window (get-buffer-window-list buf nil t))
(ghostel--adjust-size window t)))))))

(defcustom ghostel-timer-delay 0.033
"Delay in seconds before redrawing after output (roughly 30fps).
When `ghostel-adaptive-fps' is non-nil, this serves as the base
Expand Down Expand Up @@ -4851,9 +4869,19 @@ reported (some multi-monitor setups), letting the caller fall back."
"Return cell width to report to libghostty, in physical pixels."
(round (* (frame-char-width) (ghostel--cell-pixel-scale))))

(defun ghostel--cell-height ()
"Return the terminal cell height in logical pixels.
`frame-char-height' plus the buffer's `line-spacing' in pixels."
(+ (frame-char-height)
(cond ((not (display-graphic-p)) 0)
((integerp line-spacing) (max 0 line-spacing))
((floatp line-spacing)
(max 0 (round (* line-spacing (frame-char-height)))))
(t 0))))

(defun ghostel--reported-cell-height ()
"Return cell height to report to libghostty, in physical pixels."
(round (* (frame-char-height) (ghostel--cell-pixel-scale))))
(round (* (ghostel--cell-height) (ghostel--cell-pixel-scale))))

(defun ghostel--set-size-with-cell-dims (term rows cols)
"Resize TERM to ROWS×COLS, including the reported cell pixel dimensions.
Expand Down Expand Up @@ -5017,7 +5045,7 @@ for both native and Emacs PTY paths."
(setq-local hscroll-margin 0)
(setq-local truncate-lines t)
(setq-local scroll-conservatively 101)
(setq-local line-spacing 0)
(setq-local line-spacing ghostel-line-spacing)
;; Shield row geometry from a global `default-text-properties':
;; `line-spacing'/`line-height' properties supplied through its fallback
;; inflate rendered rows invisibly to the `window-screen-lines' sizing math.
Expand Down
54 changes: 54 additions & 0 deletions test/ghostel-modes-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,60 @@ invisibly to the grid sizing math (discussion #534)."
(should (null (get-char-property (point-min) 'line-height)))))
(setq-default default-text-properties old))))

(ert-deftest ghostel-test-mode-applies-line-spacing-custom ()
"`ghostel-line-spacing' seeds the buffer-local `line-spacing'."
(let ((ghostel-line-spacing 3))
(with-temp-buffer
(ghostel-mode)
(should (eql line-spacing 3)))))

(ert-deftest ghostel-test-line-spacing-set-updates-live-buffers ()
"The `ghostel-line-spacing' setter updates live terminals only.
Ghostel buffers get the new buffer-local `line-spacing' and displayed
windows a forced size adjustment; other buffers are untouched."
(let ((setter (get 'ghostel-line-spacing 'custom-set))
(old (default-value 'ghostel-line-spacing))
(calls nil))
(with-temp-buffer
(ghostel-mode)
(let ((ghostel-buf (current-buffer)))
(with-temp-buffer
(let ((plain-buf (current-buffer)))
(set-window-buffer (selected-window) ghostel-buf)
(unwind-protect
(cl-letf (((symbol-function 'ghostel--adjust-size)
(lambda (window force) (push (cons window force) calls))))
(funcall setter 'ghostel-line-spacing 4)
(should (eql (buffer-local-value 'line-spacing ghostel-buf) 4))
(should-not (buffer-local-value 'line-spacing plain-buf))
(should (equal calls
(list (cons (get-buffer-window ghostel-buf) t)))))
(funcall setter 'ghostel-line-spacing old))))))))

(ert-deftest ghostel-test-cell-height-includes-line-spacing ()
"`ghostel--cell-height' adds the buffer's `line-spacing' in pixels.
Integer spacing adds as-is, float spacing as a rounded fraction of
`frame-char-height'; unsupported values and non-graphic displays add
nothing."
(cl-letf (((symbol-function 'frame-char-height) (lambda (&optional _) 14))
((symbol-function 'display-graphic-p) (lambda (&optional _) t)))
(with-temp-buffer
(setq-local line-spacing 3)
(should (eql (ghostel--cell-height) 17))
(setq-local line-spacing 0.1)
(should (eql (ghostel--cell-height) 15))
(setq-local line-spacing 0)
(should (eql (ghostel--cell-height) 14))
;; Emacs 28/29 reject non-number values at `setq-local' time
;; (C-level `numberp' check), so nil is the only portable way to
;; exercise the fallback branch.
(setq-local line-spacing nil)
(should (eql (ghostel--cell-height) 14))
(setq-local line-spacing 3)
(cl-letf (((symbol-function 'display-graphic-p)
(lambda (&optional _) nil)))
(should (eql (ghostel--cell-height) 14))))))

(ert-deftest ghostel-test-emacs-mode-preserves-point ()
"In Emacs mode, point stays put while the terminal keeps running.
The delayed redraw path always preserves point in Emacs mode,
Expand Down
Loading