From a0765206144f49a845c8628d53111dd3d04f0e49 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 6 Jun 2026 16:49:03 -0500 Subject: 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. --- modules/calibredb-epub-config.el | 85 ++++++++++++++++++++++++++++++- tests/test-calibredb-epub-config--menu.el | 52 +++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 tests/test-calibredb-epub-config--menu.el 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 -- cgit v1.2.3