aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-06 16:49:03 -0500
committerCraig Jennings <c@cjennings.net>2026-06-06 16:49:03 -0500
commita0765206144f49a845c8628d53111dd3d04f0e49 (patch)
treeba69afdab6b155722e1c1c0a8cfb30e5484c1334
parent47940f707aceea6066f8c9860207724c5d30f9e0 (diff)
downloaddotemacs-a0765206144f49a845c8628d53111dd3d04f0e49.tar.gz
dotemacs-a0765206144f49a845c8628d53111dd3d04f0e49.zip
feat(calibredb): curated ? menu, docked description, filter-preserving sort
Three things in the calibredb search buffer. A curated transient on ? exposes just my frequent workflows (switch library, filter by format or author, sort by author/title/pubdate/format, open, describe) instead of leaving me to remember calibredb's top-level single keys. calibredb's own full dispatch moves to H. which-key can't help here: these are mode-map single keys, not a prefix. So a curated menu on ? is the discoverable entry point. The transient is defined in :config rather than top-level: its macro expands against the elpa transient, but a batch Emacs loads the older built-in transient and breaks the load. The book-detail view (d, or v) now docks to the bottom 30% and q dismisses it. calibredb-show-entry-switch goes to pop-to-buffer so a display-buffer-alist rule applies. The default switch-to-buffer-other-window ignores it. It's the same bottom-dock pattern as the signal chat buffer. Sorting kept dropping the active filter. calibredb's macro-generated sort commands all end in calibredb-search-refresh-and-clear-filter, which nulls every filter flag. I override each to refresh via calibredb-search-refresh-or-resume, which re-applies the filter, so a sort over a filtered list keeps it. I used named advice, so reloading the module doesn't stack it. Tests cover the describe command and the sort helper. The transient, bindings, dock, and advice wiring need the elpa transient and a live calibredb, so they're verified in the running daemon.
-rw-r--r--modules/calibredb-epub-config.el85
-rw-r--r--tests/test-calibredb-epub-config--menu.el52
2 files changed, 136 insertions, 1 deletions
diff --git a/modules/calibredb-epub-config.el b/modules/calibredb-epub-config.el
index 9f09e392..a17bf8c9 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)
diff --git a/tests/test-calibredb-epub-config--menu.el b/tests/test-calibredb-epub-config--menu.el
new file mode 100644
index 00000000..4860efc3
--- /dev/null
+++ b/tests/test-calibredb-epub-config--menu.el
@@ -0,0 +1,52 @@
+;;; test-calibredb-epub-config--menu.el --- calibredb curated-menu tests -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the docked book-description command bound into the curated calibredb
+;; menu. The transient itself, its `?'/`H' keybindings, and the
+;; display-buffer-alist dock live in calibredb's deferred `use-package' config
+;; (they need the elpa transient, which batch does not load) and are verified
+;; live in the daemon; here we cover the describe command, which has no transient
+;; dependency.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'calibredb-epub-config)
+
+;; calibredb vars (defvar'd here so the tests' `let' bindings are dynamic; the
+;; module's bare defvars are file-local to its own compilation unit).
+(defvar calibredb-sort-by)
+(defvar calibredb-search-filter)
+(defvar calibredb-format-filter-p)
+
+(ert-deftest test-calibredb-describe-at-point-shows-entry-without-switch ()
+ "Normal: describe calls `calibredb-show-entry' on the entry at point with no
+switch argument, so the entry lands in the docked window with focus (q quits)."
+ (let (call)
+ (cl-letf (((symbol-function 'calibredb-find-candidate-at-point)
+ (lambda () '(the-entry extra)))
+ ((symbol-function 'calibredb-show-entry)
+ (lambda (&rest args) (setq call args))))
+ (cj/calibredb-describe-at-point)
+ ;; one argument only -- the entry -- and switch is therefore nil
+ (should (equal call '(the-entry))))))
+
+(ert-deftest test-calibredb-sort-preserving-filter-keeps-filter ()
+ "Normal: the filter-preserving sort sets the field and refreshes via
+`calibredb-search-refresh-or-resume' without touching the active filter."
+ (let ((calibredb-sort-by 'id)
+ (calibredb-search-filter "epub")
+ (calibredb-format-filter-p t)
+ (refreshed nil))
+ (cl-letf (((symbol-function 'calibredb-search-refresh-or-resume)
+ (lambda (&rest _) (setq refreshed t))))
+ (cj/--calibredb-sort-preserving-filter 'author)
+ (should (eq calibredb-sort-by 'author)) ; field updated
+ (should refreshed) ; refreshed
+ (should (equal calibredb-search-filter "epub")) ; filter kept
+ (should calibredb-format-filter-p)))) ; filter flag kept
+
+(provide 'test-calibredb-epub-config--menu)
+;;; test-calibredb-epub-config--menu.el ends here