1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
|
;;; calibredb-epub-config --- Functionality for Ebook Management and Display -*- lexical-binding: t; coding: utf-8; -*-
;; author Craig Jennings <c@cjennings.net>
;;; Commentary:
;; This module provides a comprehensive ebook management and reading experience
;; within Emacs, integrating CalibreDB for library management and Nov for EPUB
;; reading.
;;
;; FEATURES:
;; - CalibreDB integration for managing your Calibre ebook library
;; - Nov mode for reading EPUB files with customized typography and layout
;; - Seamless navigation between Nov reading buffers and CalibreDB entries
;; - Image centering in EPUB documents without modifying buffer text
;; - Quick filtering and searching within your ebook library
;;
;; KEY BINDINGS:
;; - M-B: Open CalibreDB library browser
;; - In CalibreDB search mode:
;; - l: Filter by tag
;; - L: Clear all filters
;; - In Nov mode:
;; - z: Open current EPUB in external viewer (zathura)
;; - C-c C-b: Jump to CalibreDB entry for current book
;; - m: Set bookmark
;; - b: List bookmarks
;;
;; WORKFLOW:
;; 1. Press M-B to browse your Calibre library
;; 2. Use filters (l for tags, L to clear) to narrow results
;; 3. Open an EPUB to read it in Nov with optimized typography
;; 4. While reading, use C-c C-b to jump back to the book's metadata
;; 5. Use z to open in external reader when needed
;;
;; CONFIGURATION NOTES:
;; - Prefers EPUB format when available, falls back to PDF
;; - Centers images in EPUB documents using display properties
;; - Applies custom typography with larger fonts for comfortable reading
;; - Uses visual-fill-column for centered text with appropriate margins
;;; Code:
(require 'user-constants) ;; for books-dir
;; Declare functions from lazy-loaded packages
(declare-function calibredb-find-create-search-buffer "calibredb" ())
(declare-function calibredb-search-keyword-filter "calibredb" (keyword))
(declare-function cj/open-file-with-command "system-utils" (command))
(declare-function visual-fill-column-mode "visual-fill-column" (&optional arg))
(declare-function visual-fill-column--adjust-window "visual-fill-column" ())
;; -------------------------- CalibreDB Ebook Manager --------------------------
(use-package calibredb
:defer 1
:commands calibredb
:bind
("M-B" . calibredb)
;; use built-in filter by tag, add clear-filters
(:map calibredb-search-mode-map
("l" . calibredb-filter-by-tag)
("L" . cj/calibredb-clear-filters))
:config
;; basic config
(setq calibredb-root-dir books-dir)
(setq calibredb-db-dir (expand-file-name "metadata.db" calibredb-root-dir))
(setq calibredb-program "/usr/bin/calibredb")
(setq calibredb-preferred-format "epub")
(setq calibredb-search-page-max-rows 20000)
;; search window display
(setq calibredb-size-show nil)
(setq calibredb-order "asc")
(setq calibredb-id-width 7))
(defun cj/calibredb-clear-filters ()
"Clear active filters and show all results."
(interactive)
(setq calibredb-tag-filter-p nil
calibredb-favorite-filter-p nil
calibredb-author-filter-p nil
calibredb-date-filter-p nil
calibredb-format-filter-p nil
calibredb-search-current-page 1)
;; empty string resets keyword filter and refreshes listing
(calibredb-search-keyword-filter ""))
;; ------------------------------ Nov Epub Reader ------------------------------
;; Prevent magic-fallback-mode-alist from opening epub as archive-mode
;; Advise set-auto-mode to force nov-mode for .epub files before magic-fallback runs
(defun cj/force-nov-mode-for-epub (orig-fun &rest args)
"Force nov-mode for .epub files, bypassing archive-mode detection."
(if (and buffer-file-name
(string-match-p "\\.epub\\'" buffer-file-name))
(progn
;; Load nov if not already loaded
(unless (featurep 'nov)
(require 'nov nil t))
;; Call nov-mode if available, otherwise fallback to default behavior
(if (fboundp 'nov-mode)
(nov-mode)
(apply orig-fun args)))
(apply orig-fun args)))
(advice-add 'set-auto-mode :around #'cj/force-nov-mode-for-epub)
;; Visual-fill-column provides centered text with margins
(use-package visual-fill-column
:defer t
:config
(setq-default visual-fill-column-center-text t))
;; Define helper functions before use-package so they're available for hooks
(defun cj/forward-paragraph-and-center ()
"Forward one paragraph and center the page."
(interactive)
(forward-paragraph)
(recenter))
(defun cj/nov-apply-preferences ()
"Apply preferences after nov-mode has launched."
(interactive)
;; Use Merriweather for comfortable reading with appropriate scaling
;; Darker sepia color (#E8DCC0) is easier on the eyes than pure white
(face-remap-add-relative 'variable-pitch :family "Merriweather" :height 1.8 :foreground "#E8DCC0")
(face-remap-add-relative 'default :family "Merriweather" :height 180 :foreground "#E8DCC0")
(face-remap-add-relative 'fixed-pitch :height 180 :foreground "#E8DCC0")
;; Make this buffer-local so other Nov buffers can choose differently
;; Setting to t makes nov respect visual-fill-column margins
(setq-local nov-text-width t)
;; Enable visual-line-mode for proper text wrapping
(visual-line-mode 1)
;; Set fill-column as a fallback
(setq-local fill-column 100)
;; Enable visual-fill-column for centered text with margins
(when (require 'visual-fill-column nil t)
(setq-local visual-fill-column-center-text t)
;; Set text width for comfortable reading (characters per line)
(setq-local visual-fill-column-width 100)
(visual-fill-column-mode 1))
(nov-render-document)
;; Force visual-fill-column to recalculate after rendering
(when (bound-and-true-p visual-fill-column-mode)
(visual-fill-column--adjust-window)))
(use-package nov
:mode
("\\.epub\\'" . nov-mode)
:hook
(nov-mode . cj/nov-apply-preferences)
:bind
(:map nov-mode-map
("m" . bookmark-set)
("b" . bookmark-bmenu-list)
("r" . nov-render-document)
("l" . recenter-top-bottom)
("d" . sdcv-search-input)
("." . cj/forward-paragraph-and-center)
("<" . nov-history-back)
(">" . nov-history-forward)
("," . backward-paragraph)
;; open current EPUB with zathura (same key in pdf-view)
("z" . (lambda () (interactive) (cj/open-file-with-command "zathura")))
("t" . nov-goto-toc)
("C-c C-b" . cj/nov-jump-to-calibredb)))
(defun cj/nov-center-images ()
"Center images in the current Nov buffer without modifying text.
Use `line-prefix' and `wrap-prefix' with a space display property aligned to a
computed column based on the window text area width."
(let ((inhibit-read-only t))
;; Clear any prior centering prefixes first (fresh render usually makes this
;; unnecessary, but it makes the function idempotent).
(remove-text-properties (point-min) (point-max)
'(line-prefix nil wrap-prefix nil))
(save-excursion
(goto-char (point-min))
;; Work in the selected window showing this buffer (if any).
(when-let* ((win (get-buffer-window (current-buffer) t))
(col-width (window-body-width win)) ;; columns
(col-px (* col-width (window-font-width win))))
(while (let ((m (text-property-search-forward
'display nil
(lambda (_ p) (and (consp p) (eq (car-safe p) 'image))))))
(when m
(let* ((img (prop-match-value m))
(img-px (car (image-size img t))) ;; pixel width
;; Convert pixel image width to columns for alignment.
(img-cols (max 1 (ceiling (/ (float img-px)
(max 1 (window-font-width win))))))
(pad-cols (max 0 (/ (- col-width img-cols) 2)))
(prefix (propertize " " 'display `(space :align-to ,pad-cols))))
(save-excursion
(goto-char (prop-match-beginning m))
(beginning-of-line)
(let ((bol (point))
(eol (line-end-position)))
(add-text-properties bol eol
`(line-prefix ,prefix
wrap-prefix ,prefix)))))
t)))))))
(add-hook 'nov-post-html-render-hook #'cj/nov-center-images)
;; Jump from a Nov buffer to the corresponding CalibreDB entry.
(defun cj/nov--metadata-get (key)
"Return a metadata value from nov-metadata trying KEY as symbol and string."
(let* ((v (or (and (boundp 'nov-metadata)
(or (alist-get key nov-metadata nil nil #'equal)
(alist-get (if (symbolp key) (symbol-name key) key)
nov-metadata nil nil #'equal)))
nil)))
(cond
((and (listp v) (= (length v) 1)) (car v))
((stringp v) v)
(t v))))
(defun cj/nov--file-path ()
"Return the current EPUB file path when in nov-mode, or nil."
(when (derived-mode-p 'nov-mode)
;; In nov, the buffer visits the .epub; buffer-file-name is usually the EPUB.
(or buffer-file-name
(and (boundp 'nov-epub-filename) nov-epub-filename)
(and (boundp 'nov-epub-file) nov-epub-file))))
(defun cj/nov-jump-to-calibredb ()
"Open CalibreDB focused on the current EPUB's book entry.
Try to use the Calibre book id from the parent folder name (for example,
\"Title (123)\"). Fall back to a title or author search when no id exists."
(interactive)
(require 'calibredb)
(let* ((file (cj/nov--file-path))
(title (or (cj/nov--metadata-get 'title)
(cj/nov--metadata-get "title")))
(author (or (cj/nov--metadata-get 'creator)
(cj/nov--metadata-get 'author)
(cj/nov--metadata-get "creator")
(cj/nov--metadata-get "author")))
(id (when file
(let* ((parent (file-name-nondirectory
(directory-file-name (file-name-directory file)))))
(when (string-match " (\\([0-9]+\\))\\'" parent)
(match-string 1 parent))))))
(calibredb)
(with-current-buffer (calibredb-find-create-search-buffer)
(setq calibredb-search-current-page 1)
(cond
(id
(calibredb-search-keyword-filter (format "id:%s" id))
(message "CalibreDB: focused by id:%s" id))
((or title author)
(let* ((q (string-join
(delq nil (list (and title (format "title:\"%s\"" title))
(and author (format "authors:\"%s\"" author))))
" and ")))
(calibredb-search-keyword-filter q)
(message "CalibreDB: search %s" (if (string-empty-p q) "<all>" q))))
(t
(calibredb-search-keyword-filter "")
(message "CalibreDB: no metadata; showing all"))))))
(provide 'calibredb-epub-config)
;;; calibredb-epub-config.el ends here
|