diff options
Diffstat (limited to 'modules')
46 files changed, 639 insertions, 1186 deletions
diff --git a/modules/ai-term.el b/modules/ai-term.el index 3beabe6b5..6dfb669a9 100644 --- a/modules/ai-term.el +++ b/modules/ai-term.el @@ -1,4 +1,4 @@ -;;; ai-term.el --- In-Emacs AI-agent launcher with vertical-split terminal -*- lexical-binding: t; -*- +;;; ai-term.el --- AI-agent terminals backed by EAT and tmux -*- lexical-binding: t; -*- ;; Author: Craig Jennings <c@cjennings.net> @@ -7,70 +7,18 @@ ;; Layer: 3 (Domain Workflow). ;; Category: D. ;; Load shape: eager. -;; Eager reason: registers four global keys for the AI-agent terminal launcher; a -;; command-loaded deferral candidate. -;; Top-level side effects: four global key bindings. -;; Runtime requires: cl-lib, seq, cj-window-geometry-lib, cj-window-toggle-lib, -;; host-environment. +;; Eager reason: binds M-SPC and the C-; a AI-agent prefix. +;; Top-level side effects: global M-SPC binding and C-; a prefix map. +;; Runtime requires: cl-lib, seq, window-toggle/geometry helpers, host-environment. ;; Direct test load: yes. ;; -;; Picks an AI-agent project (a dir under ~/.emacs.d, ~/code/*, or -;; ~/projects/* containing .ai/protocols.org), opens or reuses a terminal -;; buffer named "agent [<basename>]", sends the agent's startup -;; instruction to it, and routes the buffer to a side window via -;; display-buffer-alist. When the frame already has a window forming the -;; half the agent would occupy (a right column on a desktop, a bottom row -;; on a laptop), the agent reuses that slot rather than splitting a third -;; window in; toggling off restores the displaced buffer to the slot. -;; Otherwise placement is a host-aware split: a right-side split at 50% -;; width on a desktop, a bottom split at 75% height on a laptop (see -;; `cj/--ai-term-default-direction'). Multiple -;; projects produce multiple coexisting buffers that share the same -;; slot; switching among them is a buffer-switch, not a -;; kill-and-recreate. +;; Opens project-scoped AI agents in EAT buffers backed by tmux sessions. Project +;; candidates come from configured roots that contain .ai/protocols.org. ;; -;; Each project's agent runs inside a tmux session named -;; "<cj/ai-term-tmux-session-prefix><basename>" (default prefix "aiv-"). -;; The prefix lets `tmux ls' be filtered to AI-term's own sessions, so -;; after an Emacs crash the project picker can match surviving sessions -;; back to their directories: matched projects sort to the top of the -;; picker (flagged "[detached]" -- session alive, no Emacs buffer -- or -;; "[running]" when a live terminal buffer exists), the rest follow in -;; alphabetical order. -;; -;; Four F-key entry points: -;; -;; - F9 `cj/ai-term' -- DWIM dispatch. If an agent buffer is -;; currently displayed in this frame, F9 toggles it off: when it -;; took over an existing window (a reused slot) the buffer it -;; displaced returns to that slot, when it was split into its own -;; window that window is removed, and when it fills the frame it -;; is buried. Otherwise, if exactly one agent buffer is alive, -;; F9 re-displays it; if zero or two-plus are alive, F9 falls -;; through to the project picker. -;; - C-F9 `cj/ai-term-pick-project' -- always show the project -;; picker, even when an agent buffer is currently displayed. -;; Used when the user wants to start a new project session -;; instead of toggling the current one. -;; - s-F9 `cj/ai-term-next' -- step to the next active agent in the -;; queue. The queue is every active agent in buffer-name order -;; (a stable rotation): attached agents (a live buffer) and -;; detached ones (a live tmux session with no Emacs buffer). -;; Stepping onto a detached agent attaches it. When an agent -;; window is on screen, swap it to the next agent and focus it, -;; wrapping after the last; when none is shown but agents exist, -;; show the first. This is the "switch among existing agents" -;; surface F9 deliberately doesn't provide. -;; - M-F9 `cj/ai-term-close' -- gracefully close an agent: kill its -;; tmux session (stopping the agent process), then its terminal -;; buffer. Its window stays in the layout (swapped to the -;; working buffer), so closing never collapses a split. Confirms -;; first. Targets the current agent, the sole live agent, or -;; prompts among several. -;; -;; Existing windmove (Shift-arrows) handles code <-> agent focus -;; toggling. Buffer-move (C-M-arrows) handles side-swap. Neither -;; needs anything new from this module. +;; Agent display reuses the host-appropriate side slot when possible, otherwise +;; splits right on desktop frames and below on laptop frames. Attached buffers +;; and detached tmux sessions share the same rotation; selecting a detached +;; agent recreates its EAT buffer and attaches to the live session. ;;; Code: @@ -82,6 +30,7 @@ (require 'keybindings) ;; provides cj/register-prefix-map (C-; a) (declare-function eat "eat" (&optional program arg)) +(declare-function cj/make-buffer-pattern-undead "undead-buffers") (defvar eat-buffer-name) (defvar eat-semi-char-mode-map) @@ -215,7 +164,7 @@ which the step materializes by attaching." Walks `buffer-list' (most-recently-selected first) and returns the first buffer that is not an AI-term agent buffer (per `cj/--ai-term-buffer-p') and is not an internal buffer (name starting -with a space). Used by the single-window F9 toggle-off so dismissing a +with a space). Used by the single-window toggle-off so dismissing a full-frame agent returns to the file the user was working in (e.g. todo.org) rather than swapping in another agent." (seq-find (lambda (b) @@ -287,7 +236,7 @@ looked up in SESSIONS, so the lossy whitespace->hyphen transform in (defun cj/--ai-term-launch-command (dir) "Return the shell command line that runs the AI tool in a project tmux session. -Uses `tmux new-session -A' so a second F9 on the same project reattaches +Uses `tmux new-session -A' so a second toggle on the same project reattaches to the running session instead of spawning a new one. The session name comes from `cj/--ai-term-tmux-session-name'; the first window is named `cj/ai-term-tmux-window-name' (default \"ai\") so a later hand-opened @@ -452,7 +401,7 @@ direction applies. Captured at toggle-off by `cj/--ai-term-display-saved'.") (defvar cj/--ai-term-last-was-bury nil - "Non-nil when the last F9 toggle-off used `bury-buffer'. + "Non-nil when the last toggle-off used `bury-buffer'. Set by `cj/ai-term' in its `toggle-off' branch: t when the agent window was the only window in the frame (so toggle-off buried @@ -462,7 +411,7 @@ buried agent in the current window (the only one) or splitting per the saved direction.") (defvar cj/--ai-term-last-toggle-deleted-split nil - "Non-nil when the last F9 toggle-off deleted the agent's own split window. + "Non-nil when the last toggle-off deleted the agent's own split window. Set t by `cj/--ai-term-toggle-off' only when it actually `delete-window's the agent (a multi-window layout where the agent had its own window); @@ -474,7 +423,7 @@ working window at the edge, displacing its buffer and collapsing the layout -- the toggle must be reversible (off then on returns the same windows).") (defvar cj/--ai-term-last-hidden-buffer nil - "The agent buffer hidden by the most recent F9 toggle-off. + "The agent buffer hidden by the most recent toggle-off. Captured in `cj/ai-term' just before an agent window is torn down, and consumed by `cj/--ai-term-dispatch' so the next toggle-on reopens the @@ -516,11 +465,24 @@ and a fraction-of-frame produces the wrong size on replay (squeezes the other windows). An integer is unambiguous, at the cost of not auto-scaling if the frame itself resizes.") +(defvar cj/--ai-term-last-fullscreen nil + "Non-nil when the agent window was last seen filling its frame. + +Maintained by `cj/--ai-term-track-geometry' on +`window-configuration-change-hook': set t whenever a live agent window is +the sole window in its frame, cleared when the agent is shown as a split +\(its dock direction and size are captured then instead). Consulted by +`cj/--ai-term-display-saved' so a summon into a single-window frame +restores the agent fullscreen rather than docking it -- the sole-window +state isn't a representable dock size, so this flag is how it round-trips. +Unlike `cj/--ai-term-last-was-bury' it does not depend on a toggle-off, so +it also covers leaving the agent by switching buffers or `C-x 1'.") + (defun cj/--ai-term-capture-state (window) "Capture WINDOW's direction and size into module-level state. Sets `cj/--ai-term-last-direction' and `cj/--ai-term-last-size' -so a subsequent F9 display can restore the user's chosen orientation +so a subsequent display can restore the user's chosen orientation and size. Called at toggle-off (just before the window is torn down). The default direction is host-aware via `cj/--ai-term-default-direction' (used only when WINDOW fills its @@ -532,6 +494,35 @@ is not live." 'cj/--ai-term-last-size '(right below left))) +(defun cj/--ai-term-window-sole-p (window) + "Return non-nil when WINDOW is the only live window in its frame. +A frame's sole window is its root window; once split, the root is an +internal window and no live window equals it." + (and (window-live-p window) + (eq window (frame-root-window (window-frame window))))) + +(defun cj/--ai-term-track-geometry (&rest _) + "Track whether the displayed agent window is fullscreen. + +Run from `window-configuration-change-hook'. Sets +`cj/--ai-term-last-fullscreen' to whether a live agent window is the sole +window in its frame, and leaves it untouched when no agent window is +displayed -- that retained value is the just-left state a later summon +replays. Dock direction and size stay owned by the toggle-off capture +\(`cj/--ai-term-capture-state'); this hook must not re-capture them, or the +repeated capture/replay drifts the dock height a couple rows per cycle." + (let ((win (cj/--ai-term-displayed-agent-window))) + (when (window-live-p win) + (setq cj/--ai-term-last-fullscreen (cj/--ai-term-window-sole-p win))))) + +(add-hook 'window-configuration-change-hook #'cj/--ai-term-track-geometry) + +;; Agent buffers ("agent [<project>]") are buried, not killed, by the +;; kill-all sweep (F1 / `cj/dashboard-only'). Register the family pattern so +;; every agent -- however and whenever created -- survives with its session. +(with-eval-after-load 'undead-buffers + (cj/make-buffer-pattern-undead "\\`agent \\[")) + (defun cj/--ai-term-reuse-existing-agent (buffer _alist) "Display-buffer action: reuse any window in this frame already showing an agent buffer. @@ -544,7 +535,7 @@ action in the chain runs. This is more specific than `display-buffer-use-some-window', which would happily steal any non-selected window (e.g. a code window above the agent split) when the user is focused in agent and -swaps projects via C-F9. The selective lookup here keeps non-agent +swaps projects via C-; a s. The selective lookup here keeps non-agent windows undisturbed and preserves the user's split geometry across project changes." (let ((win (cj/--ai-term-displayed-agent-window))) @@ -592,19 +583,27 @@ keeping the toggle reversible." win)))) (defun cj/--ai-term-display-saved (buffer alist) - "Display-buffer action: split per saved direction and size. + "Display-buffer action: restore fullscreen in a single-window frame, +otherwise split per saved direction and size. -When the prior toggle-off was a bury (single-window state, flagged -via `cj/--ai-term-last-was-bury') and the frame is still single- -window, restore the agent into the selected window in place rather -than splitting -- preserves the user's lone-window layout across -F9 toggles. +When the frame is a single window and the agent was last fullscreen +\(`cj/--ai-term-last-fullscreen', tracked by `cj/--ai-term-track-geometry') +or the prior toggle-off was a single-window bury +\(`cj/--ai-term-last-was-bury'), restore the agent into the selected window +in place rather than splitting. This round-trips a fullscreen agent -- +left by toggle-off, `C-x 1', or switching buffers -- since the sole-window +state isn't a representable dock size. Otherwise delegates to `cj/window-toggle-display-saved' against the -F9 state vars, falling back to the host-aware defaults from +toggle state vars, falling back to the host-aware defaults from `cj/--ai-term-default-direction' and `cj/--ai-term-default-size'." (cond - ((and cj/--ai-term-last-was-bury (one-window-p)) + ;; NOMINI t: don't count an active minibuffer as a second window. A summon + ;; can run with a picker prompt up, and a bare `one-window-p' then returns + ;; nil on a structurally single-window frame, misfiring the fullscreen + ;; restore into a dock -- which clears the fullscreen flag and cascades. + ((and (or cj/--ai-term-last-fullscreen cj/--ai-term-last-was-bury) + (one-window-p t)) (setq cj/--ai-term-last-was-bury nil) (let ((win (selected-window))) (set-window-buffer win buffer) @@ -627,7 +626,7 @@ through four actions in order: 2. `cj/--ai-term-reuse-existing-agent' -- otherwise, if any window in this frame already shows an agent-prefixed buffer, swap its buffer for the new one (preserves geometry across - project changes via C-F9). + project changes via C-; a s). 3. `cj/--ai-term-reuse-edge-window' -- otherwise, if the frame already has a window forming the half the agent would occupy (the right column on a desktop, the bottom row on a laptop), @@ -773,17 +772,17 @@ Signals `user-error' when no candidates exist." (expand-file-name chosen))))) (defun cj/--ai-term-dispatch () - "Compute the F9 (`cj/ai-term') action without performing it. + "Compute the `cj/ai-term' (C-; a a) action without performing it. Returns one of: - (toggle-off . WINDOW) -- agent is displayed in WINDOW; quit it. - (redisplay-recent . BUFFER) -- 1+ alive agent buffers; show MRU. - (pick-project) -- zero alive agent buffers; prompt. -When 2+ agent buffers are alive, F9 redisplays the most-recently- -selected one rather than opening the project picker. C-F9 is the -explicit \"start a different project\" surface; M-F9 is the explicit -\"switch among existing agents\" surface. F9 keeps a single, simple +When 2+ agent buffers are alive, C-; a a redisplays the most-recently- +selected one rather than opening the project picker. C-; a s is the +explicit \"start a different project\" surface; C-; a n is the explicit +\"switch among existing agents\" surface. C-; a a keeps a single, simple job: toggle whichever agent was last in use. A pure-decision helper so the dispatch logic is exercisable in tests @@ -816,7 +815,7 @@ buffers; reinvoking on the same project reuses its existing terminal. With prefix ARG, display the buffer without selecting its window. -Bound to C-F9 -- always shows the project picker, even when an agent +Bound to C-; a s -- always shows the project picker, even when an agent buffer is currently displayed. EAT renders in terminal frames as well as GUI frames, so this @@ -842,7 +841,7 @@ the agent itself." (other-buffer (window-buffer win) t))))) (defun cj/--ai-term-toggle-off (win) - "Hide the agent shown in WIN for an F9 toggle-off. Always returns nil. + "Hide the agent shown in WIN for a toggle-off. Always returns nil. Two cases, by window count: @@ -855,7 +854,7 @@ Two cases, by window count: force a swap to a non-agent buffer to keep the toggle observable. - Multi-window: collapse the agent split outright by deleting its window, so - the working buffer (e.g. todo.org) reclaims the space. F9 is a pure + the working buffer (e.g. todo.org) reclaims the space. The toggle is a pure show/hide toggle of THE agent split -- it must never surface a different agent. `quit-restore-window' can't guarantee that here: switching among several agents reuses the one slot via `set-window-buffer' (see @@ -897,21 +896,21 @@ Two cases, by window count: nil) (defun cj/ai-term (&optional arg) - "Smart F9 dispatch for the AI-term launcher. + "DWIM dispatch for the AI-term launcher. Bound to C-; a a. Behavior depends on the current state: -- If an AI-term buffer is currently displayed in this frame, F9 +- If an AI-term buffer is currently displayed in this frame, it quits its window (toggle off, buffer stays alive). -- Else, if exactly one alive AI-term buffer exists, F9 re-displays +- Else, if exactly one alive AI-term buffer exists, it re-displays it (DWIM -- the obvious next step is to look at it). -- Else (zero or 2+), F9 falls through to `cj/ai-term-pick-project'. +- Else (zero or 2+), it falls through to `cj/ai-term-pick-project'. With prefix ARG, display the buffer without selecting its window when a buffer is being shown (no effect on the toggle-off branch). -See `cj/ai-term-pick-project' (C-F9) to force the project picker. -M-F9 closes an agent via `cj/ai-term-close'." +See `cj/ai-term-pick-project' (C-; a s) to force the project picker. +C-; a k closes an agent via `cj/ai-term-close'." (interactive "P") (pcase (cj/--ai-term-dispatch) (`(toggle-off . ,win) @@ -945,7 +944,7 @@ Derives the tmux session name from BUFFER's `default-directory' (the project dir the terminal was created in) and kills it so the agent process stops. When BUFFER is shown, swaps its window to a non-agent buffer (the working file) rather than deleting the window -- closing an -agent must not collapse the user's window layout; the F9 hide toggle is +agent must not collapse the user's window layout; the hide toggle is what collapses the split. Then kills BUFFER (suppressing the process-still-running prompt -- the session is already down). No-op when BUFFER isn't an AI-term buffer." @@ -959,6 +958,8 @@ when BUFFER isn't an AI-term buffer." (let ((kill-buffer-query-functions nil)) (kill-buffer buffer)))) +(require 'system-lib) + (defun cj/--ai-term-close-target () "Return the AI-term buffer `cj/ai-term-close' should act on, or nil. @@ -973,7 +974,8 @@ buffers; nil when none are alive." ((null (cdr buffers)) (car buffers)) (t (get-buffer (completing-read "Close AI terminal: " - (mapcar #'buffer-name buffers) nil t)))))))) + (cj/completion-table 'buffer (mapcar #'buffer-name buffers)) + nil t)))))))) (defun cj/ai-term-close () "Gracefully close an AI-term agent: kill its tmux session and buffer. @@ -981,7 +983,7 @@ buffers; nil when none are alive." Targets the current agent buffer, the sole live agent, or prompts when several are alive (see `cj/--ai-term-close-target'). Asks for confirmation first -- this kills the running agent process, which can -interrupt work in progress. Bound to M-<f9>." +interrupt work in progress. Bound to C-; a k." (interactive) (let ((buffer (cj/--ai-term-close-target))) (unless buffer diff --git a/modules/auth-config.el b/modules/auth-config.el index 62d773057..c2df244b5 100644 --- a/modules/auth-config.el +++ b/modules/auth-config.el @@ -1,4 +1,4 @@ -;; auth-config.el --- Configuration for Authentication Utilities -*- lexical-binding: t; coding: utf-8; -*- +;;; auth-config.el --- Authentication and GPG integration -*- lexical-binding: t; coding: utf-8; -*- ;; author Craig Jennings <c@cjennings.net> ;;; Commentary: @@ -6,29 +6,16 @@ ;; Layer: 1 (Foundation). ;; Category: F/D. ;; Load shape: eager. -;; Eager reason: auth-source and GPG/epa setup that other modules rely on for -;; credentials early in the session. -;; Top-level side effects: auth-source/epa configuration via use-package and setq. +;; Eager reason: credentials and GPG setup are needed by other modules early. +;; Top-level side effects: auth-source/epa setup and oauth2-auto cache advice. ;; Runtime requires: system-lib, user-constants. -;; Direct test load: yes (configuration only). +;; Direct test load: yes. ;; -;; Configuration for Emacs authentication and GPG integration: - -;; • auth-source -;; – Forces use of your default authinfo file -;; – Disable external GPG agent in favor of Emacs's own prompt -;; – Keeps auth-source debug logging disabled by default - -;; • Easy PG Assistant (epa) -;; – Force using the 'gpg2' executable for encryption/decryption operations - -;; • oauth2-auto cache fix (via advice) -;; – oauth2-auto version 20250624.1919 has caching bug on line 206 -;; – Function oauth2-auto--plstore-read has `or nil` disabling cache -;; – This caused GPG passphrase prompts every ~15 minutes during gcal-sync -;; – Fix: Advice to enable hash-table cache without modifying package -;; – Works across package updates -;; – Fixed 2025-11-11 +;; Central auth-source, GPG, and credential-debug setup. Auth lookups use the +;; configured authinfo file; passphrase caching is left to gpg-agent. +;; +;; Advises oauth2-auto's plstore reader to restore in-memory caching and avoid +;; repeated GPG prompts during calendar/mail refreshes. ;;; Code: diff --git a/modules/browser-config.el b/modules/browser-config.el index d596b9e9d..564e7a275 100644 --- a/modules/browser-config.el +++ b/modules/browser-config.el @@ -76,7 +76,10 @@ Includes built-in Emacs browsers (those with nil executable)." (defun cj/save-browser-choice (browser-plist) "Save BROWSER-PLIST to the persistence file." (with-temp-file cj/browser-choice-file - (insert ";;; Browser choice - Auto-generated\n") + (insert ";;; browser-choice.el --- Generated browser selection -*- lexical-binding: t; -*-\n") + (insert ";;\n") + (insert ";; Generated by browser-config.el. Do not edit by hand; use\n") + (insert ";; `cj/choose-browser' to rewrite this file.\n") (insert (format "(setq cj/saved-browser-choice '%S)\n" browser-plist)))) (defun cj/load-browser-choice () @@ -102,7 +105,6 @@ Returns: \\='success if applied successfully, (program-var (plist-get browser-plist :program-var))) (if (null browse-fn) 'invalid-plist - ;; Set the main browse-url function (setq browse-url-browser-function browse-fn) ;; Set the specific browser program variable if it exists (when program-var diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el index c0e0e935a..b684330c8 100644 --- a/modules/calendar-sync.el +++ b/modules/calendar-sync.el @@ -8,75 +8,17 @@ ;; Layer: 3 (Domain Workflow). ;; Category: D/S. ;; Load shape: eager only when calendar-sync.local.el configures calendars. -;; Eager reason: daily-driver workflow; calendars are expected synced at the -;; first session. Timers and network fetches are guarded for batch/test loads. -;; Top-level side effects: defines a calendar keymap and conditionally registers -;; it under cj/custom-keymap; timer and network fetches guarded by -;; config/noninteractive checks. +;; Eager reason: daily agenda workflow; timers and network fetches are guarded. +;; Top-level side effects: defines C-; g map; starts sync only when configured. ;; Runtime requires: cl-lib, subr-x, system-lib, cj-org-text-lib, keybindings. -;; Direct test load: yes (private config optional; degrades cleanly when absent). +;; Direct test load: yes. ;; -;; Simple, reliable one-way sync from multiple calendars to Org mode. -;; Downloads .ics files from calendar URLs (Google, Proton, etc.) and -;; converts to Org format. No OAuth, no API complexity, just file conversion. +;; One-way calendar synchronization from configured .ics/API sources into Org +;; files. Feed URLs may be inline or resolved from auth-source via :secret-host. ;; -;; Features: -;; - Multi-calendar support (sync multiple calendars to separate files) -;; - Pure Emacs Lisp .ics parser (no external dependencies) -;; - Recurring event support (RRULE expansion) -;; - Timer-based automatic sync (every 60 minutes, configurable) -;; - Self-contained in .emacs.d (no cron, portable across machines) -;; - Read-only (can't corrupt source calendars) -;; - Works with Chime for event notifications -;; -;; Recurring Events (RRULE): -;; -;; Calendar recurring events are defined once with an RRULE -;; (recurrence rule) rather than as individual event instances. This -;; module expands recurring events into individual org entries. -;; -;; Expansion uses a rolling window approach: -;; - Past: 3 months before today -;; - Future: 12 months after today -;; -;; Every sync regenerates the entire file based on the current date, -;; so the window automatically advances as time passes. Old events -;; naturally fall off after 3 months, and new future events appear -;; as you approach them. -;; -;; Supported RRULE patterns: -;; - FREQ=DAILY: Daily events -;; - FREQ=WEEKLY;BYDAY=MO,WE,FR: Weekly on specific days -;; - FREQ=MONTHLY: Monthly events (same day each month) -;; - FREQ=YEARLY: Yearly events (anniversaries, birthdays) -;; - INTERVAL: Repeat every N periods (e.g., every 2 weeks) -;; - UNTIL: End date for recurrence -;; - COUNT: Maximum occurrences (combined with date range limit) -;; -;; Setup: -;; 1. Configure calendars in your init.el: -;; (setq calendar-sync-calendars -;; '((:name "google" -;; :url "https://calendar.google.com/calendar/ical/.../basic.ics" -;; :file gcal-file) -;; (:name "proton" -;; :url "https://calendar.proton.me/api/calendar/v1/url/.../calendar.ics" -;; :file pcal-file))) -;; -;; 2. Load and start: -;; (require 'calendar-sync) -;; (calendar-sync-start) -;; -;; 3. Add to org-agenda (optional): -;; (dolist (cal calendar-sync-calendars) -;; (add-to-list 'org-agenda-files (plist-get cal :file))) -;; -;; Usage: -;; - M-x calendar-sync-now ; Sync all or select specific calendar -;; - M-x calendar-sync-start ; Start auto-sync -;; - M-x calendar-sync-stop ; Stop auto-sync -;; - M-x calendar-sync-toggle ; Toggle auto-sync -;; - M-x calendar-sync-status ; Show sync status for all calendars +;; The parser expands recurring events into a rolling window around today, +;; regenerates target Org files on each sync, and keeps source calendars +;; read-only. Commands under C-; g start, stop, toggle, inspect, and run syncs. ;;; Code: diff --git a/modules/calibredb-epub-config.el b/modules/calibredb-epub-config.el index 1e6437d26..38aa0de05 100644 --- a/modules/calibredb-epub-config.el +++ b/modules/calibredb-epub-config.el @@ -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" ()) @@ -116,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 @@ -184,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 ------------------------------ @@ -207,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 @@ -404,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) @@ -432,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." @@ -449,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. diff --git a/modules/config-utilities.el b/modules/config-utilities.el index f448327c1..0c98a896c 100644 --- a/modules/config-utilities.el +++ b/modules/config-utilities.el @@ -114,11 +114,14 @@ Signals `user-error' if METHOD-SYMBOL is nil or not fboundp." (with-timer title (funcall method-symbol))) +(require 'system-lib) + (defun cj/benchmark-this-method () "Prompt for a title and method name, then time the execution of the method." (interactive) (let* ((title (read-string "Enter the title for the timing: ")) - (method-name (completing-read "Enter the method name to time: " obarray + (method-name (completing-read "Enter the method name to time: " + (cj/completion-table 'function obarray) #'fboundp t)) (method-symbol (intern-soft method-name))) (condition-case err diff --git a/modules/custom-comments.el b/modules/custom-comments.el index 231a03860..a2604a558 100644 --- a/modules/custom-comments.el +++ b/modules/custom-comments.el @@ -5,62 +5,17 @@ ;; Layer: 2 (Core UX). ;; Category: L/C. ;; Load shape: eager. -;; Eager reason: registers its C-; C comment submap at load. Currently eager by -;; init order; a deferral candidate for Phase 3/4 (command/autoload + -;; registration API). -;; Top-level side effects: defines cj/comment-map, registers it under C-; C. +;; Eager reason: registers C-; C comment helpers. +;; Top-level side effects: defines and registers cj/comment-map. ;; Runtime requires: keybindings. -;; Direct test load: yes (requires keybindings explicitly). +;; Direct test load: yes. ;; -;; This module provides custom comment formatting and manipulation utilities for code editing. -;; -;; Functions include: -;; - deleting all comments in a buffer, -;; - reformatting commented text into single-line paragraphs, -;; - creating centered comment headers with customizable separator characters, -;; - creating comment boxes around text -;; - inserting hyphen-style centered comments. -;; -;; These utilities help create consistent, well-formatted code comments and section headers. -;; Bound to keymap prefix: C-; C -;; -;; Comment Style Patterns: -;; -;; inline-border: -;; ========== inline-border ========== -;; -;; simple-divider: -;; ==================================== -;; simple-divider -;; ==================================== -;; -;; padded-divider: -;; ==================================== -;; padded-divider -;; ==================================== -;; -;; box: -;; ************************************ -;; * box * -;; ************************************ -;; -;; heavy-box: -;; ************************************ -;; * * -;; * heavy-box * -;; * * -;; ************************************ -;; -;; unicode-box: -;; ┌──────────────────────────────────┐ -;; │ unicode-box │ -;; └──────────────────────────────────┘ -;; -;; block-banner: -;; /************************************ -;; * block-banner -;; ************************************/ +;; Comment editing helpers: delete comments, reflow commented regions, and insert +;; consistent section headers or boxes using the current mode's comment syntax. ;; +;; Public commands live under C-; C. Decoration helpers validate single printable +;; characters before generating comment borders. + ;;; Code: (require 'keybindings) ;; provides cj/custom-keymap diff --git a/modules/custom-datetime.el b/modules/custom-datetime.el index 6bca494d8..0528688c2 100644 --- a/modules/custom-datetime.el +++ b/modules/custom-datetime.el @@ -1,4 +1,4 @@ -;;; custom-datetime.el --- -*- coding: utf-8; lexical-binding: t; -*- +;;; custom-datetime.el --- Insert formatted date and time strings -*- coding: utf-8; lexical-binding: t; -*- ;;; Commentary: ;; @@ -12,32 +12,8 @@ ;; Runtime requires: keybindings. ;; Direct test load: yes (requires keybindings explicitly). ;; -;; Utilities for inserting date/time stamps in multiple formats. -;; -;; Interactive commands: -;; - cj/insert-readable-date-time -;; - cj/insert-sortable-date-time -;; - cj/insert-sortable-time -;; - cj/insert-readable-time -;; - cj/insert-sortable-date -;; - cj/insert-readable-date -;; -;; Each command is generated by `cj/--define-datetime-inserter' from a -;; corresponding format variable: -;; readable-date-time-format, sortable-date-time-format, -;; sortable-time-format, readable-time-format, -;; sortable-date-format, readable-date-format. -;; Customize these (see `format-time-string') to change output. -;; Some defaults include a trailing space for convenient typing. -;; -;; Key bindings: -;; A prefix map `cj/datetime-map' is installed on "d" under `cj/custom-keymap': -;; r → readable date+time -;; s → sortable date+time -;; t → sortable time -;; T → readable time -;; d → sortable date -;; D → readable date +;; Date/time insertion commands under C-; d. Each command is generated from a +;; customizable format variable and inserts format-time-string output at point. ;; ;;; Code: diff --git a/modules/custom-line-paragraph.el b/modules/custom-line-paragraph.el index 2cbcecc16..dd2999c4e 100644 --- a/modules/custom-line-paragraph.el +++ b/modules/custom-line-paragraph.el @@ -1,4 +1,4 @@ -;;; custom-line-paragraph.el --- -*- coding: utf-8; lexical-binding: t; -*- +;;; custom-line-paragraph.el --- Line and paragraph editing commands -*- coding: utf-8; lexical-binding: t; -*- ;; Author: Craig Jennings <c@cjennings.net> ;; ;;; Commentary: @@ -14,16 +14,9 @@ ;; Runtime requires: keybindings (expand-region on demand via declare-function). ;; Direct test load: yes (requires keybindings explicitly). ;; -;; This module provides the following line and paragraph manipulation utilities: -;; -;; - joining lines in a region or the current line with the previous one -;; - joining separate lines into a single paragraph -;; - duplicating lines or regions (optional commenting) -;; - removing duplicate lines -;; - removing lines containing specific text -;; - underlining text with a custom character -;; -;; Bound to keymap prefix C-; l +;; Line and paragraph transforms under C-; l: join, duplicate, delete matching +;; lines, remove duplicates, and underline text. Commands operate on the active +;; region when present and otherwise on the current line or paragraph. ;; ;;; Code: diff --git a/modules/custom-ordering.el b/modules/custom-ordering.el index 0a499a35a..4dc5bff84 100644 --- a/modules/custom-ordering.el +++ b/modules/custom-ordering.el @@ -1,4 +1,4 @@ -;;; custom-ordering.el --- -*- coding: utf-8; lexical-binding: t; -*- +;;; custom-ordering.el --- Region sorting and list-format transforms -*- coding: utf-8; lexical-binding: t; -*- ;;; Commentary: ;; @@ -13,22 +13,10 @@ ;; declare-function). ;; Direct test load: yes (requires keybindings explicitly). ;; -;; Text transformation and sorting utilities for reformatting data structures. +;; Region transforms under C-; o for sorting, reversing, numbering, quote +;; toggling, and converting between line lists and comma-separated arrays. +;; Helpers preserve trailing newlines where line-oriented callers expect them. ;; -;; Array/list formatting: -;; - arrayify/listify - convert lines to comma-separated format (with/without quotes, brackets) -;; - unarrayify - convert arrays back to separate lines -;; -;; Line manipulation: -;; - toggle-quotes - swap double ↔ single quotes -;; - reverse-lines - reverse line order -;; - number-lines - add line numbers with custom format (supports zero-padding) -;; - alphabetize-region - sort words alphabetically -;; - comma-separated-text-to-lines - split CSV text into lines -;; -;; Convenience functions: listify, arrayify-json, arrayify-python -;; Bound to keymap prefix C-; o - ;;; Code: (require 'cl-lib) diff --git a/modules/custom-text-enclose.el b/modules/custom-text-enclose.el index 5b1b00a71..4d72347d1 100644 --- a/modules/custom-text-enclose.el +++ b/modules/custom-text-enclose.el @@ -1,4 +1,4 @@ -;;; custom-text-enclose.el --- -*- coding: utf-8; lexical-binding: t; -*- +;;; custom-text-enclose.el --- Wrap, unwrap, and prefix text ranges -*- coding: utf-8; lexical-binding: t; -*- ;;; Commentary: ;; @@ -12,23 +12,10 @@ ;; Runtime requires: keybindings (change-inner on demand via declare-function). ;; Direct test load: yes (requires keybindings explicitly). ;; -;; Text enclosure utilities for wrapping and line manipulation. +;; Text enclosure commands under C-; s. Commands wrap or unwrap the active +;; region/word at point, and add prefixes, suffixes, indentation, or dedentation +;; across selected lines. ;; -;; Wrapping functions: -;; - surround-word-or-region - wrap text with same delimiter on both sides -;; - wrap-word-or-region - wrap with different opening/closing delimiters -;; - unwrap-word-or-region - remove surrounding delimiters -;; -;; Line manipulation: -;; - append-to-lines - add suffix to each line -;; - prepend-to-lines - add prefix to each line -;; - indent-lines - add leading whitespace (spaces or tabs) -;; - dedent-lines - remove leading whitespace -;; -;; Most functions work on region or entire buffer when no region is active. -;; -;; Bound to keymap prefix C-; s - ;;; Code: (require 'keybindings) ;; provides cj/custom-keymap diff --git a/modules/custom-whitespace.el b/modules/custom-whitespace.el index 0d4d1cc06..cbf3eff12 100644 --- a/modules/custom-whitespace.el +++ b/modules/custom-whitespace.el @@ -1,4 +1,4 @@ -;;; custom-whitespace.el --- -*- coding: utf-8; lexical-binding: t; -*- +;;; custom-whitespace.el --- Whitespace cleanup commands -*- coding: utf-8; lexical-binding: t; -*- ;;; Commentary: ;; @@ -12,19 +12,10 @@ ;; Runtime requires: keybindings. ;; Direct test load: yes (requires keybindings explicitly). ;; -;; This module provides whitespace manipulation operations for cleaning and transforming whitespace in text. - -;; Functions include: - -;; - removing leading and trailing whitespace -;; - collapsing multiple spaces to single spaces -;; - deleting blank lines -;; - converting whitespace to hyphens. - -;; All operations work on the current line, active region, or entire buffer depending on context. - -;; Bound to keymap prefix C-; w - +;; Whitespace cleanup under C-; w: trim line edges, collapse runs of spaces, +;; delete blank lines, enforce a single blank line, and hyphenate whitespace. +;; Commands choose region, buffer, or current line based on prefix/mark state. +;; ;;; Code: (require 'keybindings) ;; provides cj/custom-keymap diff --git a/modules/dashboard-config.el b/modules/dashboard-config.el index 17a0e2c4a..53f19b72b 100644 --- a/modules/dashboard-config.el +++ b/modules/dashboard-config.el @@ -54,6 +54,7 @@ (declare-function nerd-icons-mdicon "nerd-icons") (declare-function nerd-icons-codicon "nerd-icons") (declare-function nerd-icons-octicon "nerd-icons") +(declare-function nerd-icons-wicon "nerd-icons") ;; user-constants.el provides the home-directory constant. (defvar user-home-dir) @@ -139,6 +140,7 @@ Adjust this if the title doesn't appear centered under the banner image.") (list "d" #'nerd-icons-faicon "nf-fa-folder_o" "Files" "Dirvish File Manager" (lambda () (dirvish user-home-dir))) (list "t" #'nerd-icons-devicon "nf-dev-terminal" "Terminal" "Launch Terminal" (lambda () (cj/term-toggle))) (list "a" #'nerd-icons-mdicon "nf-md-calendar" "Agenda" "Main Org Agenda" (lambda () (cj/main-agenda-display))) + (list "w" #'nerd-icons-wicon "nf-weather-day_sunny_overcast" "Weather" "Wttrin Weather Forecast" (lambda () (call-interactively #'wttrin))) (list "r" #'nerd-icons-faicon "nf-fa-rss_square" "Feeds" "Elfeed Feed Reader" (lambda () (cj/elfeed-open))) (list "b" #'nerd-icons-codicon "nf-cod-library" "Books" "Calibre Ebook Reader" (lambda () (calibredb))) (list "f" #'nerd-icons-mdicon "nf-md-school" "Flashcards" "Org-Drill" (lambda () (cj/drill-start))) @@ -152,9 +154,10 @@ Adjust this if the title doesn't appear centered under the banner image.") "Dashboard launcher table: (KEY ICON-FN ICON-NAME LABEL TOOLTIP ACTION). Drives both `dashboard-navigator-buttons' and the dashboard-mode-map keys.") -(defconst cj/dashboard--row-sizes '(4 4 3 3) +(defconst cj/dashboard--row-sizes '(5 4 3 3) "Navigator row lengths. Must sum to the number of `cj/dashboard--launchers'. -The last row groups Slack, Linear, and Signal together.") +The top row carries Weather alongside the core tools; the last row groups +Slack, Linear, and Signal together.") (defun cj/dashboard--navigator-button (l) "Build a `dashboard-navigator-buttons' entry from launcher L." @@ -272,7 +275,7 @@ system-defaults) are preserved rather than overwritten." (setq initial-buffer-choice (lambda () (get-buffer "*dashboard*")))) ;; don't display dashboard if opening a file (setq dashboard-display-icons-p t) ;; display icons on both GUI and terminal (setq dashboard-icon-type 'nerd-icons) ;; use `nerd-icons' package - (setq dashboard-set-file-icons t) ;; per-filetype icons on the list items (nerd-icons colors them by type) + (setq dashboard-set-file-icons nil) ;; no per-item icons on the list entries: URL bookmarks have no filename, so they'd render iconless next to file items -- dropping them all keeps the lists uniform (setq dashboard-set-heading-icons t) ;; nerd-icons on the section titles (Projects/Bookmarks/Recent) (setq dashboard-center-content t) ;; horizontally center dashboard content (setq dashboard-bookmarks-show-path nil) ;; don't show paths in bookmarks diff --git a/modules/dev-fkeys.el b/modules/dev-fkeys.el index 9fdfa5b3f..80b43600b 100644 --- a/modules/dev-fkeys.el +++ b/modules/dev-fkeys.el @@ -5,48 +5,17 @@ ;; Layer: 2 (Core UX). ;; Category: C. ;; Load shape: eager. -;; Eager reason: the F4/F6 developer command entry points. -;; Top-level side effects: six global F-key bindings; conditionally registers a -;; C-; P binding. +;; Eager reason: binds the F4/F6 developer command entry points. +;; Top-level side effects: global F-key bindings and optional C-; P binding. ;; Runtime requires: cl-lib, system-lib, keybindings. -;; Direct test load: yes (requires keybindings explicitly). +;; Direct test load: yes. ;; -;; Project-aware F-key block for developer workflows: +;; Project-aware F-key dispatchers. F4 chooses compile/run/clean commands by +;; project markers; C-F4 and M-F4 are fast paths. F6 runs all project tests or +;; the current file's tests using language-specific command builders. ;; -;; F4 completing-read of compile/run candidates filtered by project type -;; C-F4 fast path: compile only (no-op on interpreted projects) -;; M-F4 fast path: clean + rebuild (no-op on interpreted projects) -;; S-F4 recompile (built-in) -;; F6 completing-read of test candidates: All tests / Current file's tests -;; C-F6 fast path: current file's tests -;; -;; F4 project-type detection runs against the projectile root and falls back -;; to \\='unknown when no marker matches. Interpreted markers are checked -;; before compiled markers, so a Python or Node project that also has a -;; Makefile for tasks classifies as interpreted. -;; -;; F6 \"All tests\" delegates to `projectile-test-project'. F6 \"Current -;; file's tests\" detects the language by extension, derives the runner -;; command (elisp via the project Makefile, Python via pytest, Go via the -;; package), and pipes through `compile' from the projectile root. -;; TypeScript / JavaScript are detected but punted for v1 — the function -;; signals a user-error rather than guessing a runner. -;; -;; M-F6 is reserved for Phase 2b (\"Run a test...\" menu entry with -;; per-language test-name discovery). Phase 2b also adds buffer-local -;; last-test memory and tree-sitter-based discovery for Python / Go / -;; TypeScript. The tree-sitter discovery uses a capture-then-filter pattern -;; (queries without `:match' / `:equal' / `:pred' predicates, with the -;; pattern filter applied in Elisp) to sidestep Emacs bug #79687 — Emacs -;; 30.2 emits unsuffixed `#match' predicates that libtree-sitter 0.26 -;; rejects. The fix lives on Emacs master (commit b0143530) and is -;; targeted at Emacs 31; it has not been backported to the emacs-30 -;; branch as of 2026-05-03. See Mike Olson's writeup at -;; https://mwolson.org/blog/emacs/2026-04-20-fixing-typescript-ts-mode-in-emacs-30-2/ -;; for the same workaround applied to font-lock. -;; -;; F7 (coverage) is wired in coverage-core.el. F5 is reserved for the debug -;; ticket and intentionally left unbound here. +;; Interpreted markers win over compiled markers so task Makefiles do not turn +;; Python/Node projects into compile-first projects. ;;; Code: diff --git a/modules/dwim-shell-config.el b/modules/dwim-shell-config.el index 014194c7b..e8790a489 100644 --- a/modules/dwim-shell-config.el +++ b/modules/dwim-shell-config.el @@ -1,99 +1,23 @@ -;; dwim-shell-config.el --- Dired Shell Commands -*- coding: utf-8; lexical-binding: t; -*- +;;; dwim-shell-config.el --- Dired shell command menu -*- coding: utf-8; lexical-binding: t; -*- ;; ;;; Commentary: ;; ;; Layer: 3 (Domain Workflow). ;; Category: D/P. ;; Load shape: eager. -;; Eager reason: none; Dired/Dirvish shell commands, a command-loaded deferral -;; candidate. +;; Eager reason: none; Dired/Dirvish shell commands can load by command. ;; Top-level side effects: package configuration via use-package. -;; Runtime requires: cl-lib. +;; Runtime requires: cl-lib, system-lib. ;; Direct test load: yes. ;; -;; This module provides a collection of DWIM (Do What I Mean) shell commands -;; for common file operations in Dired and other buffers. It leverages the -;; `dwim-shell-command' package to execute shell commands on marked files -;; with smart templating and progress tracking. -;; -;; Features: -;; - Audio/Video conversion (mp3, opus, webp, HEVC) -;; - Image manipulation (resize, flip, format conversion) -;; - PDF operations (merge, split, password protection, OCR) -;; - Archive management (zip/unzip) -;; - Document conversion (epub to org, docx to pdf, pdf to txt) -;; - Git operations (clone from clipboard) -;; - External file opening with context awareness -;; -;; Workflow: -;; 1. *Mark files in Dired/Dirvish* -;; - Use =m= to mark individual files -;; - Use =* .= to mark by extension -;; - Use =% m= to mark by regexp -;; - Or operate on the file under cursor if nothing is marked -;; -;; 2. *Execute a DWIM command* -;; - Call the command via =M-x dwim-shell-commands-[command-name]= -;; - Or bind frequently used commands to keys -;; -;; 3. *Command execution* -;; - The command runs asynchronously in the background -;; - A =*Async Shell Command*= buffer shows progress -;; - Files are processed with smart templating (replacing =<<f>>=, =<<fne>>=, etc.) -;; -;; 4. *Results* -;; - New files appear in the Dired/Dirvish buffer -;; - Buffer auto-refreshes when command completes -;; - Errors appear in the async buffer if something fails -;; -;; Requirements: -;; The commands rely on various external utilities that need to be installed: -;; - ffmpeg: Audio/video conversion -;; - imagemagick (convert): Image manipulation -;; - qpdf: PDF operations (requires version 8.x+ for secure password handling) -;; - tesseract: OCR functionality -;; - pandoc: Document conversion -;; - atool: Archive extraction -;; - rsvg-convert: SVG to PNG conversion -;; - pdftotext: PDF text extraction -;; - git: Version control operations -;; - gpgconf: GPG agent management -;; - 7z (p7zip): Secure password-protected archives -;; -;; On Arch Linux, install the requirements with: -;; #+begin_src bash -;; sudo pacman -S --needed ffmpeg imagemagick qpdf tesseract tesseract-data-eng pandoc atool librsvg poppler git gnupg p7zip zip unzip mkvtoolnix-cli mpv ruby -;; #+end_src -;; -;; On MacOS, install the requirements with: -;; #+begin_src bash -;; brew install ffmpeg imagemagick qpdf tesseract pandoc atool librsvg poppler gnupg p7zip mkvtoolnix mpv -;; #+end_src -;; -;; Usage: -;; Commands operate on marked files in Dired or the current file in other modes. -;; The package automatically replaces standard shell commands with DWIM versions -;; for a more intuitive experience. -;; -;; Security: -;; Password-protected operations (PDF encryption, archive encryption) use secure -;; methods to avoid exposing passwords in process lists or command history: -;; - PDF operations: Use temporary files with restrictive permissions (mode 600) -;; - Archive operations: Use 7z instead of zip for better password handling -;; - Temporary password files are automatically cleaned up after use -;; - Note: Switched from zip to 7z for encryption due to zip's insecure -P flag -;; -;; Template Variables: -;; - <<f>>: Full path to file -;; - <<fne>>: File name without extension -;; - <<e>>: File extension -;; - <<b>>: Base name (file name with extension, no directory) -;; - <<d>>: Directory path -;; - <<n>>: Sequential number (for batch renaming) -;; - <<td>>: Temporary directory -;; - <<cb>>: Clipboard contents -;; - <<*>>: All marked files +;; Configures dwim-shell-command actions for marked Dired/Dirvish files: +;; media conversion, archive/PDF/document operations, external opening, and a +;; curated transient menu. Commands use dwim-shell templates for marked files or +;; the current buffer file. ;; +;; Password-bearing operations avoid command-line secrets by writing temporary +;; password files with restrictive permissions and deleting them from the process +;; sentinel after the spawned command exits. ;;; Code: diff --git a/modules/eat-config.el b/modules/eat-config.el index ee83adf10..f53baed31 100644 --- a/modules/eat-config.el +++ b/modules/eat-config.el @@ -479,8 +479,21 @@ pty; without tmux, moves point up in EAT's emacs-mode buffer." (defvar eat-mode-map) (declare-function eat-semi-char-mode "eat") (declare-function eat-self-input "eat") + +(defun cj/eat-text-scale-reset () + "Reset the text scale to its default in the current buffer." + (interactive) + (text-scale-set 0)) + (with-eval-after-load 'eat (keymap-set eat-semi-char-mode-map "C-<up>" #'cj/term-copy-mode-up) + ;; Zoom-out and reset reach Emacs, not the pty. EAT binds C-- to + ;; eat-self-input (forwarded to the terminal), so without this the font can + ;; only grow: C-= / C-+ pass through and zoom in, but C-- never reaches + ;; text-scale-decrease. Low cost -- the Claude TUI and tmux don't use Ctrl+-, + ;; and C-0 shadows digit-argument inside eat buffers only. + (keymap-set eat-semi-char-mode-map "C--" #'text-scale-decrease) + (keymap-set eat-semi-char-mode-map "C-0" #'cj/eat-text-scale-reset) ;; Escape forwards ESC to the pty, so it cancels tmux copy-mode (tmux binds ;; Escape to cancel) and works in TUIs; in EAT's own emacs/char mode it returns ;; to semi-char. One key gets out of either copy view. diff --git a/modules/elfeed-config.el b/modules/elfeed-config.el index eb2659ab5..e5cbb36c0 100644 --- a/modules/elfeed-config.el +++ b/modules/elfeed-config.el @@ -41,6 +41,13 @@ (declare-function eww-browse-url "eww") (declare-function eww-readable "eww") +;; elfeed paints its search and entry buffers with manual `face' text properties +;; (the date, title, feed, and tag faces the theme styles). Left in +;; `global-font-lock-mode', font-lock overwrites those with syntactic string +;; fontification, so the buffer loses the theme colors. Exclude both modes, the +;; same reason dashboard and mu4e are excluded. +(cj/exclude-from-global-font-lock 'elfeed-search-mode 'elfeed-show-mode) + ;; ------------------------------- Elfeed Config ------------------------------- (use-package elfeed diff --git a/modules/erc-config.el b/modules/erc-config.el index 3e98a66a3..4eac812c4 100644 --- a/modules/erc-config.el +++ b/modules/erc-config.el @@ -140,6 +140,8 @@ Change this value to use a different nickname.") server-buffers)) +(require 'system-lib) + (defun cj/erc-switch-to-buffer-with-completion () "Switch to an ERC buffer using completion. If no ERC buffers exist, prompt to connect to a server. @@ -148,7 +150,7 @@ Buffer names are shown with server context for clarity." (let* ((erc-buffers (erc-buffer-list)) (buffer-names (mapcar #'buffer-name erc-buffers))) (if buffer-names - (let ((selected (completing-read "Switch to ERC buffer: " buffer-names nil t))) + (let ((selected (completing-read "Switch to ERC buffer: " (cj/completion-table 'buffer buffer-names) nil t))) (switch-to-buffer selected)) (message "No ERC buffers found.") (when (y-or-n-p "Connect to an IRC server? ") diff --git a/modules/eww-config.el b/modules/eww-config.el index ff7ddc211..3b0e22dcd 100644 --- a/modules/eww-config.el +++ b/modules/eww-config.el @@ -73,6 +73,12 @@ ;; --------------------------------- EWW Config -------------------------------- +(require 'system-lib) +;; eww renders pages with shr, which paints with manual `face' properties. Left +;; in `global-font-lock-mode' font-lock overwrites them and the page loses its +;; colors, the same issue as elfeed-show and mu4e-view. Exclude eww-mode. +(cj/exclude-from-global-font-lock 'eww-mode) + (use-package eww :ensure nil ;; built-in :bind diff --git a/modules/flycheck-config.el b/modules/flycheck-config.el index 1afd3ae6c..613817444 100644 --- a/modules/flycheck-config.el +++ b/modules/flycheck-config.el @@ -6,40 +6,17 @@ ;; Layer: 2 (Core UX). ;; Category: C/P. ;; Load shape: eager. -;; Eager reason: general linting setup; spec target is hook-loaded, a deferral -;; candidate. -;; Top-level side effects: package configuration via use-package, binds into -;; cj/custom-keymap through use-package :map. +;; Eager reason: linting keymap and mode hooks; could become hook-loaded. +;; Top-level side effects: package config and C-; ? binding. ;; Runtime requires: keybindings. -;; Direct test load: yes (requires keybindings explicitly). +;; Direct test load: yes. ;; -;; This file configures Flycheck for on-demand syntax and grammar checking. -;; - Flycheck starts automatically only in sh-mode and emacs-lisp-mode - -;; - This binds a custom helper (=cj/flycheck-list-errors=) to "C-; ?" -;; for popping up Flycheck's error list in another window. - -;; - It also customizes Checkdoc to suppress only the "sentence-end-double-space" -;; and "warn-escape" warnings. - -;; - It registers LanguageTool for comprehensive grammar checking of prose files -;; (text-mode, markdown-mode, gfm-mode, org-mode). - -;; Note: Grammar checking is on-demand only to avoid performance issues. -;; Hitting "C-; ?" runs cj/flycheck-prose-on-demand if in an org buffer. - -;; The cj/flycheck-prose-on-demand function: -;; - Turns on flycheck for the local buffer -;; - Enables LanguageTool checker -;; - Triggers an immediate check -;; - Displays errors in the *Flycheck errors* buffer - -;; Installation: -;; On Arch Linux: -;; sudo pacman -S languagetool +;; Flycheck configuration for automatic shell/Elisp linting and on-demand prose +;; grammar checks. C-; ? opens the Flycheck error list, enabling prose checking +;; first when appropriate. ;; -;; The wrapper script at scripts/languagetool-flycheck formats LanguageTool's -;; JSON output into flycheck-compatible format. It requires Python 3. +;; LanguageTool uses scripts/languagetool-flycheck to adapt JSON output to +;; Flycheck's checker protocol. ;;; Code: diff --git a/modules/flyspell-and-abbrev.el b/modules/flyspell-and-abbrev.el index 376a9dc51..b73bfdf32 100644 --- a/modules/flyspell-and-abbrev.el +++ b/modules/flyspell-and-abbrev.el @@ -6,48 +6,18 @@ ;; Layer: 2 (Core UX). ;; Category: C/P. ;; Load shape: eager. -;; Eager reason: text-mode spelling and abbrev hooks; spec target is hook-loaded. -;; Top-level side effects: package configuration via use-package (mode hooks). +;; Eager reason: text-mode spelling and abbrev hooks. +;; Top-level side effects: package configuration via use-package. ;; Runtime requires: cl-lib. ;; Direct test load: yes. ;; -;; WORKFLOW: -;; This module provides intelligent spell checking with automatic abbreviation -;; creation to prevent repeated misspellings. +;; On-demand Flyspell workflow with automatic abbrev creation from accepted +;; corrections. C-' checks/corrects nearby misspellings; C-c f toggles Flyspell +;; with mode-aware behavior. ;; -;; KEYBINDINGS: -;; C-' - Main spell check interface (cj/flyspell-then-abbrev) -;; C-c f - Toggle flyspell on/off (cj/flyspell-toggle) -;; M-o - Access 'other options' during correction (save to dictionary, etc.) -;; -;; SPELL CHECKING WORKFLOW: -;; 1. Press C-' to start spell checking -;; 2. Finds the nearest misspelled word above the cursor -;; 3. Prompts for correction or allows saving to personal dictionary -;; 4. Press C-' again to move to the next misspelling -;; 5. Each correction automatically creates an abbrev for future auto-expansion -;; -;; FLYSPELL ACTIVATION: -;; Flyspell is NOT automatically enabled. You activate it manually: -;; - C-c f - Toggle flyspell on (uses smart mode detection) or off -;; - C-' - Runs flyspell-buffer then starts correction workflow -;; -;; When enabled, flyspell adapts to the buffer type: -;; - Programming modes (prog-mode): Only checks comments and strings -;; - Text modes (text-mode): Checks all text -;; - Other modes: Must enable manually with C-c f -;; -;; ABBREVIATION AUTO-EXPANSION: -;; Each spell correction creates an abbrev that auto-expands the misspelling -;; to the correct spelling when you type it in the future. This significantly -;; increases typing speed over time. -;; -;; Original idea from Artur Malabarba: -;; http://endlessparentheses.com/ispell-and-abbrev-the-perfect-auto-correct.html -;; -;; NOTES: -;; The default flyspell keybinding "C-;" is unbound in this config as it's -;; used for the custom keymap (cj/custom-keymap). +;; Flyspell is not enabled globally. Programming buffers check comments/strings +;; when enabled; prose buffers check all text. The default C-; Flyspell binding +;; is intentionally left free for cj/custom-keymap. ;;; Code: diff --git a/modules/font-config.el b/modules/font-config.el index 3272a946e..7617ba01e 100644 --- a/modules/font-config.el +++ b/modules/font-config.el @@ -6,51 +6,18 @@ ;; Layer: 2 (Core UX). ;; Category: C/P/S. ;; Load shape: eager. -;; Eager reason: font setup for the first frame, plus font keybindings. -;; Top-level side effects: binds five global font keys, runs font-installation -;; checks, configures packages via use-package. +;; Eager reason: first-frame font setup and font keybindings. +;; Top-level side effects: font keys, font checks, package config. ;; Runtime requires: host-environment, keybindings. ;; Direct test load: yes. ;; -;; This module provides font configuration, including: -;; -;; 1. Font Management: -;; - Dynamic font preset switching via `fontaine' package -;; - Separate configurations for fixed-pitch and variable-pitch fonts -;; - Multiple size presets for different viewing contexts -;; - Per-frame font configuration tracking for daemon mode compatibility -;; -;; 2. Icon Support: -;; - All-the-icons integration with automatic font installation -;; - Nerd fonts support for enhanced icons in terminals and GUI -;; - Platform-specific emoji font configuration (Noto, Apple, Segoe) -;; - Emojify package for emoji rendering and insertion -;; -;; 3. Typography Enhancements: -;; - Programming ligatures via `ligature' package -;; - Mode-specific ligature rules for markdown and programming -;; - Text scaling keybindings for quick size adjustments -;; -;; 4. Utility Functions: -;; - `cj/font-installed-p': Check font availability -;; - `cj/display-available-fonts': Interactive font browser with samples -;; - Frame-aware font application for client/server setups -;; -;; Configuration Notes: -;; - Default preset: BerkeleyMono Nerd Font; height 120 on laptops, 140 on desktops -;; - Variable pitch: Lexend in the default preset; Merriweather for fallback presets -;; - Handles both standalone and daemon mode Emacs instances -;; - Emoji fonts selected based on OS availability -;; -;; Keybindings: -;; - M-S-f: Select font preset (fontaine-set-preset) -;; - C-z F: Display available fonts -;; - C-+/C-=: Increase text scale -;; - C--/C-_: Decrease text scale -;; - C-c E i: Insert emoji -;; - C-c E l: List emojis -;; +;; Configures fontaine presets, text scaling keys, icon/emoji fonts, and +;; programming ligatures. Presets are applied per frame so daemon clients get +;; the intended fixed/variable pitch sizes. ;; +;; Also carries font-rendering safeguards for known HarfBuzz/font-cache crashes +;; triggered by emoji and Arabic shaping in this setup. + ;;; Code: (require 'host-environment) @@ -226,7 +193,6 @@ If FRAME is nil, uses the selected frame." (all-the-icons-nerd-fonts-prefer)) ;; ----------------------------- Emoji Fonts Per OS ---------------------------- -;; Set emoji fonts in priority order (first found wins) (defun cj/setup-emoji-fontset (&optional _frame) "Set emoji fonts in priority order (first found wins). diff --git a/modules/jumper.el b/modules/jumper.el index 61b6464a5..1fbd1293b 100644 --- a/modules/jumper.el +++ b/modules/jumper.el @@ -11,72 +11,17 @@ ;; Layer: 4 (Optional). ;; Category: O/L. ;; Load shape: eager. -;; Eager reason: none; navigation helper, a command-loaded deferral candidate. -;; Top-level side effects: defines a jumper keymap. +;; Eager reason: none; jump commands can autoload. +;; Top-level side effects: defines jumper keymap. ;; Runtime requires: cl-lib. ;; Direct test load: yes. ;; -;; Jumper provides a simple way to store and jump between locations -;; in your codebase without needing to remember register assignments. +;; Small register-backed jump list. Locations are stored in numbered registers, +;; shown through completion with file/line context, and removed explicitly when +;; no longer useful. ;; -;; PURPOSE: -;; -;; When working on large codebases, you often need to jump between -;; multiple related locations: a function definition, its tests, its -;; callers, configuration files, etc. Emacs registers are perfect for -;; this, but require you to remember which register you assigned to -;; which location. Jumper automates register management, letting you -;; focus on your work instead of bookkeeping. -;; -;; WORKFLOW: -;; -;; 1. Navigate to an important location in your code -;; 2. Press M-SPC SPC to store it (automatically assigned to register 0) -;; 3. Continue working, storing more locations as needed (registers 1-9) -;; 4. Press M-SPC j to jump back to any stored location -;; 5. Select from the list using completion (shows file, line, context) -;; 6. Press M-SPC d to remove locations you no longer need -;; -;; RECOMMENDED USAGE: -;; -;; Store locations temporarily while working on a feature: -;; - Store the main function you're implementing -;; - Store the test file where you're writing tests -;; - Store the caller that needs updating -;; - Store the documentation that needs changes -;; - Jump between them freely as you work -;; - Clear them when done with the feature -;; -;; SPECIAL BEHAVIORS: -;; -;; - Duplicate prevention: Storing the same location twice shows a message -;; instead of wasting a register slot. -;; -;; - Single location toggle: When only one location is stored, M-SPC j -;; toggles between that location and your current position. Perfect for -;; rapid back-and-forth between two related files. -;; -;; - Last location tracking: The last position before each jump is saved -;; in register 'z', allowing quick "undo" of navigation. -;; -;; - Smart selection: With multiple locations, completing-read shows -;; helpful context: "[0] filename.el:42 - function definition..." -;; -;; KEYBINDINGS: -;; -;; M-SPC SPC Store current location in next available register -;; M-SPC j Jump to a stored location (with completion) -;; M-SPC d Delete a stored location from the list -;; -;; CONFIGURATION: -;; -;; You can customize the prefix key and maximum locations: -;; -;; (setq jumper-prefix-key "C-c j") ; Change prefix key -;; (setq jumper-max-locations 20) ; Store up to 20 locations -;; -;; Note: Changing jumper-max-locations requires restarting Emacs or -;; manually reinitializing jumper--registers. +;; A single stored location toggles with the current point; each jump records the +;; previous point in register z for a quick return path. ;;; Code: diff --git a/modules/keyboard-compat.el b/modules/keyboard-compat.el index 914a343a6..172f96c7b 100644 --- a/modules/keyboard-compat.el +++ b/modules/keyboard-compat.el @@ -6,90 +6,18 @@ ;; Layer: 1 (Foundation). ;; Category: F/S. ;; Load shape: eager. -;; Eager reason: normalizes terminal/GUI key input so the first session's -;; keybindings resolve consistently. -;; Top-level side effects: adds `cj/keyboard-compat-terminal-setup' to -;; `emacs-startup-hook'. +;; Eager reason: normalizes terminal/GUI key input before custom bindings matter. +;; Top-level side effects: adds cj/keyboard-compat-terminal-setup to startup. ;; Runtime requires: host-environment. -;; Direct test load: yes (registers a startup hook; batch-safe). +;; Direct test load: yes. ;; -;; This module fixes keyboard input differences between terminal and GUI Emacs. +;; Normalizes Meta+Shift bindings across GUI and terminal frames. GUI frames +;; translate M-uppercase events to explicit M-S-lowercase keys; terminal frames +;; decode arrow escape sequences before key lookup so ESC O prefixes do not trip +;; M-S bindings. ;; -;; THE PROBLEM: Meta+Shift keybindings behave differently in terminal vs GUI -;; ========================================================================= -;; -;; In Emacs, there are two ways to express "Meta + Shift + o": -;; -;; 1. M-O (Meta + uppercase O) - key code 134217807 -;; 2. M-S-o (Meta + explicit Shift modifier + lowercase o) - key code 167772271 -;; -;; These are NOT the same key in Emacs! -;; -;; GUI Emacs behavior: -;; When you press Meta+Shift+o on your keyboard, GUI Emacs receives M-O -;; (uppercase O). It does NOT receive M-S-o. This is because the keyboard -;; sends Shift+o as uppercase 'O', not as a Shift modifier plus lowercase 'o'. -;; -;; Terminal Emacs behavior: -;; Terminals send escape sequences for special keys. Arrow keys send: -;; - Up: ESC O A -;; - Down: ESC O B -;; - Right: ESC O C -;; - Left: ESC O D -;; -;; The problem: ESC O is interpreted as M-O by Emacs! So if you bind M-O -;; to a function, pressing the up arrow sends "ESC O A", Emacs sees "M-O" -;; and triggers your function instead of moving up. Arrow keys break. -;; -;; THE SOLUTION: Different handling for each display type -;; ====================================================== -;; -;; For terminal mode (handled by cj/keyboard-compat-terminal-setup): -;; - Use input-decode-map to translate arrow escape sequences BEFORE -;; any keybinding lookup. ESC O A becomes [up], not M-O followed by A. -;; - Keybindings use M-S-o syntax (some terminals support explicit Shift) -;; - Disable graphical icons that show as unicode artifacts -;; -;; For GUI mode (handled by cj/keyboard-compat-gui-setup): -;; - Use key-translation-map to translate M-O to M-S-o BEFORE lookup -;; - This way, pressing Meta+Shift+o (which sends M-O) gets translated -;; to M-S-o, matching the keybinding definitions -;; - All 18 Meta+Shift keybindings work correctly -;; -;; WHY NOT JUST USE M-O FOR KEYBINDINGS? -;; ===================================== -;; -;; We could bind to M-O directly, but: -;; 1. Terminal arrow keys would break (ESC O prefix conflict) -;; 2. We'd need to maintain two sets of bindings (M-O for GUI, something -;; else for terminal) -;; -;; By using M-S-o syntax everywhere and translating M-O -> M-S-o in GUI mode, -;; we have one consistent set of keybindings that work everywhere. -;; -;; KEYBINDINGS AFFECTED: -;; ==================== -;; -;; The following M-S- keybindings are translated from M-uppercase in GUI: -;; -;; M-O -> M-S-o cj/kill-other-window (undead-buffers.el) -;; M-M -> M-S-m cj/kill-all-other-buffers-and-windows (undead-buffers.el) -;; M-Y -> M-S-y yank-media (keybindings.el) -;; M-F -> M-S-f fontaine-set-preset (font-config.el) -;; M-W -> M-S-w wttrin (weather-config.el) -;; M-E -> M-S-e eww (eww-config.el) -;; M-L -> M-S-l cj/switch-themes (ui-theme.el) -;; M-R -> M-S-r cj/elfeed-open (elfeed-config.el) -;; M-V -> M-S-v cj/split-and-follow-right (ui-navigation.el) -;; M-H -> M-S-h cj/split-and-follow-below (ui-navigation.el) -;; M-T -> M-S-t toggle-window-split (ui-navigation.el) -;; M-Z -> M-S-z cj/undo-kill-buffer (ui-navigation.el) -;; M-U -> M-S-u winner-undo (ui-navigation.el) -;; M-D -> M-S-d dwim-shell-commands-menu (dwim-shell-config.el) -;; M-I -> M-S-i edit-indirect-region (text-config.el) -;; M-C -> M-S-c time-zones (chrono-tools.el) -;; M-B -> M-S-b calibredb (calibredb-epub-config.el) -;; M-K -> M-S-k show-kill-ring (show-kill-ring.el) +;; Also provides terminal-specific display fallbacks, such as hiding icon glyphs +;; that render poorly outside GUI frames. ;;; Code: diff --git a/modules/local-repository.el b/modules/local-repository.el index 9ce7a1af3..a9df09d38 100644 --- a/modules/local-repository.el +++ b/modules/local-repository.el @@ -1,4 +1,4 @@ -;;; local-repository.el --- local repository functionality -*- lexical-binding: t; coding: utf-8; -*- +;;; local-repository.el --- Local package archive helpers -*- lexical-binding: t; coding: utf-8; -*- ;; author Craig Jennings <c@cjennings.net> ;;; Commentary: @@ -6,12 +6,15 @@ ;; Layer: 4 (Optional). ;; Category: O/D/P. ;; Load shape: eager. -;; Eager reason: none; local package-mirror workflow, a command-loaded deferral -;; candidate. +;; Eager reason: none; local package mirror commands can autoload. ;; Top-level side effects: none. -;; Runtime requires: elpa-mirror. +;; Runtime requires: elpa-mirror when updating the mirror. ;; Direct test load: yes. ;; +;; Adds the checked-in local package archive to package-archives with high +;; priority, and provides a command to refresh that archive from installed +;; packages via elpa-mirror. + ;;; Code: (require 'elpa-mirror nil t) ;; optional; cj/update-localrepo-repository fails at call-time if absent diff --git a/modules/mousetrap-mode.el b/modules/mousetrap-mode.el index 3817e0081..656d49e2f 100644 --- a/modules/mousetrap-mode.el +++ b/modules/mousetrap-mode.el @@ -1,4 +1,4 @@ -;;; mousetrap-mode.el --- -*- coding: utf-8; lexical-binding: t; -*- +;;; mousetrap-mode.el --- Profile-based mouse event blocking -*- coding: utf-8; lexical-binding: t; -*- ;; ;;; Commentary: ;; @@ -11,25 +11,12 @@ ;; Runtime requires: cl-lib. ;; Direct test load: yes. ;; -;; Mouse Trap Mode is a minor mode for Emacs that disables most mouse and -;; trackpad events to prevent accidental text modifications. Hitting the -;; trackpad and finding my text is being inserted in an unintended place is -;; quite annoying, especially when you're overcaffeinated. +;; Global minor mode that blocks accidental mouse edits while preserving allowed +;; interaction categories per major-mode profile: scroll, click, drag, and +;; multi-click. ;; -;; The mode uses a profile-based architecture to selectively enable/disable -;; mouse events based on the current major mode. Profiles define which -;; event categories are allowed (scrolling, clicks, drags, etc.), and modes -;; are mapped to profiles. -;; -;; The keymap is built dynamically when the mode is toggled, so you can -;; change profiles or mode mappings and re-enable the mode without reloading -;; your Emacs configuration. -;; -;; Keymaps are buffer-local via `emulation-mode-map-alists', so each buffer -;; gets the correct profile for its major mode independently. -;; -;; Inspired by this blog post from Malabarba -;; https://endlessparentheses.com/disable-mouse-only-inside-emacs.html +;; The mode builds buffer-local emulation keymaps from profiles, so changing a +;; profile or mode mapping takes effect after toggling the mode. ;; ;;; Code: diff --git a/modules/mu4e-org-contacts-integration.el b/modules/mu4e-org-contacts-integration.el index daa12701a..6062b8cf5 100644 --- a/modules/mu4e-org-contacts-integration.el +++ b/modules/mu4e-org-contacts-integration.el @@ -2,8 +2,13 @@ ;; author: Craig Jennings <c@cjennings.net> ;;; Commentary: -;; This module provides seamless integration between org-contacts and mu4e's -;; email composition, enabling automatic contact completion in email fields. +;; +;; Completion-at-point integration between org-contacts and mu4e/org-msg compose +;; buffers. Header fields complete against org contact email strings; message +;; bodies keep their normal TAB behavior. +;; +;; Dependencies are optional at file load. Activation is a no-op when mu4e or +;; org-contacts is unavailable so the wider config can still load. ;;; Code: diff --git a/modules/mu4e-org-contacts-setup.el b/modules/mu4e-org-contacts-setup.el index 64e9a611f..bfb9b1f24 100644 --- a/modules/mu4e-org-contacts-setup.el +++ b/modules/mu4e-org-contacts-setup.el @@ -2,8 +2,10 @@ ;; author: Craig Jennings <c@cjennings.net> ;;; Commentary: -;; Simple setup file to enable org-contacts integration with mu4e. -;; Add this to your mail-config.el or load it after both mu4e and org-contacts. +;; +;; Thin activation wrapper for mu4e-org-contacts-integration. If mu4e is loaded, +;; enable org-contacts completion and disable mu4e's internal contact collector +;; so completion has one source of truth. ;;; Code: diff --git a/modules/music-config.el b/modules/music-config.el index 76fff283b..86f6eb130 100644 --- a/modules/music-config.el +++ b/modules/music-config.el @@ -5,90 +5,18 @@ ;; Layer: 4 (Optional). ;; Category: O/D/P/S. ;; Load shape: eager. -;; Eager reason: none; optional music workflow that registers a music keymap, a -;; command-loaded deferral candidate. EMMS hooks should run only after EMMS. -;; Top-level side effects: defines a music keymap under cj/custom-keymap, one -;; global key, package config. +;; Eager reason: none; optional music workflow that registers a music keymap. +;; Top-level side effects: defines C-; m map, one global key, package config. ;; Runtime requires: subr-x, user-constants, keybindings. -;; Direct test load: yes (requires keybindings explicitly). +;; Direct test load: yes. ;; -;; Music management in Emacs via EMMS with MPV backend. -;; Focus: simple, modular helpers; consistent error handling; streamlined UX. -;; -;; Highlights: -;; - Fuzzy add: select files/dirs; dirs have trailing /; case-insensitive; stable order -;; - Recursive directory add -;; - Dired/Dirvish integration (add selection) -;; - M3U playlist save/load/edit/reload -;; - Radio station M3U creation (streaming URLs supported) -;; - Playlist window toggling -;; - Consume mode (remove tracks after playback) -;; - MPV as player (no daemon required) -;; -;; Keybindings (playlist-mode-map): -;; -;; Aligned with ncmpcpp defaults where possible (83% match). -;; Additional EMMS-specific bindings for features ncmpcpp lacks. -;; -;; Key Action ncmpcpp default Match -;; ─── ────── ─────────────── ───── -;; Playback -;; SPC pause add_item * -;; s stop stop ✓ -;; > / n next track next ✓ -;; < / P previous track previous ✓ -;; p play selected (enter) ✓ -;; f seek forward seek_forward ✓ -;; b seek backward seek_backward ✓ -;; -;; Toggles -;; r repeat playlist toggle_repeat ✓ -;; t repeat track (none) + -;; z random toggle_random ✓ -;; x consume toggle_crossfade * -;; Z shuffle shuffle ✓ -;; -;; Volume -;; + / = volume up volume_up ✓ -;; - volume down volume_down ✓ -;; -;; Info -;; i song info show_song_info ✓ -;; o jump to playing jump_to_playing ✓ -;; -;; Playlist management -;; a add music (fuzzy) add_selected ✓ -;; c / C clear playlist clear_playlist ✓ -;; S save playlist (none) + -;; L load playlist (none) + -;; E edit playlist M3U (none) + -;; g reload playlist (none) + -;; A append track to M3U (none) + -;; q quit/bury quit ✓ -;; -;; Track reordering -;; S-up move track up (shift-up) ✓ -;; S-down move track down (shift-down) ✓ -;; C-up move track up (alias) (none) + -;; C-down move track down (alias) (none) + -;; -;; Other -;; R create radio station (none) + -;; -;; Legend: ✓ = matches ncmpcpp default -;; * = intentional divergence (see below) -;; + = EMMS-only feature -;; -;; Intentional divergences from ncmpcpp defaults: -;; -;; SPC/p swap: ncmpcpp defaults p=pause, SPC=add_item_to_playlist. -;; This config uses SPC=pause (more natural in Emacs) and p=play -;; selected track. Pause via SPC is a common media player convention. -;; -;; x=consume vs crossfade: ncmpcpp's crossfade is an mpd daemon -;; feature. EMMS uses mpv directly, so consume mode (remove tracks -;; after playback) is more useful here. +;; EMMS setup using an mpv subprocess player, M3U playlist helpers, fuzzy +;; file/directory adds, Dired/Dirvish integration, radio-station creation, and +;; playlist window toggling. ;; +;; The playlist keymap intentionally follows ncmpcpp where it maps cleanly, with +;; EMMS-specific additions for M3U editing and consume mode. + ;;; Code: (require 'subr-x) @@ -108,8 +36,6 @@ (defvar emms-random-playlist) (defvar emms-playlist-selected-marker) (defvar emms-source-file-default-directory) -(defvar emms-player-mpv-parameters) -(defvar emms-player-mpv-regexp) (defvar emms-player-playing-p) (defvar emms-player-paused-p) (defvar emms-playlist-mode-map) @@ -146,9 +72,98 @@ (defvar cj/music-file-extensions '("aac" "flac" "m4a" "mp3" "ogg" "opus" "wav") "List of valid music file extensions.") +(defvar cj/music-seek-seconds 5 + "Seconds to move when seeking forward or backward in the current track.") + (defvar cj/music-playlist-buffer-name "*EMMS-Playlist*" "Name of the EMMS playlist buffer used by this configuration.") +;;; Subprocess mpv player (reliable playback) + +;; The IPC player (emms-player-mpv) drives mpv over a socket -- start mpv idle, +;; connect, send loadfile. That handshake was leaving mpv loaded but never +;; streaming, so playback silently failed. Driving mpv with the track as a +;; direct argument -- the invocation that plays every time -- is the reliable +;; path. --no-config isolates this mpv from the interactive/video mpv setup so +;; the two cannot interfere. Pause is in place via process signals; in-track +;; seek is not available with a subprocess player (the trade for reliability). + +(declare-function emms-player "emms") +(declare-function emms-player-set "emms") +(declare-function emms-player-simple-start "emms-player-simple") +(declare-function emms-player-simple-stop "emms-player-simple") +(defvar emms-player-simple-process-name) +(defvar emms-player-cj/music-mpv) + +(defvar cj/music--mpv-regex + (concat "\\(?:\\." (regexp-opt cj/music-file-extensions) "\\'\\)" + "\\|\\`\\(?:https?\\|mms\\)://") + "Track names the subprocess mpv player handles: music files or stream URLs.") + +(defvar cj/music--mpv-socket + (expand-file-name "emms/mpv-control.sock" user-emacs-directory) + "IPC control socket for the subprocess mpv player. +mpv opens it per playback via --input-ipc-server. It does NOT affect startup: +mpv still plays the track passed as a direct argument, so the reliable start is +unchanged. The socket only carries control commands (seek) to the already +playing process, which is where the old idle + loadfile handshake failed.") + +(defun cj/music--mpv-start (track) + "Play TRACK by running mpv with the track name as a direct argument." + (emms-player-simple-start (emms-track-name track) + 'emms-player-cj/music-mpv + "mpv" + (list "--no-video" "--no-config" "--really-quiet" + (concat "--input-ipc-server=" cj/music--mpv-socket)))) + +(defun cj/music--mpv-stop () + "Stop the mpv subprocess." + (emms-player-simple-stop)) + +(defun cj/music--mpv-playable-p (track) + "Return non-nil if the subprocess mpv player can play TRACK." + (and (executable-find "mpv") + (memq (emms-track-type track) '(file url)) + (string-match cj/music--mpv-regex (emms-track-name track)))) + +(defun cj/music--mpv-pause () + "Pause the mpv subprocess in place by stopping it (SIGSTOP)." + (let ((proc (get-process emms-player-simple-process-name))) + (when (and proc (process-live-p proc)) + (signal-process proc 'SIGSTOP)))) + +(defun cj/music--mpv-resume () + "Resume the paused mpv subprocess (SIGCONT)." + (let ((proc (get-process emms-player-simple-process-name))) + (when (and proc (process-live-p proc)) + (signal-process proc 'SIGCONT)))) + +(defun cj/music--mpv-command (json) + "Send JSON (a one-line mpv IPC command) to the control socket. +A no-op when nothing is playing or the socket is gone, so it never errors." + (when (file-exists-p cj/music--mpv-socket) + (ignore-errors + (let ((proc (make-network-process :name "cj-music-mpv-cmd" + :family 'local + :service cj/music--mpv-socket + :noquery t))) + (unwind-protect + (progn (process-send-string proc (concat json "\n")) + (accept-process-output proc 0.1)) + (delete-process proc)))))) + +(defun cj/music-seek-forward () + "Seek `cj/music-seek-seconds' seconds forward in the current track." + (interactive) + (cj/music--mpv-command + (format "{\"command\": [\"seek\", %d, \"relative\"]}" cj/music-seek-seconds))) + +(defun cj/music-seek-backward () + "Seek `cj/music-seek-seconds' seconds backward in the current track." + (interactive) + (cj/music--mpv-command + (format "{\"command\": [\"seek\", %d, \"relative\"]}" (- cj/music-seek-seconds)))) + ;;; Buffer-local state (defvar-local cj/music-playlist-file nil @@ -843,7 +858,7 @@ For URL tracks: decoded URL." :commands (emms-mode-line-mode) :config (require 'emms-setup) - (require 'emms-player-mpv) + (require 'emms-player-simple) (require 'emms-playlist-mode) (require 'emms-source-file) (require 'emms-source-playlist) @@ -852,8 +867,13 @@ For URL tracks: decoded URL." (setq emms-source-file-default-directory cj/music-root) (setq emms-playlist-default-major-mode 'emms-playlist-mode) - ;; Use MPV as player - MUST be set before emms-all - (setq emms-player-list '(emms-player-mpv)) + ;; Use the reliable subprocess mpv player (built above) - MUST be set before emms-all + (setq emms-player-cj/music-mpv + (emms-player #'cj/music--mpv-start #'cj/music--mpv-stop + #'cj/music--mpv-playable-p)) + (emms-player-set emms-player-cj/music-mpv 'pause #'cj/music--mpv-pause) + (emms-player-set emms-player-cj/music-mpv 'resume #'cj/music--mpv-resume) + (setq emms-player-list '(emms-player-cj/music-mpv)) ;; Now initialize EMMS (emms-all) @@ -862,17 +882,6 @@ For URL tracks: decoded URL." (emms-playing-time-display-mode -1) (emms-mode-line-mode -1) - ;; MPV configuration - ;; MPV supports both local files and stream URLs - (setq emms-player-mpv-parameters - '("--quiet" "--no-video" "--audio-display=no")) - - ;; Update supported file types for mpv player - (setq emms-player-mpv-regexp - (concat "\\(?:\\`\\(?:https?\\|mms\\)://\\)\\|\\(?:\\." - (regexp-opt cj/music-file-extensions) - "\\'\\)")) - ;; Keep cj/music-playlist-file in sync if playlist is cleared. ;; Ensure we don't stack duplicate advice on reload. (advice-remove 'emms-playlist-clear #'cj/music--after-playlist-clear) @@ -908,8 +917,8 @@ For URL tracks: decoded URL." (">" . cj/music-next) ("P" . cj/music-previous) ("<" . cj/music-previous) - ("f" . emms-seek-forward) - ("b" . emms-seek-backward) + ("f" . cj/music-seek-forward) + ("b" . cj/music-seek-backward) ("q" . emms-playlist-mode-bury-buffer) ("a" . cj/music-fuzzy-select-and-add) ;; Toggles (aligned with ncmpcpp) diff --git a/modules/nerd-icons-config.el b/modules/nerd-icons-config.el index e2edb0717..e38db7d80 100644 --- a/modules/nerd-icons-config.el +++ b/modules/nerd-icons-config.el @@ -72,7 +72,20 @@ every call. The `memq' check skips when the face is already present." :after (nerd-icons marginalia) :hook (marginalia-mode . nerd-icons-completion-marginalia-setup) :config - (nerd-icons-completion-mode)) + (nerd-icons-completion-mode) + ;; The `cj/--nerd-icons-color-dir' advice forces `nerd-icons-yellow' onto every + ;; dir icon, so the package's inherit-behind `nerd-icons-completion-dir-face' + ;; can never win. Redefine the file-category icon so completing-read folders + ;; carry the dir face: copy the icon first (the memoized original stays + ;; untouched, so dired/dirvish folders are unaffected) and prepend the dir face + ;; so it takes the foreground. Files keep their own type face. + (cl-defmethod nerd-icons-completion-get-icon (cand (_cat (eql file))) + (if (string-suffix-p "/" cand) + (let ((icon (copy-sequence + (nerd-icons-icon-for-dir cand :height nerd-icons-completion-icon-size)))) + (add-face-text-property 0 (length icon) 'nerd-icons-completion-dir-face nil icon) + (concat icon " ")) + (concat (nerd-icons-icon-for-file cand :height nerd-icons-completion-icon-size) " ")))) (use-package nerd-icons-ibuffer :after nerd-icons diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el index 3234cc929..9ccd21d7b 100644 --- a/modules/org-agenda-config.el +++ b/modules/org-agenda-config.el @@ -6,51 +6,18 @@ ;; Layer: 3 (Domain Workflow). ;; Category: D/S. ;; Load shape: eager. -;; Eager reason: daily agenda workflow; the user expects agenda available at the -;; first session. -;; Top-level side effects: one add-hook and an idle timer that builds the agenda -;; file cache 10s after startup (guarded; spec tracks the cache lifecycle). +;; Eager reason: agenda should be available in the first session. +;; Top-level side effects: agenda hooks plus guarded idle cache build. ;; Runtime requires: user-constants, system-lib, cj-cache-lib. ;; Direct test load: yes. ;; -;; Performance: -;; - Caches agenda file list to avoid scanning projects directory on every view -;; - Cache builds asynchronously 10 seconds after Emacs startup (non-blocking) -;; - First agenda view uses cache if ready, otherwise builds synchronously -;; - Subsequent views are instant (cached) -;; - Cache auto-refreshes after 1 hour -;; - Manual refresh: M-x cj/org-agenda-refresh-files (e.g., after adding projects) +;; Org agenda configuration for global, project-scoped, and buffer-scoped task +;; views. F8 opens the main agenda; modified F8 bindings narrow by project, +;; current buffer, or task list. ;; -;; Agenda views are tied to the F8 (fate) key. -;; -;; "We are what we repeatedly do. -;; Excellence, then, is not an act, but a habit" -;; -- Aristotle -;; -;; "...watch your actions, they become habits; -;; watch your habits, they become character; -;; watch your character, for it becomes your destiny." -;; -- Lao Tzu -;; -;; -;; f8 - MAIN AGENDA which organizes all tasks and events into: -;; - all unfinished priority A tasks -;; - the weekly schedule, including the habit consistency graph -;; - all priority B tasks -;; -;; C-f8 - PROJECT AGENDA showing the main agenda filtered to a single project. -;; Prompts for project selection, then shows overdue/hi-pri/schedule/B tasks -;; scoped to that project's todo.org plus all calendars and inbox. -;; -;; s-f8 - TASK LIST containing all tasks from all agenda targets. -;; -;; M-f8 - TASK LIST containing all tasks from just the current org-mode buffer. -;; -;; NOTE: -;; Files that contain information relevant to the agenda are: the inbox, the -;; schedule-file, the synced calendars, and the per-project todo.org files found -;; in immediate subdirectories of projects-dir. (org-roam notes are refile -;; targets, not agenda sources -- see org-refile-config.el.) +;; Agenda files come from inbox, schedule files, synced calendars, and immediate +;; project todo.org files. The file list is cached and rebuilt asynchronously to +;; keep normal agenda opens fast. ;;; Code: (require 'user-constants) diff --git a/modules/org-contacts-config.el b/modules/org-contacts-config.el index 64abb9fb5..944d75c10 100644 --- a/modules/org-contacts-config.el +++ b/modules/org-contacts-config.el @@ -168,15 +168,29 @@ Added: %U" ;;; ------------------------- Quick Contact Functions --------------------------- +(require 'system-lib) + (defun cj/org-contacts-find () "Find and open a contact." (interactive) (find-file contacts-file) (goto-char (point-min)) - (let ((contact (completing-read "Find contact: " - (org-map-entries - (lambda () (nth 4 (org-heading-components))) - nil (list contacts-file))))) + (let* ((alist (org-map-entries + (lambda () + (cons (nth 4 (org-heading-components)) + (or (org-entry-get nil "EMAIL") + (org-entry-get nil "PHONE")))) + nil (list contacts-file))) + (contact (completing-read + "Find contact: " + (cj/completion-table-annotated + 'contact + (lambda (cand) + (let ((info (cdr (assoc cand alist)))) + (when (and info (> (length info) 0)) + (concat " " (propertize info 'face + 'completions-annotations))))) + alist)))) (goto-char (point-min)) (search-forward contact) (org-fold-show-entry) diff --git a/modules/org-webclipper.el b/modules/org-webclipper.el index f32cad3fd..40ceada76 100644 --- a/modules/org-webclipper.el +++ b/modules/org-webclipper.el @@ -5,50 +5,17 @@ ;; Layer: 4 (Optional). ;; Category: O/D/P. ;; Load shape: eager. -;; Eager reason: none; web clipping runs via org-protocol/command, a Phase 4 -;; protocol/command-loaded deferral candidate. +;; Eager reason: none; protocol and direct clipping can load on command. ;; Top-level side effects: org-protocol handler registration via use-package. -;; Runtime requires: none (configures packages via use-package). +;; Runtime requires: none. ;; Direct test load: yes. ;; -;; This package provides a seamless "fire-and-forget" workflow for clipping -;; web pages from the browser directly into an Org file using org-protocol -;; and org-web-tools. +;; Captures web pages into Org from org-protocol, EWW, or W3M. The protocol path +;; records URL/title dynamically around org-capture; the direct path clips the +;; current browser buffer. ;; -;; Features: -;; - Browser bookmarklet integration via org-protocol -;; - Automatic conversion to Org format using eww-readable and Pandoc -;; - One-click capture from any web page -;; - Preserves page structure and formatting -;; - Smart heading adjustment (removes page title, demotes remaining headings) -;; -;; Setup: -;; 1. Ensure this file is loaded in your Emacs configuration -;; 2. Make sure emacsclient is configured for org-protocol -;; 3. Add the following bookmarklet to your browser's bookmarks bar: -;; -;; javascript:location.href='org-protocol://webclip?url='+encodeURIComponent(location.href)+'&title='+encodeURIComponent(document.title);void(0); -;; -;; To add the bookmarklet: -;; a. Create a new bookmark in your browser -;; b. Set the name to: Clip to Org (or your preference) -;; c. Set the URL to the JavaScript code above -;; d. Save it to your bookmarks bar for easy access -;; -;; 4. Click the bookmarklet on any web page to clip its content -;; -;; The clipped content will be added to the file specified by `webclipped-file` -;; under the "Webclipped Inbox" heading with proper formatting and metadata. -;; -;; Architecture: -;; - cj/--process-webclip-content: Pure function for content processing -;; - cj/org-protocol-webclip-handler: Handles URL fetching and capture -;; - cj/org-webclipper-EWW: Direct capture from EWW/W3M buffers -;; -;; Requirements: -;; - org-web-tools package -;; - Pandoc installed on your system -;; - Emacs server running (M-x server-start) +;; Content is converted to readable Org, normalized, and filed under the +;; configured webclip inbox heading. ;;; Code: @@ -85,7 +52,6 @@ See `cj/--webclip-url' for the binding contract.") (defun cj/webclipper-ensure-initialized () "Ensure webclipper is initialized when first used." (unless cj/webclipper-initialized - ;; Load required packages now (require 'org-protocol) (require 'org-capture) (require 'org-web-tools) diff --git a/modules/selection-framework.el b/modules/selection-framework.el index 464654a20..7f7f9a475 100644 --- a/modules/selection-framework.el +++ b/modules/selection-framework.el @@ -128,7 +128,6 @@ ;; Optionally tweak the register preview window. (advice-add #'register-preview :override #'consult-register-window) - ;; Configure other variables and modes (setq xref-show-xrefs-function #'consult-xref xref-show-definitions-function #'consult-xref) diff --git a/modules/signal-config.el b/modules/signal-config.el index 86cb523ce..edb7d0dc3 100644 --- a/modules/signal-config.el +++ b/modules/signal-config.el @@ -309,7 +309,13 @@ opens the chosen recipient in `signel-chat'." (candidates (cons note-self cj/signel--contact-cache)) (table (lambda (string pred action) (if (eq action 'metadata) - '(metadata + `(metadata + (category . signal-contact) + (annotation-function + . ,(lambda (cand) + (let ((r (cdr (assoc cand candidates)))) + (when r + (concat " " (propertize r 'face 'completions-annotations)))))) (display-sort-function . identity) (cycle-sort-function . identity)) (complete-with-action action candidates string pred)))) diff --git a/modules/system-commands.el b/modules/system-commands.el index 44ac3ae89..de5e88535 100644 --- a/modules/system-commands.el +++ b/modules/system-commands.el @@ -6,14 +6,14 @@ ;; Layer: 3 (Domain Workflow). ;; Category: D/S. ;; Load shape: eager. -;; Eager reason: registers the C-; ! system-command keymap; high-impact commands +;; Eager reason: binds C-; ! to the system-command menu; high-impact commands ;; that should run only by command (command-loaded target). -;; Top-level side effects: defines a system-command keymap under cj/custom-keymap. +;; Top-level side effects: binds C-; ! to the system-command menu in cj/custom-keymap. ;; Runtime requires: keybindings, host-environment, rx. ;; Direct test load: yes (requires keybindings explicitly). ;; ;; System commands for logout, lock, suspend, shutdown, reboot, and Emacs -;; exit/restart. Provides both a keymap (C-; !) and a completing-read menu. +;; exit/restart. C-; ! opens a completing-read menu of all commands. ;; ;; Commands include: ;; - Logout (terminate user session) @@ -28,8 +28,8 @@ ;; ;;; Code: -;; `keybindings' provides `cj/custom-keymap', which is referenced at load -;; time by the `keymap-set' call at the tail of this file. An +;; `keybindings' provides `cj/custom-keymap' and `cj/register-command', +;; referenced at load time by the binding call at the tail of this file. An ;; `eval-when-compile' require would silence the byte-compiler but leave ;; the load-time reference void if anything required `system-commands' ;; before `keybindings'. Make the dependency explicit. @@ -181,29 +181,10 @@ daemon alive rather than killing the session blindly." (when-let ((cmd (alist-get choice commands nil nil #'equal))) (call-interactively cmd)))) -(defvar-keymap cj/system-command-map - :doc "Keymap for system commands." - "!" #'cj/system-command-menu - "L" #'cj/system-cmd-logout - "r" #'cj/system-cmd-reboot - "s" #'cj/system-cmd-shutdown - "S" #'cj/system-cmd-suspend - "l" #'cj/system-cmd-lock - "E" #'cj/system-cmd-exit-emacs - "e" #'cj/system-cmd-restart-emacs) -(cj/register-prefix-map "!" cj/system-command-map) - -(with-eval-after-load 'which-key - (which-key-add-key-based-replacements - "C-; !" "system commands" - "C-; ! !" "system command menu" - "C-; ! L" "logout" - "C-; ! E" "exit Emacs" - "C-; ! S" "suspend" - "C-; ! e" "restart Emacs" - "C-; ! l" "lock screen" - "C-; ! r" "reboot" - "C-; ! s" "shutdown")) +;; C-; ! opens the completing-read menu directly. The per-command leaf +;; keys (s/r/e/l/L/E/S) were removed 2026-06-28 to reclaim the key +;; real-estate; every command stays reachable through the menu. +(cj/register-command "!" #'cj/system-command-menu "system commands") (provide 'system-commands) ;;; system-commands.el ends here diff --git a/modules/system-lib.el b/modules/system-lib.el index 49bb6cd1a..8b954c6a9 100644 --- a/modules/system-lib.el +++ b/modules/system-lib.el @@ -164,6 +164,29 @@ contributes its own modes regardless of load order." (setq font-lock-global-modes (cj/--font-lock-global-modes-excluding font-lock-global-modes mode)))) +(defun cj/completion-table (category collection) + "Return a completion table over COLLECTION tagged with completion CATEGORY. +COLLECTION is anything `completing-read' accepts (list, alist, obarray, hash +table, or another table). The table reports CATEGORY in its metadata so +marginalia (and embark, consult, sorting) can recognize and annotate the +candidates. Use a standard category (file, buffer, function, theme, ...) when +the candidates match one; marginalia then annotates them with no further work." + (lambda (string predicate action) + (if (eq action 'metadata) + `(metadata (category . ,category)) + (complete-with-action action collection string predicate)))) + +(defun cj/completion-table-annotated (category annotate collection) + "Like `cj/completion-table' but also attach ANNOTATE as the annotation function. +ANNOTATE is called with a candidate string and returns its annotation suffix, or +nil. Use this for a custom CATEGORY that marginalia has no built-in annotator +for: marginalia falls back to the table's own annotation function." + (lambda (string predicate action) + (if (eq action 'metadata) + `(metadata (category . ,category) + (annotation-function . ,annotate)) + (complete-with-action action collection string predicate)))) + (defun cj/format-region-with-program (program &rest args) "Replace the current buffer with PROGRAM ARGS run over its contents, via argv. Runs PROGRAM (with ARGS) on the whole buffer through `call-process-region' diff --git a/modules/system-utils.el b/modules/system-utils.el index 00be88906..b393aa33f 100644 --- a/modules/system-utils.el +++ b/modules/system-utils.el @@ -157,6 +157,12 @@ detached from Emacs." (keymap-set ibuffer-mode-map "d" #'ibuffer-diff-with-file) (keymap-set ibuffer-mode-map "D" #'ibuffer-mark-for-delete)) +;; ibuffer paints its rows with manual `face' properties (nerd-icons + ibuffer +;; faces). Left in `global-font-lock-mode', font-lock leaks keyword fontification +;; onto buffer and mode names, mixing wrong colors in. Exclude it, the same fix +;; as the shr-rendered reader modes. +(cj/exclude-from-global-font-lock 'ibuffer-mode) + ;;; -------------------------- Scratch Buffer Happiness ------------------------- (defvar scratch-emacs-version-and-system diff --git a/modules/test-runner.el b/modules/test-runner.el index 50d4f7e40..48a2b09fe 100644 --- a/modules/test-runner.el +++ b/modules/test-runner.el @@ -6,69 +6,18 @@ ;; Layer: 2 (Core UX). ;; Category: C/L. ;; Load shape: eager. -;; Eager reason: the test keymap entry point and project-scoped runner state. -;; Top-level side effects: defines a test keymap, registers it under cj/custom-keymap. +;; Eager reason: registers the C-; t test runner entry point and state. +;; Top-level side effects: defines and registers cj/test-map. ;; Runtime requires: ert, cl-lib, keybindings. -;; Direct test load: yes (requires keybindings explicitly). +;; Direct test load: yes. ;; -;; This module provides a powerful ERT test runner with focus/unfocus workflow -;; for efficient test-driven development in Emacs Lisp projects. -;; -;; PURPOSE: -;; -;; When working on large Emacs Lisp projects with many test files, you often -;; want to focus on running just the tests relevant to your current work without -;; waiting for the entire suite to run. This module provides a smart test runner -;; that supports both running all tests and focusing on specific test files. -;; -;; WORKFLOW: -;; -;; 1. Run all tests initially to establish baseline (C-; t R) -;; 2. Add test files to focus while working on a feature (C-; t a) -;; 3. Run focused tests repeatedly as you develop (C-; t r) -;; 4. Add more test files as needed (C-; t b from within test buffer) -;; 5. View your focused test list at any time (C-; t v) -;; 6. Clear focus and run all tests before finishing (C-; t c, then C-; t R) -;; -;; PROJECT INTEGRATION: -;; -;; - Automatically discovers test directories in Projectile projects -;; (looks for "test" or "tests" under project root) -;; - Falls back to ~/.emacs.d/tests if not in a Projectile project -;; - Test files must match pattern: test-*.el -;; -;; SPECIAL BEHAVIORS: -;; -;; - Smart test running: Automatically runs all or focused tests based on mode -;; - Test extraction: Discovers test names via regex to run specific tests -;; - At-point execution: Run individual test at cursor position (C-; t .) -;; - Error handling: Continues loading tests even if individual files fail -;; -;; KEYBINDINGS: -;; -;; C-; t L Load all test files -;; C-; t R Run all tests (full suite) -;; C-; t r Run tests smartly (all or focused based on mode) -;; C-; t . Run test at point -;; C-; t a Add test file to focus (with completion) -;; C-; t b Add current buffer's test file to focus -;; C-; t c Clear all focused test files -;; C-; t v View list of focused test files -;; C-; t t Toggle mode between 'all and 'focused -;; -;; RECOMMENDED USAGE: -;; -;; While implementing a feature: -;; - Add the main test file for the feature you're working on -;; - Add any related test files that might be affected -;; - Use C-; t r to repeatedly run just those focused tests -;; - This provides fast feedback during development -;; -;; Before committing: -;; - Clear the focus with C-; t c -;; - Run the full suite with C-; t R to ensure nothing broke -;; - Verify all tests pass before pushing changes +;; Project-aware ERT runner with two modes: all tests or a focused file set. +;; Test roots come from Projectile projects, falling back to the config's tests +;; directory, and test files are discovered by the test-*.el convention. ;; +;; Commands under C-; t load tests, run all/focused tests, run the test at point, +;; and manage the per-project focus list. + ;;; Code: (require 'ert) diff --git a/modules/tramp-config.el b/modules/tramp-config.el index e3b835f1f..f2bc8457c 100644 --- a/modules/tramp-config.el +++ b/modules/tramp-config.el @@ -57,7 +57,6 @@ (setq tramp-auto-save-directory (expand-file-name "tramp-auto-save" user-emacs-directory)) - ;; Create directory if it doesn't exist (unless (file-exists-p tramp-auto-save-directory) (make-directory tramp-auto-save-directory t)) diff --git a/modules/transcription-config.el b/modules/transcription-config.el index e00306d1e..944063b88 100644 --- a/modules/transcription-config.el +++ b/modules/transcription-config.el @@ -195,6 +195,8 @@ transcript lands alongside the source, not next to the temp /tmp audio." (txt-file (car outputs)) (log-file (cdr outputs)) (buffer-name (format " *transcribe-%s*" (file-name-nondirectory audio-file))) + (stderr-buffer-name (format " *transcribe-stderr-%s*" + (file-name-nondirectory audio-file))) (process-name (format "transcribe-%s" (file-name-nondirectory audio-file)))) (unless (file-executable-p script) @@ -203,15 +205,25 @@ transcript lands alongside the source, not next to the temp /tmp audio." (cj/--init-log-file log-file audio-file script) (let* ((process-environment (cj/--build-process-environment cj/transcribe-backend)) + ;; A live, explicitly-managed buffer for stderr. Passing a file PATH + ;; to :stderr makes Emacs create a phantom buffer named after the + ;; path, so the error text never reaches the log file and that buffer + ;; leaks per run; the sentinel drains this buffer into the log and + ;; kills it. Keeping stderr off the stdout :buffer leaves the + ;; transcript (stdout) clean. + (stderr-buffer (with-current-buffer (get-buffer-create stderr-buffer-name) + (erase-buffer) + (current-buffer))) (process (make-process :name process-name :buffer (get-buffer-create buffer-name) :command (list script audio-file) :sentinel (lambda (proc event) - (cj/--transcription-sentinel proc event audio-file txt-file log-file) + (cj/--transcription-sentinel proc event audio-file + txt-file log-file stderr-buffer) (when cleanup-file (ignore-errors (delete-file cleanup-file)))) - :stderr log-file))) + :stderr stderr-buffer))) (cj/--track-transcription process audio-file) (cj/--notify "Transcription" (format "Started on %s" (file-name-nondirectory audio-file))) @@ -294,20 +306,25 @@ References TXT-FILE on success (normal urgency), LOG-FILE on failure (format "Errored. Logs in %s" (file-name-nondirectory log-file)) 'critical))) -(defun cj/--transcription-sentinel (process event _audio-file txt-file log-file) +(defun cj/--transcription-sentinel (process event _audio-file txt-file log-file stderr-buffer) "Sentinel for transcription PROCESS. EVENT is the process event string. TXT-FILE and LOG-FILE are the -associated output files." +associated output files. STDERR-BUFFER holds the process's stderr; its +contents are appended to LOG-FILE so the \"Logs in <file>\" notification +points at real error text, and the buffer is then killed so it does not +leak per run." (let* ((success-p (and (string-match-p "finished" event) (= 0 (process-exit-status process)))) (process-buffer (process-buffer process))) (cj/--write-transcript-on-success process-buffer success-p txt-file) - (cj/--append-to-log process-buffer log-file event) + (cj/--append-to-log stderr-buffer log-file event) (cj/--update-transcription-status process success-p) (when (and success-p (not (cj/--should-keep-log success-p))) (delete-file log-file)) (when (buffer-live-p process-buffer) (kill-buffer process-buffer)) + (when (buffer-live-p stderr-buffer) + (kill-buffer stderr-buffer)) (cj/--notify-completion success-p txt-file log-file) (run-at-time 600 nil #'cj/--cleanup-completed-transcriptions) (force-mode-line-update t))) diff --git a/modules/ui-navigation.el b/modules/ui-navigation.el index cb0fc5697..76dd686a6 100644 --- a/modules/ui-navigation.el +++ b/modules/ui-navigation.el @@ -110,7 +110,9 @@ existing split does. No-op when SIDE is nil." (defun cj/window-resize-sticky () "Resize the active window's divider in the just-pressed arrow's direction \(via `windsize'), then keep `cj/window-resize-map' active so bare arrows keep -nudging until any other key. Bound to `C-; b <left>/<right>/<up>/<down>'. +nudging until any other key. Bound to `C-; b <arrow>' and to the global +`M-<arrow>' keys (each direction); the arrow is read with `event-basic-type', +so the Meta modifier on the M-<arrow> path is stripped and both behave alike. When the selected window is the sole window in the frame there is no divider to move, so the first arrow instead splits a sliver away on the @@ -119,13 +121,21 @@ buffer; the current window keeps almost the whole frame and the following arrows shrink it via `windsize', so it reads the same as resizing an existing split." (interactive) - (let ((key (key-description (vector last-command-event)))) + (let ((key (format "<%s>" (event-basic-type last-command-event)))) (if (one-window-p) (cj/window--pull-away (cj/window-pull-side key)) (let ((cmd (keymap-lookup cj/window-resize-map key))) (when cmd (call-interactively cmd))))) (set-transient-map cj/window-resize-map t)) +;; M-<arrow> mirrors `C-; b <arrow>': one chord to pull a split from a sole +;; window or nudge a divider. M-<up>/<down> are otherwise unbound; M-<left>/ +;; <right> shed their word-motion, which stays on `C-<left>'/`C-<right>'. +(keymap-global-set "M-<left>" #'cj/window-resize-sticky) +(keymap-global-set "M-<right>" #'cj/window-resize-sticky) +(keymap-global-set "M-<up>" #'cj/window-resize-sticky) +(keymap-global-set "M-<down>" #'cj/window-resize-sticky) + ;; ------------------------------ Window Splitting ----------------------------- (defun cj/split-and-follow-right () diff --git a/modules/ui-theme.el b/modules/ui-theme.el index eb4efd9b5..499e71a49 100644 --- a/modules/ui-theme.el +++ b/modules/ui-theme.el @@ -37,13 +37,17 @@ ;; ------------------------------- Switch Themes ------------------------------- ;; loads themes in completing read, then persists via the functions below +(require 'system-lib) + (defun cj/switch-themes () "Function to switch themes and save chosen theme name for persistence. Unloads any other applied themes before applying the chosen theme." (interactive) (let ((chosentheme (completing-read "Load custom theme: " - (mapcar #'symbol-name - (custom-available-themes))))) + (cj/completion-table + 'theme + (mapcar #'symbol-name + (custom-available-themes)))))) (cj/theme-disable-all) (cj/theme-load-name chosentheme)) (cj/save-theme-to-file)) diff --git a/modules/undead-buffers.el b/modules/undead-buffers.el index fe43575e9..4780ef227 100644 --- a/modules/undead-buffers.el +++ b/modules/undead-buffers.el @@ -32,7 +32,13 @@ (defvar cj/undead-buffer-list '("*scratch*" "*EMMS-Playlist*" "*Messages*" "*ert*" "*AI-Assistant*") - "Buffers to bury instead of killing.") + "Buffer names to bury instead of killing (exact match).") + +(defvar cj/undead-buffer-regexps nil + "Regexps for buffer names to bury instead of killing, alongside +`cj/undead-buffer-list'. Use for dynamically-named buffer families where an +exact name can't be pre-listed -- e.g. ai-term agents, named \"agent [<project>]\". +Register one with `cj/make-buffer-pattern-undead'.") (defun cj/make-buffer-undead (name) "Append NAME to `cj/undead-buffer-list' if not present. @@ -41,6 +47,23 @@ Signal an error if NAME is not a non-empty string. Return the updated list." (error "cj/bury-alive-add: NAME must be a non-empty string")) (add-to-list 'cj/undead-buffer-list name t)) +(defun cj/make-buffer-pattern-undead (regexp) + "Append REGEXP to `cj/undead-buffer-regexps' if not present. +A buffer whose name matches REGEXP is buried instead of killed. Signal an +error if REGEXP is not a non-empty string. Return the updated list." + (unless (and (stringp regexp) (> (length regexp) 0)) + (error "cj/make-buffer-pattern-undead: REGEXP must be a non-empty string")) + (add-to-list 'cj/undead-buffer-regexps regexp t)) + +(defun cj/--buffer-undead-p (name) + "Return non-nil when buffer NAME should be buried instead of killed. +NAME is undead when it is in `cj/undead-buffer-list' (exact) or matches any +regexp in `cj/undead-buffer-regexps'." + (and (stringp name) + (or (member name cj/undead-buffer-list) + (seq-some (lambda (re) (string-match-p re name)) + cj/undead-buffer-regexps)))) + (defun cj/kill-buffer-or-bury-alive (buffer) "Kill BUFFER or bury it if it's in `cj/undead-buffer-list'." (interactive "bBuffer to kill or bury: ") @@ -49,7 +72,7 @@ Signal an error if NAME is not a non-empty string. Return the updated list." (progn (add-to-list 'cj/undead-buffer-list (buffer-name)) (message "Added %s to bury-alive-list" (buffer-name))) - (if (member (buffer-name) cj/undead-buffer-list) + (if (cj/--buffer-undead-p (buffer-name)) (bury-buffer) (kill-buffer))))) (keymap-global-set "<remap> <kill-buffer>" #'cj/kill-buffer-or-bury-alive) @@ -60,7 +83,7 @@ Undead-buffers are buffers in `cj/undead-buffer-list'." (let* ((buf (current-buffer)) (name (buffer-name buf))) (and - (not (member name cj/undead-buffer-list)) + (not (cj/--buffer-undead-p name)) (buffer-file-name buf) (buffer-modified-p buf)))) diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el index 1672529f7..fce3d9033 100644 --- a/modules/video-audio-recording.el +++ b/modules/video-audio-recording.el @@ -6,104 +6,19 @@ ;; Layer: 4 (Optional). ;; Category: O/D/S. ;; Load shape: eager. -;; Eager reason: none; registers a recording keymap, but device probing should -;; run only on command (command-loaded target). -;; Top-level side effects: defines cj/record-map and conditionally registers it -;; under C-; r. +;; Eager reason: none; records only on command, but registers C-; r at load. +;; Top-level side effects: defines cj/record-map and registers it when possible. ;; Runtime requires: system-lib, keybindings. -;; Direct test load: yes (requires keybindings explicitly). +;; Direct test load: yes. ;; -;; Desktop video and audio recording from within Emacs using ffmpeg. -;; Records from both microphone and system audio simultaneously, which -;; makes it suitable for capturing meetings, presentations, and desktop activity. -;; -;; Architecture: -;; - Audio recordings use ffmpeg directly with PulseAudio inputs → M4A/AAC -;; - Video recordings differ by display server: -;; - X11: ffmpeg with x11grab + PulseAudio → MKV -;; - Wayland: wf-recorder piped to ffmpeg for audio mixing → MKV -;; (wf-recorder captures the compositor, ffmpeg mixes in audio) -;; -;; Process lifecycle: -;; - Start: `start-process-shell-command` creates a shell running the -;; ffmpeg (or wf-recorder|ffmpeg) pipeline. Process ref is stored in -;; `cj/video-recording-ffmpeg-process' or `cj/audio-recording-ffmpeg-process'. -;; - Stop: SIGINT is sent to the shell's process group so all pipeline -;; children (wf-recorder, ffmpeg) receive it. We then poll until the -;; process actually exits, giving ffmpeg time to finalize the container. -;; - Cleanup: A process sentinel auto-clears the process variable and -;; updates the modeline if the process dies unexpectedly. -;; -;; Note: video-recordings-dir and audio-recordings-dir are defined -;; (and directory created) in user-constants.el -;; -;; Quick Start -;; =========== -;; 1. Press C-; r s to run quick setup -;; 2. Pick a microphone from the list -;; 3. Pick an audio output — [in use] shows which apps are playing -;; 4. Press C-; r a to start/stop audio recording -;; 5. Recording starts - you'll see in your modeline -;; 6. Press C-; r a again to stop (🔴 disappears) -;; -;; Device Setup -;; ============ -;; C-; r a automatically prompts for device selection on first use. -;; Device selection lasts for the current Emacs session only. -;; -;; Manual device selection: -;; -;; C-; r s (cj/recording-quick-setup) - RECOMMENDED -;; Two-step setup: pick a mic, then pick an audio output to capture. -;; Both steps show status: [in use], [ready], [available], [muted]. -;; Audio outputs also show which apps are playing through them. -;; Sorted: in use → ready → available → muted. -;; -;; C-; r S (cj/recording-select-devices) - ADVANCED -;; Manual selection: choose mic and monitor separately. -;; Use when you need different devices for input/output. -;; -;; C-; r d (cj/recording-list-devices) -;; List all available audio devices and current configuration. -;; -;; C-; r w (cj/recording-show-active-audio) - DIAGNOSTIC TOOL -;; Show which apps are currently playing audio and through which device. -;; Use this DURING a phone call to see if the call audio is going through -;; the device you think it is. Helps diagnose "missing one side" issues. -;; -;; Pre-Recording Validation -;; ======================== -;; Every time you start a recording, the system audio device is -;; validated automatically: -;; 1. If the configured monitor device no longer exists (e.g. -;; USB DAC unplugged), it's auto-updated to the current -;; default sink's monitor. -;; 2. If no audio is currently playing through the monitored sink, -;; a warning is shown in the echo area. Recording proceeds -;; without interruption — run C-; r s to see active streams. -;; -;; Testing Devices Before Important Recordings -;; ============================================ -;; Always test devices before important recordings: -;; -;; C-; r t b (cj/recording-test-both) - RECOMMENDED -;; Guided test: mic only, monitor only, then both together. -;; Catches hardware issues before they ruin recordings! -;; -;; C-; r t m (cj/recording-test-mic) -;; Quick 5-second mic test with playback. -;; -;; C-; r t s (cj/recording-test-monitor) -;; Quick 5-second system audio test with playback. -;; -;; To adjust volumes: -;; - Use =M-x cj/recording-adjust-volumes= (or your keybinding =r l=) -;; - Or customize permanently: =M-x customize-group RET cj-recording RET= -;; - Or in your config: -;; #+begin_src emacs-lisp -;; (setq cj/recording-mic-boost 1.5) ; 50% louder -;; (setq cj/recording-system-volume 0.7) ; 30% quieter +;; Starts and stops ffmpeg-backed audio/video recordings from Emacs. Audio +;; captures microphone plus system monitor; video uses x11grab on X11 and +;; wf-recorder piped into ffmpeg on Wayland. ;; +;; Recording processes are tracked in module variables, stopped with SIGINT so +;; containers finalize cleanly, and reflected in the modeline. Device selection +;; is session-local; quick setup and device tests live under C-; r. + ;;; Code: (require 'system-lib) diff --git a/modules/weather-config.el b/modules/weather-config.el index 416db0323..017d9e31b 100644 --- a/modules/weather-config.el +++ b/modules/weather-config.el @@ -1,4 +1,4 @@ -;;; weather-config.el --- -*- lexical-binding: t; coding: utf-8; -*- +;;; weather-config.el --- wttrin weather display and modeline setup -*- lexical-binding: t; coding: utf-8; -*- ;; author: Craig Jennings <c@cjennings.net> ;;; Commentary: ;; @@ -11,9 +11,8 @@ ;; Runtime requires: none (configures packages via use-package). ;; Direct test load: yes. ;; -;; Call M-W to open wttrin with your preferred location list immediately. -;; Adjust the city list by editing `wttrin-default-locations` or answering wttrin prompts when asked. -;; Forecasts arrive in an Emacs buffer, so you can stay keyboard-only while checking weather. +;; Configures wttrin for favorite-location forecasts, mode-line weather, and +;; whereami-backed geolocation. M-S-w opens the weather buffer. ;; ;;; Code: @@ -22,10 +21,13 @@ ;; ----------------------------------- Wttrin ---------------------------------- (use-package wttrin - :vc (:url "git@cjennings.net:emacs-wttrin.git" - :branch "main" - :rev :newest) - ;; :load-path "~/code/emacs-wttrin" ;; uncomment + comment :vc above for local dev + ;; Load from the local checkout (currently release/0.4.0) so recent wttrin + ;; changes are testable without a package pull. Swap back to :vc below for + ;; production tracking. + :load-path "~/code/emacs-wttrin" + ;; :vc (:url "git@cjennings.net:emacs-wttrin.git" + ;; :branch "release/0.4.0" + ;; :rev :newest) :demand t ;; REQUIRED: mode-line must start at Emacs startup :preface ;; Change this to t to enable debug logging @@ -39,6 +41,9 @@ ;; colors) is unchanged. (setopt wttrin-display-options "F") (setopt wttrin-favorite-location "New Orleans, LA") + ;; Scale the weather font to fit the window width, clamped to a floor/cap + ;; (wttrin-font-height-min/-max, default 100/200). + (setopt wttrin-auto-fit-font t) ;; Higher-accuracy geolocation via the whereami WiFi-scan script (Google-backed), ;; far better than IP behind a VPN or cellular hotspot. Used by the picker's ;; "Current location (detect)" entry; wttrin falls back to its IP provider if the |
