aboutsummaryrefslogtreecommitdiff
path: root/modules/calibredb-epub-config.el
diff options
context:
space:
mode:
Diffstat (limited to 'modules/calibredb-epub-config.el')
-rw-r--r--modules/calibredb-epub-config.el182
1 files changed, 163 insertions, 19 deletions
diff --git a/modules/calibredb-epub-config.el b/modules/calibredb-epub-config.el
index 4243e509a..6d5963515 100644
--- a/modules/calibredb-epub-config.el
+++ b/modules/calibredb-epub-config.el
@@ -51,6 +51,7 @@
(require 'user-constants) ;; for books-dir
(require 'subr-x)
+(require 'transient) ;; cj/calibredb-menu is a transient prefix
;; Declare functions from lazy-loaded packages
(declare-function calibredb-find-create-search-buffer "calibredb" ())
@@ -59,6 +60,24 @@
(declare-function nov-render-document "nov" ())
(defvar nov-text-width) ; from nov.el; set buffer-local here
+;; calibredb commands the curated menu drives (all autoloaded by calibredb)
+(declare-function calibredb-switch-library "calibredb" ())
+(declare-function calibredb-filter-by-book-format "calibredb" ())
+(declare-function calibredb-filter-by-author-sort "calibredb" ())
+(declare-function calibredb-search-clear-filter "calibredb" ())
+(declare-function calibredb-sort-by-author "calibredb" ())
+(declare-function calibredb-sort-by-title "calibredb" ())
+(declare-function calibredb-sort-by-pubdate "calibredb" ())
+(declare-function calibredb-sort-by-format "calibredb" ())
+(declare-function calibredb-find-file "calibredb" ())
+(declare-function calibredb-dispatch "calibredb" ())
+(declare-function calibredb-show-entry "calibredb" (entry &optional switch))
+(declare-function calibredb-find-candidate-at-point "calibredb" ())
+(declare-function calibredb-search-refresh-or-resume "calibredb" (&optional begin position))
+(defvar calibredb-show-entry-switch) ; from calibredb-show.el
+(defvar calibredb-sort-by) ; from calibredb-core.el
+(defvar calibredb-search-filter) ; from calibredb-search.el
+
;; -------------------------- CalibreDB Ebook Manager --------------------------
(defun cj/calibredb-clear-filters ()
@@ -73,6 +92,23 @@
;; empty string resets keyword filter and refreshes listing
(calibredb-search-keyword-filter ""))
+(defun cj/calibredb-describe-at-point ()
+ "Show the book at point in the docked *calibredb-entry* buffer.
+Displays the entry without switching focus back to the list, so it lands
+in the bottom-docked window (see the `display-buffer-alist' entry below)
+and q (`calibredb-entry-quit') dismisses it."
+ (interactive)
+ (calibredb-show-entry (car (calibredb-find-candidate-at-point))))
+
+(defun cj/--calibredb-sort-preserving-filter (field)
+ "Set `calibredb-sort-by' to FIELD and refresh, keeping the active filter.
+calibredb's own `calibredb-sort-by-*' commands refresh with
+`calibredb-search-refresh-and-clear-filter', which drops the active filter
+on every sort. This refreshes with `calibredb-search-refresh-or-resume',
+which re-applies `calibredb-search-filter' instead."
+ (setq calibredb-sort-by field)
+ (calibredb-search-refresh-or-resume))
+
(use-package calibredb
:commands calibredb
:bind
@@ -80,7 +116,10 @@
;; use built-in filter by tag, add clear-filters
(:map calibredb-search-mode-map
("l" . calibredb-filter-by-tag)
- ("L" . cj/calibredb-clear-filters))
+ ("L" . cj/calibredb-clear-filters)
+ ;; "?" -> curated menu of frequent workflows; "H" -> the full dispatch
+ ("?" . cj/calibredb-menu)
+ ("H" . calibredb-dispatch))
:config
;; basic config
(setq calibredb-root-dir books-dir)
@@ -88,6 +127,50 @@
(setq calibredb-program "/usr/bin/calibredb")
(setq calibredb-preferred-format "epub")
(setq calibredb-search-page-max-rows 500)
+ ;; Dock the book-detail buffer to the bottom 30%; q dismisses it.
+ ;; `pop-to-buffer' honours `display-buffer-alist' (the default
+ ;; `switch-to-buffer-other-window' would not).
+ (setq calibredb-show-entry-switch #'pop-to-buffer)
+ (add-to-list 'display-buffer-alist
+ '("\\`\\*calibredb-entry\\*\\'"
+ (display-buffer-at-bottom)
+ (window-height . 0.3)))
+ ;; A curated menu of the frequent calibredb workflows, bound to `?' in the
+ ;; search buffer; calibredb's own full dispatch (the wall of every command)
+ ;; moves to `H'. Defined here in `:config' so it only builds once calibredb
+ ;; (and its matching transient) is loaded. This is the "? brings up a
+ ;; discoverable help menu" convention.
+ (transient-define-prefix cj/calibredb-menu ()
+ "Frequent calibredb workflows."
+ [["Library"
+ ("l" "switch library" calibredb-switch-library)]
+ ["Filter"
+ ("f" "format" calibredb-filter-by-book-format)
+ ("a" "author" calibredb-filter-by-author-sort)
+ ("x" "reset filter" calibredb-search-clear-filter)]
+ ["Sort"
+ ("A" "author (last name)" calibredb-sort-by-author)
+ ("t" "title" calibredb-sort-by-title)
+ ("p" "pubdate" calibredb-sort-by-pubdate)
+ ("g" "group by format" calibredb-sort-by-format)]
+ ["Book"
+ ("o" "open" calibredb-find-file)
+ ("d" "describe" cj/calibredb-describe-at-point)
+ ("H" "full calibredb menu" calibredb-dispatch)]]
+ [("q" "quit" transient-quit-one)])
+
+ ;; Keep the active filter when sorting. calibredb's macro-generated
+ ;; `calibredb-sort-by-*' commands refresh-and-clear-filter, dropping the
+ ;; filter on every sort; override each to refresh-or-resume so the filter
+ ;; survives. Named advice keeps the override idempotent across reloads.
+ (dolist (field '(id title author format date pubdate tag size language))
+ (let ((cmd (intern (format "calibredb-sort-by-%s" field)))
+ (adv (intern (format "cj/--calibredb-sort-keep-filter-%s" field)))
+ (f field))
+ (defalias adv
+ (lambda (&rest _) (interactive) (cj/--calibredb-sort-preserving-filter f))
+ (format "Sort by %s, keeping the active filter (override)." field))
+ (advice-add cmd :override adv)))
;; search window display
(setq calibredb-size-show nil)
@@ -158,6 +241,29 @@ layout passes -- each pass narrows the body width but not the natural width."
"Return the preferred EPUB text column count for WINDOW."
(cj/nov--text-width (cj/nov--natural-window-width window)))
+(defun cj/nov--rerender-preserving-position ()
+ "Re-render the nov document, restoring point's relative position.
+Capture point as a fraction of the buffer, re-render, then move point to the
+same fraction of the re-rendered buffer so the reading position is kept
+approximately."
+ (let ((frac (when (> (point-max) (point-min))
+ (/ (float (- (point) (point-min)))
+ (- (point-max) (point-min))))))
+ (nov-render-document)
+ (when frac
+ (goto-char (+ (point-min)
+ (round (* frac (- (point-max) (point-min)))))))))
+
+(defun cj/nov--center-in-window (win total width)
+ "Center a WIDTH-column text block in WIN, given its TOTAL natural width.
+Set equal left/right display margins and push the fringes to the window edge."
+ ;; floor: never let the margins squeeze the text area below WIDTH.
+ (let ((margin (max 0 (/ (- total width) 2))))
+ (set-window-margins win margin margin))
+ ;; Push the fringes out to the window's edge; otherwise they sit between the
+ ;; margin and the text and show as thin vertical lines beside it.
+ (set-window-fringes win nil nil t))
+
(defun cj/nov-update-layout (&optional _frame)
"Size the EPUB text column for this buffer and center it in its window.
`nov-text-width' is set so nov's `shr' fills the text to roughly 80% of the
@@ -173,20 +279,9 @@ command."
(width (cj/nov--text-width total)))
(unless (eql nov-text-width width)
(setq-local nov-text-width width)
- (let ((frac (when (> (point-max) (point-min))
- (/ (float (- (point) (point-min)))
- (- (point-max) (point-min))))))
- (nov-render-document)
- (when frac
- (goto-char (+ (point-min)
- (round (* frac (- (point-max) (point-min)))))))))
+ (cj/nov--rerender-preserving-position))
(when win
- ;; floor: never let the margins squeeze the text area below WIDTH.
- (let ((margin (max 0 (/ (- total width) 2))))
- (set-window-margins win margin margin))
- ;; Push the fringes out to the window's edge; otherwise they sit between
- ;; the margin and the text and show as thin vertical lines beside it.
- (set-window-fringes win nil nil t)))))
+ (cj/nov--center-in-window win total width)))))
(defun cj/--nov-adjust-margin (delta)
"Add DELTA to `cj/nov-margin-percent' (clamped 0..25), re-lay-out, and report.
@@ -210,11 +305,12 @@ 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
- ;; Darker sepia color (#E8DCC0) is easier on the eyes than pure white
- (face-remap-add-relative 'variable-pitch :family "Merriweather" :height 1.0 :foreground "#E8DCC0")
- (face-remap-add-relative 'default :family "Merriweather" :height 180 :foreground "#E8DCC0")
- (face-remap-add-relative 'fixed-pitch :height 180 :foreground "#E8DCC0")
+ ;; Use Merriweather for comfortable reading with appropriate scaling.
+ ;; Darker sepia color (#E8DCC0) is easier on the eyes than pure white.
+ (let ((sepia "#E8DCC0"))
+ (face-remap-add-relative 'variable-pitch :family "Merriweather" :height 1.0 :foreground sepia)
+ (face-remap-add-relative 'default :family "Merriweather" :height 180 :foreground sepia)
+ (face-remap-add-relative 'fixed-pitch :height 180 :foreground sepia))
;; Enable visual-line-mode for proper text wrapping
(visual-line-mode 1)
;; Set fill-column as a fallback
@@ -327,6 +423,54 @@ Try to use the Calibre book id from the parent folder name (for example,
("t" . nov-goto-toc)
("C-c C-b" . cj/nov-jump-to-calibredb)))
+;; ------------------------- Nov bookmark naming -------------------------------
+;; In a nov buffer "m" is bound to `bookmark-set' (above). nov's
+;; `nov-bookmark-make-record' names the record after `(buffer-name)' -- the EPUB
+;; filename, extension and all. Rebuild it as "Author, Title" parsed from the
+;; filename: under Calibre's "<Title> - <Author>.epub" naming the filename is
+;; more complete than the EPUB's embedded metadata (which carries truncated
+;; titles and author-sort "Last, First" forms).
+
+(defun cj/--nov-clean-title (s)
+ "Clean a title or author S parsed from an EPUB filename, or nil when blank.
+Restores a colon where Calibre sanitized \":\" to \"_\" (\"Frege_ A Guide\"
+-> \"Frege: A Guide\"), turns any leftover underscore into a space, and
+collapses runs of whitespace."
+ (when (stringp s)
+ (let* ((colon (replace-regexp-in-string "_ " ": " s))
+ (spaced (replace-regexp-in-string "_" " " colon))
+ (out (string-trim (replace-regexp-in-string "[ \t]+" " " spaced))))
+ (and (not (string-empty-p out)) out))))
+
+(defun cj/--nov-bookmark-name-from-file (path)
+ "Return \"Author, Title\" derived from an EPUB PATH's filename, or nil.
+Splits the filename (sans extension) on its last \" - \" into title and
+author per Calibre's \"<Title> - <Author>\" convention, restoring colons and
+reordering to \"Author, Title\". Falls back to the cleaned whole name when
+there is no \" - \" separator."
+ (when (and (stringp path) (not (string-empty-p path)))
+ (let ((base (file-name-sans-extension (file-name-nondirectory path))))
+ (if (string-match "\\`\\(.+\\) - \\(.+\\)\\'" base)
+ (let ((title (cj/--nov-clean-title (match-string 1 base)))
+ (author (cj/--nov-clean-title (match-string 2 base))))
+ (cond ((and author title) (format "%s, %s" author title))
+ (title title)
+ (author author)
+ (t nil)))
+ (cj/--nov-clean-title base)))))
+
+(defun cj/--nov-bookmark-rename-record (record)
+ "Replace RECORD's bookmark name with \"Author, Title\" from its EPUB filename.
+Advice (:filter-return) on `nov-bookmark-make-record'. RECORD is
+\(NAME . ALIST) carrying a `filename'; left unchanged when no name derives."
+ (let ((name (cj/--nov-bookmark-name-from-file
+ (alist-get 'filename (cdr record)))))
+ (if name (cons name (cdr record)) record)))
+
+(with-eval-after-load 'nov
+ (advice-add 'nov-bookmark-make-record :filter-return
+ #'cj/--nov-bookmark-rename-record))
+
(defun cj/--nov-image-padding-cols (col-width img-px font-width-px)
"Return left-padding columns to center an IMG-PX-wide image in COL-WIDTH cols.
FONT-WIDTH-PX is the column width in pixels; clamped up to 1 so a zero or