aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-29 22:07:45 -0400
committerCraig Jennings <c@cjennings.net>2026-06-29 22:07:45 -0400
commit659197c7b2518bbea7f8c8f20f8970e9fdaa218b (patch)
tree587ab287c12497566be0dfe8d90ba60696bce8df /modules
parenta200947f67df4e906ba3a2b40cc0be056a2112e7 (diff)
downloaddotemacs-659197c7b2518bbea7f8c8f20f8970e9fdaa218b.tar.gz
dotemacs-659197c7b2518bbea7f8c8f20f8970e9fdaa218b.zip
feat(nov): reading-view theme layer with palettes and font sizing
EPUB reading prefs were scattered: a hardcoded Merriweather/180 font-remap in calibredb-epub-config's nov hook, no color control (the old sepia foreground had been stripped), and a frame-global EBook fontaine preset as the only way to size up. That preset resized the font in every buffer in the frame, not just the book. I pulled the reading view into its own layer, modules/nov-reading.el, on top of stock nov (no fork). It owns three things, all buffer-local: a reading palette (sepia/dark/light, each a face the dupre theme owns, sepia the default), the serif typography (family plus a defcustom base height replacing the hardcoded 180), and page font sizing (+/- bump the size live, = resets to the base). Width moves to { }. calibredb-epub-config keeps the library and width/centering layout. Its nov hook now calls into the layer. The three palette faces register as a nov-reading app in theme-studio (face_data.py), so they're tunable there like any other app. I dropped the EBook fontaine preset, since reading size is buffer-local now.
Diffstat (limited to 'modules')
-rw-r--r--modules/calibredb-epub-config.el25
-rw-r--r--modules/font-config.el5
-rw-r--r--modules/nov-reading.el176
3 files changed, 190 insertions, 16 deletions
diff --git a/modules/calibredb-epub-config.el b/modules/calibredb-epub-config.el
index a8b131be8..b03d83ed0 100644
--- a/modules/calibredb-epub-config.el
+++ b/modules/calibredb-epub-config.el
@@ -31,6 +31,8 @@
(declare-function nov-render-document "nov" ())
(defvar nov-text-width) ; from nov.el; set buffer-local here
+(require 'nov-reading) ;; reading-view theme layer: palettes + typography + size
+
;; calibredb commands the curated menu drives (all autoloaded by calibredb)
(declare-function calibredb-switch-library "calibredb" ())
(declare-function calibredb-search-keyword-filter "calibredb-search")
@@ -314,12 +316,8 @@ A positive DELTA narrows the text column; a negative DELTA widens it."
(defun cj/nov-apply-preferences ()
"Apply preferences after nov-mode has launched."
(interactive)
- ;; Use Merriweather for comfortable reading with appropriate scaling.
- ;; (Reading fg color stripped; falls back to the theme default until a
- ;; themeable reading face exists -- see todo.org.)
- (face-remap-add-relative 'variable-pitch :family "Merriweather" :height 1.0)
- (face-remap-add-relative 'default :family "Merriweather" :height 180)
- (face-remap-add-relative 'fixed-pitch :height 180)
+ ;; Reading typography + color palette live in the nov-reading theme layer.
+ (cj/nov-reading-setup)
;; Enable visual-line-mode for proper text wrapping
(visual-line-mode 1)
;; Set fill-column as a fallback
@@ -428,14 +426,19 @@ Try to use the Calibre book id from the parent folder name (for example,
("<" . nov-history-back)
(">" . nov-history-forward)
("," . backward-paragraph)
- ;; +/= widen the text column, -/_ narrow it (50%..100% of the window)
- ("+" . cj/nov-widen-text)
- ("=" . cj/nov-widen-text)
- ("-" . cj/nov-narrow-text)
- ("_" . cj/nov-narrow-text)
+ ;; +/- adjust the page font size, = resets it to the default height
+ ("+" . cj/nov-reading-text-bigger)
+ ("-" . cj/nov-reading-text-smaller)
+ ("=" . cj/nov-reading-text-reset)
+ ;; { } adjust the text-column width (50%..100% of the window)
+ ("}" . cj/nov-widen-text)
+ ("{" . cj/nov-narrow-text)
;; open current EPUB with zathura (same key in pdf-view)
("z" . cj/nov-open-external)
("t" . nov-goto-toc)
+ ;; c cycles reading palettes (sepia/dark/light/none); C picks one by name
+ ("c" . cj/nov-cycle-reading-palette)
+ ("C" . cj/nov-set-reading-palette)
("C-c C-b" . cj/nov-jump-to-calibredb)))
;; ------------------------- Nov bookmark naming -------------------------------
diff --git a/modules/font-config.el b/modules/font-config.el
index 095b4c8c1..3aa3d80f6 100644
--- a/modules/font-config.el
+++ b/modules/font-config.el
@@ -83,11 +83,6 @@
(FiraCode-Literata
:default-family "Fira Code Nerd Font"
:variable-pitch-family "Literata")
- (EBook
- :default-family "Lexend"
- :default-weight regular
- :default-height 200
- :variable-pitch-family "Lexend")
(24-point-font
:default-height 240)
(20-point-font
diff --git a/modules/nov-reading.el b/modules/nov-reading.el
new file mode 100644
index 000000000..0181b03a0
--- /dev/null
+++ b/modules/nov-reading.el
@@ -0,0 +1,176 @@
+;;; nov-reading.el --- Reading-view theme layer for nov-mode EPUBs -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+;;
+;; Layer: 4 (Added features).
+;; Category: O (optional commands + faces).
+;; Load shape: eager.
+;; Eager reason: defines the reading faces and commands the nov launch hook and
+;; keymap reference; the faces must exist for theme-studio's inventory too.
+;; Top-level side effects: defface x3, defcustoms, a defgroup.
+;; Runtime requires: none (face-remap and text-scale are built in).
+;; Direct test load: yes.
+;;
+;; A small theme layer on top of the stock `nov' package (no fork): how an EPUB
+;; *reads*, kept buffer-local so it never disturbs the frame or other buffers.
+;; Two knobs:
+;;
+;; - Reading palette -- the background + foreground, as sepia / dark / light,
+;; each a face the dupre theme / theme-studio own (registered as the
+;; "nov-reading" bespoke app in theme-studio's face_data.py).
+;; - Typography -- a serif family and a base height, with +/-/= adjusting the
+;; page font size live via a buffer-local text-scale on top of the base.
+;;
+;; calibredb-epub-config.el owns the library/calibre side and the text-width /
+;; centering layout; this module owns reading color and typography. Its launch
+;; entry point `cj/nov-reading-setup' is called from that module's nov-mode hook.
+
+;;; Code:
+
+(defgroup cj/nov-reading nil
+ "Reading-view theming for nov-mode EPUBs."
+ :group 'cj)
+
+;; ----------------------------- Reading palettes ------------------------------
+;; nov renders through shr and defines no faces, so a palette is a buffer-local
+;; face-remap of `default'. Each palette is one face carrying a :background and
+;; :foreground, so the theme owns the real colors (the hex defaults here are a
+;; starting point to tune in theme-studio).
+
+(defface cj/nov-reading-sepia
+ '((t :background "#1f1b16" :foreground "#c9b187"))
+ "Sepia reading palette for nov-mode: warm dark background, tan text."
+ :group 'cj/nov-reading)
+
+(defface cj/nov-reading-dark
+ '((t :background "#15140f" :foreground "#cfc8b8"))
+ "Dark reading palette for nov-mode: near-black background, light-gray text."
+ :group 'cj/nov-reading)
+
+(defface cj/nov-reading-light
+ '((t :background "#ece3cf" :foreground "#2a2622"))
+ "Light reading palette for nov-mode: cream background, near-black text."
+ :group 'cj/nov-reading)
+
+(defcustom cj/nov-reading-palettes
+ '(("sepia" . cj/nov-reading-sepia)
+ ("dark" . cj/nov-reading-dark)
+ ("light" . cj/nov-reading-light))
+ "Alist of reading-palette NAME -> face for nov-mode.
+Each face supplies the reading view's :background and :foreground; the selector
+and cycle commands choose among these names. Add an entry to add a palette."
+ :type '(alist :key-type string :value-type face)
+ :group 'cj/nov-reading)
+
+(defcustom cj/nov-reading-default-palette "sepia"
+ "Reading palette applied to a fresh nov-mode buffer.
+A key in `cj/nov-reading-palettes', or nil for the theme's normal rendering."
+ :type '(choice (const :tag "None (theme default)" nil) string)
+ :group 'cj/nov-reading)
+
+(defvar-local cj/nov--reading-remap-cookie nil
+ "The `face-remap-add-relative' cookie for the active reading palette, or nil.")
+
+(defvar-local cj/nov--reading-palette nil
+ "Name of the reading palette active in this buffer, or nil for none.")
+
+(defun cj/nov--reading-palette-face (name)
+ "Return the face for palette NAME, or nil when NAME is nil or unknown."
+ (cdr (assoc name cj/nov-reading-palettes)))
+
+(defun cj/nov--next-reading-palette (current names)
+ "Return the palette after CURRENT in the cycle NAMES then nil, wrapping.
+CURRENT nil is the no-palette state, and a returned nil means no palette. An
+unknown CURRENT falls back to the first palette."
+ (let* ((cycle (append names (list nil)))
+ (tail (cdr (member current cycle))))
+ (car (or tail cycle))))
+
+(defun cj/nov--apply-reading-palette (name)
+ "Apply reading palette NAME buffer-local; NAME nil removes any palette.
+Removes the previous palette remap first so switching never stacks remaps, and
+leaves the typography remap (a separate `default' remap) untouched."
+ (when cj/nov--reading-remap-cookie
+ (face-remap-remove-relative cj/nov--reading-remap-cookie)
+ (setq cj/nov--reading-remap-cookie nil))
+ (let ((face (cj/nov--reading-palette-face name)))
+ (when face
+ (setq cj/nov--reading-remap-cookie
+ (face-remap-add-relative 'default face)))
+ (setq cj/nov--reading-palette (and face name))))
+
+(defun cj/nov-set-reading-palette (name)
+ "Choose reading palette NAME for this nov buffer; \"none\" clears it.
+Interactively prompts among `cj/nov-reading-palettes' plus \"none\"."
+ (interactive
+ (list (completing-read "Reading palette: "
+ (cons "none" (mapcar #'car cj/nov-reading-palettes))
+ nil t)))
+ (unless (derived-mode-p 'nov-mode)
+ (user-error "Not in a nov-mode buffer"))
+ (cj/nov--apply-reading-palette (unless (equal name "none") name))
+ (message "Reading palette: %s" (or cj/nov--reading-palette "none")))
+
+(defun cj/nov-cycle-reading-palette ()
+ "Cycle to the next reading palette, then the no-palette state, wrapping."
+ (interactive)
+ (unless (derived-mode-p 'nov-mode)
+ (user-error "Not in a nov-mode buffer"))
+ (let ((next (cj/nov--next-reading-palette
+ cj/nov--reading-palette
+ (mapcar #'car cj/nov-reading-palettes))))
+ (cj/nov--apply-reading-palette next)
+ (message "Reading palette: %s" (or next "none"))))
+
+;; ------------------------------- Typography ----------------------------------
+
+(defcustom cj/nov-reading-font-family "Merriweather"
+ "Variable-pitch serif family for the EPUB reading view."
+ :type 'string
+ :group 'cj/nov-reading)
+
+(defcustom cj/nov-reading-text-height 180
+ "Base `default'-face height (1/10 pt) a fresh nov buffer opens at.
+The +/-/= keys adjust the page size from here with a buffer-local text-scale;
+that adjustment resets to this base each time a book is opened."
+ :type 'integer
+ :group 'cj/nov-reading)
+
+(defun cj/nov-reading-apply-typography ()
+ "Apply the reading family and base height buffer-local.
+Remaps `variable-pitch', `default', and `fixed-pitch' so nov's shr output reads
+as a comfortably-sized serif page."
+ (face-remap-add-relative 'variable-pitch
+ :family cj/nov-reading-font-family :height 1.0)
+ (face-remap-add-relative 'default
+ :family cj/nov-reading-font-family
+ :height cj/nov-reading-text-height)
+ (face-remap-add-relative 'fixed-pitch :height cj/nov-reading-text-height))
+
+(defun cj/nov-reading-text-bigger ()
+ "Increase the page font size (buffer-local), on top of the base height."
+ (interactive)
+ (text-scale-increase 1))
+
+(defun cj/nov-reading-text-smaller ()
+ "Decrease the page font size (buffer-local), on top of the base height."
+ (interactive)
+ (text-scale-decrease 1))
+
+(defun cj/nov-reading-text-reset ()
+ "Reset the page font size back to the base reading height (buffer-local)."
+ (interactive)
+ (text-scale-set 0))
+
+;; ------------------------------- Launch hook ---------------------------------
+
+(defun cj/nov-reading-setup ()
+ "Apply the reading view (typography + default palette) to this nov buffer.
+Called from the nov-mode launch hook in calibredb-epub-config.el."
+ (cj/nov-reading-apply-typography)
+ (when cj/nov-reading-default-palette
+ (cj/nov--apply-reading-palette cj/nov-reading-default-palette)))
+
+(provide 'nov-reading)
+;;; nov-reading.el ends here