diff options
Diffstat (limited to 'modules/calibredb-epub-config.el')
| -rw-r--r-- | modules/calibredb-epub-config.el | 192 |
1 files changed, 113 insertions, 79 deletions
diff --git a/modules/calibredb-epub-config.el b/modules/calibredb-epub-config.el index a17bf8c91..a8b131be8 100644 --- a/modules/calibredb-epub-config.el +++ b/modules/calibredb-epub-config.el @@ -1,4 +1,4 @@ -;;; calibredb-epub-config --- Functionality for Ebook Management and Display -*- lexical-binding: t; coding: utf-8; -*- +;;; calibredb-epub-config.el --- Functionality for Ebook Management and Display -*- lexical-binding: t; coding: utf-8; -*- ;; author Craig Jennings <c@cjennings.net> ;;; Commentary: @@ -6,46 +6,17 @@ ;; Layer: 4 (Optional). ;; Category: O/D/P. ;; Load shape: eager. -;; Eager reason: none; optional ebook workflow, a command-loaded deferral -;; candidate for Phase 4. -;; Top-level side effects: one add-hook, one advice-add, package config. -;; Runtime requires: user-constants, subr-x. +;; Eager reason: none; ebook commands can load by command. +;; Top-level side effects: one hook, one advice, package config. +;; Runtime requires: user-constants, subr-x, transient. ;; Direct test load: yes. ;; -;; This module provides a comprehensive ebook management and reading experience -;; within Emacs, integrating CalibreDB for library management and Nov for EPUB -;; reading. +;; CalibreDB and Nov integration for browsing the Calibre library and reading +;; EPUBs inside Emacs. The module adds a curated CalibreDB transient, filter +;; helpers, Nov typography, image centering, and reader-to-library navigation. ;; -;; 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 +;; EPUB is preferred when available; external opening remains available for +;; formats or workflows better handled outside Emacs. ;;; Code: @@ -62,6 +33,15 @@ ;; calibredb commands the curated menu drives (all autoloaded by calibredb) (declare-function calibredb-switch-library "calibredb" ()) +(declare-function calibredb-search-keyword-filter "calibredb-search") + +;; calibredb's filter-scope flags (set in `cj/--calibredb-open-to-favorites'); +;; declared special so the assignments compile clean when calibredb is absent. +(defvar calibredb-tag-filter-p) +(defvar calibredb-favorite-filter-p) +(defvar calibredb-author-filter-p) +(defvar calibredb-date-filter-p) +(defvar calibredb-format-filter-p) (declare-function calibredb-filter-by-book-format "calibredb" ()) (declare-function calibredb-filter-by-author-sort "calibredb" ()) (declare-function calibredb-search-clear-filter "calibredb" ()) @@ -77,6 +57,13 @@ (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 filter-state vars (set by cj/calibredb-clear-filters and friends) +(defvar calibredb-tag-filter-p) ; from calibredb-search.el +(defvar calibredb-favorite-filter-p) ; from calibredb-search.el +(defvar calibredb-author-filter-p) ; from calibredb-search.el +(defvar calibredb-date-filter-p) ; from calibredb-search.el +(defvar calibredb-format-filter-p) ; from calibredb-search.el +(defvar calibredb-search-current-page) ; from calibredb-search.el ;; -------------------------- CalibreDB Ebook Manager -------------------------- @@ -109,6 +96,26 @@ which re-applies `calibredb-search-filter' instead." (setq calibredb-sort-by field) (calibredb-search-refresh-or-resume)) +(defun cj/--calibredb-open-to-favorites (&rest _) + "Filter the calibredb search to books tagged `calibredb-favorite-keyword'. +Advice (:after) on `calibredb' so every launch lands on the favorite-keyword +books (Craig's \"in-progress\" reading list); clear with L / x to see the +whole library. Scopes to the tag field (sets `calibredb-tag-filter-p', +clears the other filter-scope flags), because a bare keyword filter matches +the keyword in any field -- title, author, or the description -- and would +surface books that merely mention it. No-op unless a non-empty string +keyword is set." + (when (and (boundp 'calibredb-favorite-keyword) + (stringp calibredb-favorite-keyword) + (not (string-empty-p calibredb-favorite-keyword)) + (fboundp 'calibredb-search-keyword-filter)) + (setq calibredb-tag-filter-p t + calibredb-favorite-filter-p nil + calibredb-author-filter-p nil + calibredb-date-filter-p nil + calibredb-format-filter-p nil) + (calibredb-search-keyword-filter calibredb-favorite-keyword))) + (use-package calibredb :commands calibredb :bind @@ -177,7 +184,10 @@ which re-applies `calibredb-search-filter' instead." (setq calibredb-order "asc") (setq calibredb-id-width 7) (setq calibredb-favorite-icon "🔖") - (setq calibredb-favorite-keyword "in-progress")) + (setq calibredb-favorite-keyword "in-progress") + ;; Open every calibredb launch (dashboard, M-x, elsewhere) filtered to the + ;; in-progress favorites; L / x clears to the whole library. + (advice-add 'calibredb :after #'cj/--calibredb-open-to-favorites)) ;; ------------------------------ Nov Epub Reader ------------------------------ @@ -200,7 +210,6 @@ Adjust it live with `cj/nov-widen-text' and `cj/nov-narrow-text'.") (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 @@ -241,6 +250,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 @@ -256,20 +288,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. @@ -293,11 +314,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. + ;; (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) ;; Enable visual-line-mode for proper text wrapping (visual-line-mode 1) ;; Set fill-column as a fallback @@ -384,6 +406,12 @@ Try to use the Calibre book id from the parent folder name (for example, (calibredb-search-keyword-filter "") (message "CalibreDB: no metadata; showing all")))))) +(require 'system-lib) +;; nov renders epub via shr, which paints with manual `face' properties. Left in +;; `global-font-lock-mode' font-lock overwrites them and the book loses its +;; colors, the same issue as elfeed-show and mu4e-view. Exclude nov-mode. +(cj/exclude-from-global-font-lock 'nov-mode) + (use-package nov :mode ("\\.epub\\'" . nov-mode) @@ -412,14 +440,15 @@ Try to use the Calibre book id from the parent folder name (for example, ;; ------------------------- 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. +;; Both nov (EPUB) and pdf-view (PDF) name a new bookmark after the buffer -- +;; the file's name, extension and all. Rebuild it as "Author, Title" parsed +;; from the filename: under Calibre's "<Title> - <Author>.<ext>" naming the +;; filename is more complete than the file's embedded metadata (which carries +;; truncated titles and author-sort "Last, First" forms). One :filter-return +;; advice serves both record functions; the parser is extension-agnostic. + +(defun cj/--reading-clean-title (s) + "Clean a title or author S parsed from a book 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." @@ -429,34 +458,39 @@ collapses runs of whitespace." (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. +(defun cj/--reading-bookmark-name-from-file (path) + "Return \"Author, Title\" derived from a book 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." +there is no \" - \" separator. Extension-agnostic, so it serves EPUB and PDF." (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)))) + (let ((title (cj/--reading-clean-title (match-string 1 base))) + (author (cj/--reading-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 + (cj/--reading-clean-title base))))) + +(defun cj/--reading-bookmark-rename-record (record) + "Replace RECORD's bookmark name with \"Author, Title\" from its filename. +Advice (:filter-return) on `nov-bookmark-make-record' and +`pdf-view-bookmark-make-record'. RECORD is (NAME . ALIST) carrying a +`filename'; left unchanged when no name derives." + (let ((name (cj/--reading-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)) + #'cj/--reading-bookmark-rename-record)) + +(with-eval-after-load 'pdf-view + (advice-add 'pdf-view-bookmark-make-record :filter-return + #'cj/--reading-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. |
