diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/calendar-sync.el | 22 | ||||
| -rw-r--r-- | modules/calibredb-epub-config.el | 25 | ||||
| -rw-r--r-- | modules/eat-config.el | 26 | ||||
| -rw-r--r-- | modules/font-config.el | 40 | ||||
| -rw-r--r-- | modules/keyboard-compat.el | 6 | ||||
| -rw-r--r-- | modules/nov-reading.el | 282 |
6 files changed, 353 insertions, 48 deletions
diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el index 1079a72be..297d1fe61 100644 --- a/modules/calendar-sync.el +++ b/modules/calendar-sync.el @@ -381,12 +381,28 @@ Syncs all calendars immediately, then every `calendar-sync-interval-minutes'." ;; User can manually sync or it will happen on next timer tick if auto-sync is enabled )) -;; Start auto-sync if enabled and calendars are configured -;; Syncs immediately then every calendar-sync-interval-minutes (default: 60 minutes) +;; Defer auto-sync until calendar data is first needed. +;; +;; The :secret-host feed URLs live in authinfo.gpg, and BOTH the immediate sync +;; and every periodic timer tick resolve them. Calling `calendar-sync-start' at +;; load (immediate sync + recurring timer) therefore decrypts authinfo.gpg right +;; after startup, prompting for the GPG passphrase on a cold gpg-agent (e.g. +;; after a reboot). Defer the whole start to the first org-agenda use, so the +;; unlock happens when the user actually asks for calendar data. A manual +;; `calendar-sync-start' / `calendar-sync-now' still works on demand. +(defun calendar-sync--auto-start-on-first-agenda () + "Start auto-sync on the first org-agenda use, then remove this hook. +One-shot: deferring `calendar-sync-start' until the agenda is first built keeps a +cold gpg-agent from being prompted for the authinfo passphrase at startup. +Removes itself before starting so a `calendar-sync-start' error can't re-fire it." + (remove-hook 'org-agenda-mode-hook #'calendar-sync--auto-start-on-first-agenda) + (calendar-sync-start)) + +;; Arm the deferred start when auto-sync is enabled and calendars are configured. (when (and calendar-sync-auto-start calendar-sync-calendars (not noninteractive)) - (calendar-sync-start)) + (add-hook 'org-agenda-mode-hook #'calendar-sync--auto-start-on-first-agenda)) (provide 'calendar-sync) diff --git a/modules/calibredb-epub-config.el b/modules/calibredb-epub-config.el index a8b131be8..b03d83ed0 100644 --- a/modules/calibredb-epub-config.el +++ b/modules/calibredb-epub-config.el @@ -31,6 +31,8 @@ (declare-function nov-render-document "nov" ()) (defvar nov-text-width) ; from nov.el; set buffer-local here +(require 'nov-reading) ;; reading-view theme layer: palettes + typography + size + ;; calibredb commands the curated menu drives (all autoloaded by calibredb) (declare-function calibredb-switch-library "calibredb" ()) (declare-function calibredb-search-keyword-filter "calibredb-search") @@ -314,12 +316,8 @@ 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. - ;; (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) + ;; Reading typography + color palette live in the nov-reading theme layer. + (cj/nov-reading-setup) ;; Enable visual-line-mode for proper text wrapping (visual-line-mode 1) ;; Set fill-column as a fallback @@ -428,14 +426,19 @@ Try to use the Calibre book id from the parent folder name (for example, ("<" . nov-history-back) (">" . nov-history-forward) ("," . backward-paragraph) - ;; +/= widen the text column, -/_ narrow it (50%..100% of the window) - ("+" . cj/nov-widen-text) - ("=" . cj/nov-widen-text) - ("-" . cj/nov-narrow-text) - ("_" . cj/nov-narrow-text) + ;; +/- adjust the page font size, = resets it to the default height + ("+" . cj/nov-reading-text-bigger) + ("-" . cj/nov-reading-text-smaller) + ("=" . cj/nov-reading-text-reset) + ;; { } adjust the text-column width (50%..100% of the window) + ("}" . cj/nov-widen-text) + ("{" . cj/nov-narrow-text) ;; open current EPUB with zathura (same key in pdf-view) ("z" . cj/nov-open-external) ("t" . nov-goto-toc) + ;; c cycles reading palettes (sepia/dark/light/none); C picks one by name + ("c" . cj/nov-cycle-reading-palette) + ("C" . cj/nov-set-reading-palette) ("C-c C-b" . cj/nov-jump-to-calibredb))) ;; ------------------------- Nov bookmark naming ------------------------------- diff --git a/modules/eat-config.el b/modules/eat-config.el index f53baed31..1de24dc4f 100644 --- a/modules/eat-config.el +++ b/modules/eat-config.el @@ -27,6 +27,7 @@ (declare-function eat "eat" (&optional program arg)) (declare-function eshell "eshell" (&optional arg)) +(declare-function cj/dashboard-only "dashboard-config") (defvar eat-mode-map) (defvar eat-semi-char-mode-map) (defvar eshell-buffer-name) @@ -110,12 +111,17 @@ ARGS is (TERMINAL OUTPUT)." (eat-sixel-render-formats '(xpm svg half-block background none)) ; inline images (on by default) (eat-query-before-killing-running-terminal 'auto) ; confirm before killing a terminal with a live process :config - ;; F12 and C-; must reach Emacs from inside EAT. In semi-char mode (EAT's - ;; default) EAT forwards unbound keys to the terminal -- a letter runs + ;; F1, F12, and C-; must reach Emacs from inside EAT. In semi-char mode + ;; (EAT's default) EAT forwards unbound keys to the terminal -- a letter runs ;; `eat-self-input' -- so bind these explicitly or they never reach Emacs: - ;; F12 toggles the terminal window, C-; opens the global prefix map. + ;; F1 runs the kill-all sweep back to the dashboard (`cj/dashboard-only', + ;; which buries agent buffers rather than killing them), F12 toggles the + ;; terminal window, C-; opens the global prefix map. Unlike ghostel, EAT + ;; needs no exception-list or keymap rebuild -- the bind alone suffices. + (keymap-set eat-semi-char-mode-map "<f1>" #'cj/dashboard-only) (keymap-set eat-semi-char-mode-map "<f12>" #'cj/term-toggle) (keymap-set eat-semi-char-mode-map "C-;" cj/custom-keymap) + (keymap-set eat-mode-map "<f1>" #'cj/dashboard-only) (keymap-set eat-mode-map "<f12>" #'cj/term-toggle) (keymap-set eat-mode-map "C-;" cj/custom-keymap)) @@ -302,6 +308,16 @@ Escape." (interactive) (cj/--term-send-string "\e")) +(defun cj/term-backward-kill-word () + "Delete the previous word in the terminal program's input line. +Sends M-DEL (ESC DEL) to the pty, which readline and most line editors map to +backward-kill-word -- the same word-boundary delete C-<backspace> does in normal +Emacs buffers (it stops at punctuation). EAT's default forwards C-<backspace> as +a bare key the program ignores, so the word never gets deleted; sending the +escape sequence the program actually understands is what makes the key work." + (interactive) + (cj/--term-send-string "\e\d")) + (defun cj/term--tmux-output (&rest args) "Run tmux with ARGS and return its stdout. Signal `user-error' when tmux exits with a non-zero status." @@ -499,6 +515,10 @@ pty; without tmux, moves point up in EAT's emacs-mode buffer." ;; to semi-char. One key gets out of either copy view. (keymap-set eat-semi-char-mode-map "<escape>" #'cj/term-send-escape) (keymap-set eat-mode-map "<escape>" #'eat-semi-char-mode) + ;; Ctrl+Backspace deletes the previous word, matching its behavior in normal + ;; buffers. Terminals send no standard code for it, so EAT's default forwards + ;; a bare key the program drops; send M-DEL instead (readline backward-kill-word). + (keymap-set eat-semi-char-mode-map "C-<backspace>" #'cj/term-backward-kill-word) ;; Word-motion arrows edit the terminal program's input (claude, readline), so ;; forward them to the pty. EAT's default leaves them in the non-bound-keys ;; list, which moved Emacs point instead and desynced it from the real cursor diff --git a/modules/font-config.el b/modules/font-config.el index 2be051ddc..3aa3d80f6 100644 --- a/modules/font-config.el +++ b/modules/font-config.el @@ -83,11 +83,6 @@ (FiraCode-Literata :default-family "Fira Code Nerd Font" :variable-pitch-family "Literata") - (EBook - :default-family "Lexend" - :default-weight regular - :default-height 200 - :variable-pitch-family "Lexend") (24-point-font :default-height 240) (20-point-font @@ -165,32 +160,27 @@ If FRAME is nil, uses the selected frame." t nil)) -;; ------------------------------- All The Icons ------------------------------- -;; icons made available through fonts +;; ------------------------------- Nerd Icons fonts ---------------------------- +;; nerd-icons (configured in nerd-icons-config.el) renders glyphs from the +;; "Symbols Nerd Font Mono" font. Auto-install it on the first GUI frame when +;; it is missing -- the same convenience the dropped all-the-icons setup gave. -(declare-function all-the-icons-install-fonts "all-the-icons") +(declare-function nerd-icons-install-fonts "nerd-icons") -(defun cj/maybe-install-all-the-icons-fonts (&optional _frame) - "Install all-the-icons fonts if needed and we have a GUI." +(defun cj/maybe-install-nerd-icons-fonts (&optional _frame) + "Install the nerd-icons font if it is missing and we have a GUI." (when (and (env-gui-p) - (not (cj/font-installed-p "all-the-icons"))) - (all-the-icons-install-fonts t) + (not (cj/font-installed-p "Symbols Nerd Font Mono"))) + (nerd-icons-install-fonts t) ;; Remove this hook after successful installation - (remove-hook 'server-after-make-frame-hook #'cj/maybe-install-all-the-icons-fonts))) + (remove-hook 'server-after-make-frame-hook #'cj/maybe-install-nerd-icons-fonts))) -(use-package all-the-icons - :demand t - :config - ;; Handle both daemon and non-daemon modes +;; nerd-icons loads after this module (see init.el order), so defer the wiring +;; until it is present. Daemon: install on the first GUI frame; otherwise now. +(with-eval-after-load 'nerd-icons (if (daemonp) - (add-hook 'server-after-make-frame-hook #'cj/maybe-install-all-the-icons-fonts) - (cj/maybe-install-all-the-icons-fonts))) - -(use-package all-the-icons-nerd-fonts - :after all-the-icons - :demand t - :config - (all-the-icons-nerd-fonts-prefer)) + (add-hook 'server-after-make-frame-hook #'cj/maybe-install-nerd-icons-fonts) + (cj/maybe-install-nerd-icons-fonts))) ;; ----------------------------- Emoji Fonts Per OS ---------------------------- diff --git a/modules/keyboard-compat.el b/modules/keyboard-compat.el index 172f96c7b..9395b9c86 100644 --- a/modules/keyboard-compat.el +++ b/modules/keyboard-compat.el @@ -68,12 +68,6 @@ This runs after init to override any package settings." nerd-icons-icon-for-buffer)) (advice-add fn :around #'cj/--icon-blank-in-terminal))) -(with-eval-after-load 'all-the-icons - (dolist (fn '(all-the-icons-icon-for-file - all-the-icons-icon-for-dir - all-the-icons-icon-for-mode)) - (advice-add fn :around #'cj/--icon-blank-in-terminal))) - ;; ============================================================================= ;; GUI-specific fixes ;; ============================================================================= diff --git a/modules/nov-reading.el b/modules/nov-reading.el new file mode 100644 index 000000000..4134f4975 --- /dev/null +++ b/modules/nov-reading.el @@ -0,0 +1,282 @@ +;;; nov-reading.el --- Reading-view theme layer for nov-mode EPUBs -*- lexical-binding: t; -*- +;; author: Craig Jennings <c@cjennings.net> + +;;; Commentary: +;; +;; Layer: 4 (Added features). +;; Category: O (optional commands + faces). +;; Load shape: eager. +;; Eager reason: defines the reading faces and commands the nov launch hook and +;; keymap reference; the faces must exist for theme-studio's inventory too. +;; Top-level side effects: defface x9 (3 palettes + per-palette heading/link), +;; defcustoms, a defgroup, a defvar. +;; Runtime requires: none (face-remap and text-scale are built in). +;; Direct test load: yes. +;; +;; A small theme layer on top of the stock `nov' package (no fork): how an EPUB +;; *reads*, kept buffer-local so it never disturbs the frame or other buffers. +;; Two knobs: +;; +;; - Reading palette -- the background + foreground, as sepia / dark / light, +;; each a face the dupre theme / theme-studio own (registered as the +;; "nov-reading" bespoke app in theme-studio's face_data.py). +;; - Typography -- a serif family and a base height, with +/-/= adjusting the +;; page font size live via a buffer-local text-scale on top of the base. +;; The live size is remembered globally, so every book opens where you left +;; it; "=" returns to the base height. +;; +;; calibredb-epub-config.el owns the library/calibre side and the text-width / +;; centering layout; this module owns reading color and typography. Its launch +;; entry point `cj/nov-reading-setup' is called from that module's nov-mode hook. + +;;; Code: + +(defgroup cj/nov-reading nil + "Reading-view theming for nov-mode EPUBs." + :group 'cj) + +;; ----------------------------- Reading palettes ------------------------------ +;; nov renders through shr and defines no faces, so a palette is a buffer-local +;; face-remap of `default'. Each palette is one face carrying a :background and +;; :foreground, so the theme owns the real colors (the hex defaults here are a +;; starting point to tune in theme-studio). + +(defface cj/nov-reading-sepia + '((t :background "#1f1b16" :foreground "#c9b187")) + "Sepia reading palette for nov-mode: warm dark background, tan text." + :group 'cj/nov-reading) + +(defface cj/nov-reading-dark + '((t :background "#15140f" :foreground "#cfc8b8")) + "Dark reading palette for nov-mode: near-black background, light-gray text." + :group 'cj/nov-reading) + +(defface cj/nov-reading-light + '((t :background "#ece3cf" :foreground "#2a2622")) + "Light reading palette for nov-mode: cream background, near-black text." + :group 'cj/nov-reading) + +;; Structural faces: recolor shr's heading (h1-h6) and link faces per palette, +;; remapped buffer-local so the EPUB's hierarchy reads in the palette's accent +;; while mail/eww (the other shr consumers) keep the theme's shr colors. Heading +;; faces carry :foreground only -- shr's per-level height and weight survive the +;; relative remap; link faces add :underline so the cue reads as a link. + +(defface cj/nov-reading-sepia-heading + '((t :foreground "#e6c98a")) + "Heading accent for the sepia reading palette (recolors shr-h1..h6)." + :group 'cj/nov-reading) + +(defface cj/nov-reading-sepia-link + '((t :foreground "#c98f5a" :underline t)) + "Link accent for the sepia reading palette (recolors shr-link)." + :group 'cj/nov-reading) + +(defface cj/nov-reading-dark-heading + '((t :foreground "#e8e0cc")) + "Heading accent for the dark reading palette (recolors shr-h1..h6)." + :group 'cj/nov-reading) + +(defface cj/nov-reading-dark-link + '((t :foreground "#8fb0c4" :underline t)) + "Link accent for the dark reading palette (recolors shr-link)." + :group 'cj/nov-reading) + +(defface cj/nov-reading-light-heading + '((t :foreground "#5a3d28")) + "Heading accent for the light reading palette (recolors shr-h1..h6)." + :group 'cj/nov-reading) + +(defface cj/nov-reading-light-link + '((t :foreground "#8a5a2a" :underline t)) + "Link accent for the light reading palette (recolors shr-link)." + :group 'cj/nov-reading) + +(defcustom cj/nov-reading-palettes + '(("sepia" :face cj/nov-reading-sepia + :heading cj/nov-reading-sepia-heading + :link cj/nov-reading-sepia-link) + ("dark" :face cj/nov-reading-dark + :heading cj/nov-reading-dark-heading + :link cj/nov-reading-dark-link) + ("light" :face cj/nov-reading-light + :heading cj/nov-reading-light-heading + :link cj/nov-reading-light-link)) + "Alist of reading-palette NAME -> face property list for nov-mode. +Each entry's plist supplies the palette's colors, all theme-owned faces: + :face reading-view :background and :foreground, remapped onto `default' + :heading recolors shr's heading faces (h1-h6) for this palette + :link recolors shr's link face for this palette +The selector and cycle commands choose among these names. Add an entry to add a +palette; omit :heading or :link to leave that element at the theme's default." + :type '(alist :key-type string + :value-type + (plist :options ((:face face) (:heading face) (:link face)))) + :group 'cj/nov-reading) + +(defcustom cj/nov-reading-default-palette "sepia" + "Reading palette applied to a fresh nov-mode buffer. +A key in `cj/nov-reading-palettes', or nil for the theme's normal rendering." + :type '(choice (const :tag "None (theme default)" nil) string) + :group 'cj/nov-reading) + +(defvar-local cj/nov--reading-remap-cookies nil + "List of `face-remap-add-relative' cookies for the active reading palette. +Covers the `default' remap and any shr heading/link remaps, so switching +palettes can remove them all at once.") + +(defvar-local cj/nov--reading-palette nil + "Name of the reading palette active in this buffer, or nil for none.") + +(defun cj/nov--reading-palette-plist (name) + "Return the face property list for palette NAME, or nil when unknown. +NAME nil (the no-palette state) and unknown names both yield nil." + (cdr (assoc name cj/nov-reading-palettes))) + +(defun cj/nov--reading-palette-face (name) + "Return the base (bg/fg) face for palette NAME, or nil when NAME is unknown." + (plist-get (cj/nov--reading-palette-plist name) :face)) + +(defun cj/nov--next-reading-palette (current names) + "Return the palette after CURRENT in the cycle NAMES then nil, wrapping. +CURRENT nil is the no-palette state, and a returned nil means no palette. An +unknown CURRENT falls back to the first palette." + (let* ((cycle (append names (list nil))) + (tail (cdr (member current cycle)))) + (car (or tail cycle)))) + +(defun cj/nov--apply-reading-palette (name) + "Apply reading palette NAME buffer-local; NAME nil removes any palette. +Remaps `default' to the palette's :face, and (when present) shr's heading faces +h1-h6 to its :heading face and shr-link to its :link face. Removes the previous +palette's remaps first so switching never stacks, and leaves the typography +remap (a separate `default' remap) untouched." + (mapc #'face-remap-remove-relative cj/nov--reading-remap-cookies) + (setq cj/nov--reading-remap-cookies nil) + (let* ((plist (cj/nov--reading-palette-plist name)) + (face (plist-get plist :face))) + (when face + (push (face-remap-add-relative 'default face) + cj/nov--reading-remap-cookies) + (let ((heading (plist-get plist :heading))) + (when heading + (dolist (h '(shr-h1 shr-h2 shr-h3 shr-h4 shr-h5 shr-h6)) + (push (face-remap-add-relative h heading) + cj/nov--reading-remap-cookies)))) + (let ((link (plist-get plist :link))) + (when link + (push (face-remap-add-relative 'shr-link link) + cj/nov--reading-remap-cookies)))) + (setq cj/nov--reading-palette (and face name)))) + +(defun cj/nov-set-reading-palette (name) + "Choose reading palette NAME for this nov buffer; \"none\" clears it. +Interactively prompts among `cj/nov-reading-palettes' plus \"none\"." + (interactive + (list (completing-read "Reading palette: " + (cons "none" (mapcar #'car cj/nov-reading-palettes)) + nil t))) + (unless (derived-mode-p 'nov-mode) + (user-error "Not in a nov-mode buffer")) + (cj/nov--apply-reading-palette (unless (equal name "none") name)) + (message "Reading palette: %s" (or cj/nov--reading-palette "none"))) + +(defun cj/nov-cycle-reading-palette () + "Cycle to the next reading palette, then the no-palette state, wrapping." + (interactive) + (unless (derived-mode-p 'nov-mode) + (user-error "Not in a nov-mode buffer")) + (let ((next (cj/nov--next-reading-palette + cj/nov--reading-palette + (mapcar #'car cj/nov-reading-palettes)))) + (cj/nov--apply-reading-palette next) + (message "Reading palette: %s" (or next "none")))) + +;; ------------------------------- Typography ---------------------------------- + +(defcustom cj/nov-reading-font-family "Merriweather" + "Variable-pitch serif family for the EPUB reading view." + :type 'string + :group 'cj/nov-reading) + +(defcustom cj/nov-reading-text-height 180 + "Base `default'-face height (1/10 pt) the reading view renders at. +The +/-/= keys adjust the page size from here with a buffer-local text-scale. +That adjustment is remembered globally (see `cj/nov-reading-text-scale-file'): +every book and every session opens at the size you last left it, and `=' +returns to this base." + :type 'integer + :group 'cj/nov-reading) + +(defvar cj/nov-reading-text-scale-file + (expand-file-name "data/nov-reading-text-scale" user-emacs-directory) + "File persisting the global reading text-scale offset across sessions. +A single integer: the buffer-local `text-scale-mode-amount' the +/-/= keys +last set, applied on top of `cj/nov-reading-text-height' when a book opens.") + +(defun cj/nov-reading--parse-text-scale (s) + "Parse S (a string or nil) as an integer text-scale offset; 0 when invalid. +Surrounding whitespace is tolerated; non-integer content yields 0." + (let ((trimmed (and (stringp s) (string-trim s)))) + (if (and trimmed (string-match-p "\\`[+-]?[0-9]+\\'" trimmed)) + (string-to-number trimmed) + 0))) + +(defun cj/nov-reading--load-text-scale () + "Return the persisted reading text-scale offset, or 0 when none is saved." + (if (file-readable-p cj/nov-reading-text-scale-file) + (cj/nov-reading--parse-text-scale + (with-temp-buffer + (insert-file-contents cj/nov-reading-text-scale-file) + (buffer-string))) + 0)) + +(defun cj/nov-reading--save-text-scale (amount) + "Persist AMOUNT as the global reading text-scale offset. +Creates the data directory when absent." + (make-directory (file-name-directory cj/nov-reading-text-scale-file) t) + (with-temp-file cj/nov-reading-text-scale-file + (insert (number-to-string amount)))) + +(defun cj/nov-reading-apply-typography () + "Apply the reading family and base height buffer-local. +Remaps `variable-pitch', `default', and `fixed-pitch' so nov's shr output reads +as a comfortably-sized serif page." + (face-remap-add-relative 'variable-pitch + :family cj/nov-reading-font-family :height 1.0) + (face-remap-add-relative 'default + :family cj/nov-reading-font-family + :height cj/nov-reading-text-height) + (face-remap-add-relative 'fixed-pitch :height cj/nov-reading-text-height)) + +(defun cj/nov-reading-text-bigger () + "Increase the page font size and remember it across books and sessions." + (interactive) + (text-scale-increase 1) + (cj/nov-reading--save-text-scale text-scale-mode-amount)) + +(defun cj/nov-reading-text-smaller () + "Decrease the page font size and remember it across books and sessions." + (interactive) + (text-scale-decrease 1) + (cj/nov-reading--save-text-scale text-scale-mode-amount)) + +(defun cj/nov-reading-text-reset () + "Reset the page font size to the base reading height; clears the saved offset." + (interactive) + (text-scale-set 0) + (cj/nov-reading--save-text-scale 0)) + +;; ------------------------------- Launch hook --------------------------------- + +(defun cj/nov-reading-setup () + "Apply the reading view (typography + default palette) to this nov buffer. +Restores the remembered page font size on top of the base height. +Called from the nov-mode launch hook in calibredb-epub-config.el." + (cj/nov-reading-apply-typography) + (text-scale-set (cj/nov-reading--load-text-scale)) + (when cj/nov-reading-default-palette + (cj/nov--apply-reading-palette cj/nov-reading-default-palette))) + +(provide 'nov-reading) +;;; nov-reading.el ends here |
