;;; my-nov.el -- Extensions for nov.el -*- lexical-binding: t -*- ;; Copyright (C) 2023 Free Software Foundation. ;; Author: Yuchen Pei ;; Package-Requires: ((emacs "28.2")) ;; This file is part of dotfiles. ;; dotfiles is free software: you can redistribute it and/or modify it under ;; the terms of the GNU Affero General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; dotfiles is distributed in the hope that it will be useful, but WITHOUT ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General ;; Public License for more details. ;; You should have received a copy of the GNU Affero General Public ;; License along with dotfiles. If not, see . ;;; Commentary: ;; Extensions for nov.el. ;;; Code: (require 'nov) ;; override nov-render-title ;; this is because header line does not work with follow mode (defun my-nov-render-title (dom) "Custom rendering function for DOM. Sets `header-line-format' to a combination of the EPUB title and chapter title." (let ((title (cdr (assq 'title nov-metadata))) (chapter-title (car (esxml-node-children dom)))) (when (not chapter-title) (setq chapter-title "No title")) ;; this shouldn't happen for properly authored EPUBs (when (not title) (setq title "No title")) ;; TODO: fix mode line update (setq mode-line-buffer-identification (format "%s: %s (%d%%)" title chapter-title (/ (* 100 (my-nov-word-position)) my-nov-total-word-count) )) )) (defun my-nov-render-span (dom) (unless (equal (dom-attr dom 'epub:type) "pagebreak") (shr-generic dom))) (defun my-nov-find-file-with-ipath (file-name ipath) "Find epub file and goto IPATH. Useful for recoll." (find-file file-name) (unless (derived-mode-p 'nov-mode) (nov-mode)) (nov-goto-document (nov-find-document (lambda (p) (eq ipath (car p)))))) (defun my-nov-scroll-up (arg) "Scroll with `scroll-up' or visit next chapter if at bottom." (interactive "P") (if (>= (follow-window-end) (point-max)) (nov-next-document) (follow-scroll-up arg))) (defun my-nov-copy-buffer-file-with-staging () (interactive) (unless (derived-mode-p 'nov-mode) (error "Not in nov mode")) (pcase-let* ((name (completing-read (format "Copy %s to: " nov-file-name) my-copy-file-targets nil t)) (`(,dest ,staging) (alist-get name my-copy-file-targets nil nil #'equal))) (my-copy-file-with-staging nov-file-name dest staging))) (defun my-nov-set-margins () ;; Does not work as well as setq left- and right-margin-width ;; (set-window-margins nil 3 2) (setq left-margin-width 3) (setq right-margin-width 2) ;; Does not work as well as setq left- and right-fringe-width ;; (set-window-fringes nil 0 0) (setq left-fringe-width 0) (setq right-fringe-width 0) (visual-line-mode) ) (defvar-local my-nov-document-word-counts nil "Word count of each nov document.") (defvar-local my-nov-total-word-count nil "Total word count of the epub.") (defun my-nov-count-words () (interactive) (unless my-nov-document-word-counts (message "Counting words...") (setq my-nov-document-word-counts (apply 'vector (seq-map (lambda (doc) (with-temp-buffer (pcase-let ((`(,name . ,file) doc)) (insert-file-contents file) (nov-render-html) (cons name (count-words (point-min) (point-max)))))) nov-documents))) (setq my-nov-total-word-count (seq-reduce (lambda (sum pair) (+ sum (cdr pair))) my-nov-document-word-counts 0)) (message "Counting words...done"))) (defun my-nov-stats () (interactive) (message "%d words; %d standard pages" my-nov-total-word-count (ceiling (/ my-nov-total-word-count 300.0)))) ;;; TODO: also show current percentage in the total book in the mode ;;; line (defun my-nov-goto-nth-word (n) "Go to the nth word of the current epub." (my-nov-count-words) (setq nov-documents-index -1) (let ((found (seq-find (lambda (pair) (setq n (- n (cdr pair))) (setq nov-documents-index (1+ nov-documents-index)) (<= n 0)) my-nov-document-word-counts))) (nov-render-document) (if (> n 0) (end-of-buffer) (forward-word (+ n (cdr found))))) ) (defun my-nov-word-position () "Where are we in terms of word position? Return n, such that nth word of the epub is at the beginning of the screen." (my-nov-count-words) (let ((result 0)) (dotimes (i nov-documents-index) (setq result (+ result (cdr (aref my-nov-document-word-counts i))))) (save-excursion (move-to-window-line 0) (setq result (+ result (count-words (point-min) (point))))))) (defun my-nov-skim-forward () "Forward by 3-10% of the book." (interactive) (let ((pc (+ 3 (random 8)))) (my-nov-goto-nth-word (+ (my-nov-word-position) (/ (* my-nov-total-word-count pc) 100))) (message "Skimmed forward by %d%% of the book" pc))) (defun my-nov-skim-backward () "Backward by 3-10% of the book." (interactive) (let ((pc (+ 3 (random 8)))) (my-nov-goto-nth-word (max 0 (- (my-nov-word-position) (/ (* my-nov-total-word-count pc) 100)))) (message "Skimmed backward by %d%% of the book" pc))) (defun my-nov-goto-random-position () "Goto a random position in the epub." (interactive) (my-nov-count-words) (let ((n (random my-nov-total-word-count))) (my-nov-goto-nth-word n) (message "Went to the %dth word (%d%% of the book)." n (/ (* n 100) my-nov-total-word-count)))) (provide 'my-nov) ;;; my-nov.el ends here