aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/calendar-sync-source.el69
-rw-r--r--modules/custom-buffer-file.el53
-rw-r--r--modules/custom-case.el43
-rw-r--r--modules/custom-comments.el38
-rw-r--r--modules/custom-datetime.el2
-rw-r--r--modules/custom-line-paragraph.el61
-rw-r--r--modules/custom-ordering.el41
-rw-r--r--modules/custom-text-enclose.el17
-rw-r--r--modules/duet-config.el19
-rw-r--r--modules/flyspell-and-abbrev.el7
-rw-r--r--modules/font-config.el48
-rw-r--r--modules/keyboard-compat.el3
-rw-r--r--modules/keyboard-macros.el12
-rw-r--r--modules/local-repository.el62
-rw-r--r--modules/lorem-optimum.el18
-rw-r--r--modules/mu4e-org-contacts-setup.el31
-rw-r--r--modules/org-agenda-config.el2
-rw-r--r--modules/org-babel-config.el3
-rw-r--r--modules/org-contacts-config.el37
-rw-r--r--modules/org-drill-config.el55
-rw-r--r--modules/org-export-config.el16
-rw-r--r--modules/org-reveal-config.el33
-rw-r--r--modules/org-roam-config.el48
-rw-r--r--modules/org-webclipper.el20
-rw-r--r--modules/selection-framework.el4
-rw-r--r--modules/show-kill-ring.el122
-rw-r--r--modules/takuzu-config.el17
-rw-r--r--modules/text-config.el6
-rw-r--r--modules/ui-config.el9
-rw-r--r--modules/ui-theme.el10
-rw-r--r--modules/undead-buffers.el14
31 files changed, 391 insertions, 529 deletions
diff --git a/modules/calendar-sync-source.el b/modules/calendar-sync-source.el
index 15c91c59..10dea66e 100644
--- a/modules/calendar-sync-source.el
+++ b/modules/calendar-sync-source.el
@@ -138,39 +138,25 @@ Checks `cj/debug-modules' for symbol `calendar-sync' or t (all)."
;;; .ics Fetch
-(defun calendar-sync--fetch-ics (url callback)
- "Fetch .ics file from URL asynchronously using curl.
-Calls CALLBACK with the .ics content as string (normalized to Unix line endings)
-or nil on error. CALLBACK signature: (lambda (content) ...).
-
-The fetch happens asynchronously and doesn't block Emacs. The callback is
-invoked when the fetch completes, either successfully or with an error."
- (condition-case err
- (let ((buffer (generate-new-buffer " *calendar-sync-curl*")))
- (make-process
- :name "calendar-sync-curl"
- :buffer buffer
- :command (list "curl" "-s" "-L" "--fail"
- "--connect-timeout" "10"
- "--max-time" (number-to-string calendar-sync-fetch-timeout)
- url)
- :sentinel
- (lambda (process event)
- (when (memq (process-status process) '(exit signal))
- (let ((buf (process-buffer process)))
- (when (buffer-live-p buf)
- (let ((content
- (with-current-buffer buf
- (if (and (eq (process-status process) 'exit)
- (= (process-exit-status process) 0))
- (calendar-sync--normalize-line-endings (buffer-string))
- (calendar-sync--log-silently "calendar-sync: Fetch error: curl failed: %s" (string-trim event))
- nil))))
- (kill-buffer buf)
- (funcall callback content))))))))
- (error
- (calendar-sync--log-silently "calendar-sync: Fetch error: %s" (error-message-string err))
- (funcall callback nil))))
+(defun calendar-sync--fetch-sentinel-finish (success event temp-file buffer callback)
+ "Finish an async .ics fetch.
+SUCCESS is non-nil when curl exited cleanly, EVENT the process event
+string, TEMP-FILE the curl output path, BUFFER the process buffer, and
+CALLBACK the continuation. On success CALLBACK receives TEMP-FILE (the
+caller owns deleting it); on failure the error is logged, TEMP-FILE is
+removed, and CALLBACK receives nil. Extracted from the sentinel so the
+success, failure, and cleanup branches are unit-testable without a live
+curl process."
+ (when (buffer-live-p buffer)
+ (unless success
+ (calendar-sync--log-silently "calendar-sync: Fetch error: curl failed: %s"
+ (string-trim event)))
+ (kill-buffer buffer))
+ (if success
+ (funcall callback temp-file)
+ (when (file-exists-p temp-file)
+ (delete-file temp-file))
+ (funcall callback nil)))
(defun calendar-sync--fetch-ics-file (url callback)
"Fetch .ics from URL to a temp file asynchronously.
@@ -190,19 +176,10 @@ owns deleting the temp file after a successful callback."
:sentinel
(lambda (process event)
(when (memq (process-status process) '(exit signal))
- (let ((buf (process-buffer process))
- (success (and (eq (process-status process) 'exit)
- (= (process-exit-status process) 0))))
- (when (buffer-live-p buf)
- (unless success
- (calendar-sync--log-silently "calendar-sync: Fetch error: curl failed: %s"
- (string-trim event)))
- (kill-buffer buf))
- (if success
- (funcall callback temp-file)
- (when (file-exists-p temp-file)
- (delete-file temp-file))
- (funcall callback nil)))))))
+ (calendar-sync--fetch-sentinel-finish
+ (and (eq (process-status process) 'exit)
+ (= (process-exit-status process) 0))
+ event temp-file (process-buffer process) callback)))))
(error
(calendar-sync--log-silently "calendar-sync: Fetch error: %s" (error-message-string err))
(funcall callback nil))))
diff --git a/modules/custom-buffer-file.el b/modules/custom-buffer-file.el
index 25555b53..0ca06cf9 100644
--- a/modules/custom-buffer-file.el
+++ b/modules/custom-buffer-file.el
@@ -51,8 +51,13 @@
(declare-function ps-print-buffer-with-faces "ps-print")
(declare-function ps-print-region-with-faces "ps-print")
-;; mm-decode for email viewing (mm-handle-type is a macro, needs early require)
-(require 'mm-decode)
+;; mm-handle-type is a macro used in `cj/--email-handle-is-type-p', so mm-decode
+;; is only needed at compile time here; `cj/view-email-in-buffer' requires it at
+;; runtime before any mm-* call, so the eager startup require is unnecessary.
+(eval-when-compile (require 'mm-decode))
+(declare-function mm-dissect-buffer "mm-decode")
+(declare-function mm-insert-part "mm-decode")
+(declare-function mm-destroy-parts "mm-decode")
(require 'external-open) ;; for cj/xdg-open, cj/open-this-file-with
(require 'system-lib) ;; cj/confirm-strong (overwrite confirms), used below
@@ -236,13 +241,16 @@ blast-radius operation on this map. The VC path is not double-prompted:
(cj/--delete-buffer-and-file))))))
(defun cj/copy-link-to-buffer-file ()
- "Copy the full file:// path of the current buffer's source file to the kill ring."
+ "Copy the full file:// path of the current buffer's source file to the kill ring.
+Signal a `user-error' when the buffer is not visiting a file, matching the
+other copy commands in this module."
(interactive)
(let ((file-path (buffer-file-name)))
- (when file-path
- (setq file-path (concat "file://" file-path))
- (kill-new file-path)
- (message "Copied file link to kill ring: %s" file-path))))
+ (unless file-path
+ (user-error "Buffer is not visiting a file"))
+ (setq file-path (concat "file://" file-path))
+ (kill-new file-path)
+ (message "Copied file link to kill ring: %s" file-path)))
(defvar cj/buffer-source-functions
'((eww-mode . (lambda () (eww-current-url)))
@@ -941,19 +949,24 @@ Signals an error if:
(let* ((handle (mm-dissect-buffer t))
(displayable-part (cj/--email-find-displayable-part handle))
(buffer-name (format "*Email: %s*" (file-name-nondirectory buffer-file-name))))
- (unless displayable-part
- (user-error "No displayable content found in email"))
- (with-current-buffer (get-buffer-create buffer-name)
- (let ((inhibit-read-only t))
- (erase-buffer)
- (mm-insert-part displayable-part)
- (goto-char (point-min))
- (when (cj/--email-handle-is-type-p displayable-part "text/html")
- (shr-render-region (point-min) (point-max)))
- (goto-char (point-min))
- (special-mode)))
- (mm-destroy-parts handle)
- (switch-to-buffer buffer-name)))
+ ;; `mm-dissect-buffer' allocates handles that must be freed even when we
+ ;; bail out early (no displayable part), so destroy them from the cleanup
+ ;; form rather than after the body.
+ (unwind-protect
+ (progn
+ (unless displayable-part
+ (user-error "No displayable content found in email"))
+ (with-current-buffer (get-buffer-create buffer-name)
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (mm-insert-part displayable-part)
+ (goto-char (point-min))
+ (when (cj/--email-handle-is-type-p displayable-part "text/html")
+ (shr-render-region (point-min) (point-max)))
+ (goto-char (point-min))
+ (special-mode)))
+ (switch-to-buffer buffer-name))
+ (mm-destroy-parts handle))))
;; --------------------------- Buffer And File Keymap --------------------------
diff --git a/modules/custom-case.el b/modules/custom-case.el
index 87622695..dde2d6c1 100644
--- a/modules/custom-case.el
+++ b/modules/custom-case.el
@@ -49,13 +49,14 @@
(downcase-region (car bounds) (cdr bounds))
(user-error "No symbol at point")))))
-(defun cj/--title-case-capitalize-word-p (word is-first prev-word-end word-skip chars-skip-reset)
+(defun cj/--title-case-capitalize-word-p (word is-first is-last prev-word-end word-skip chars-skip-reset)
"Return non-nil when WORD at point should be capitalized in title case.
Point is at WORD's first character. WORD is capitalized when it is the first
-word (IS-FIRST), is not a minor skip word (in WORD-SKIP), or immediately follows
-a skip-reset character (one of CHARS-SKIP-RESET: : ! ?), reached by skipping
-blanks back to PREV-WORD-END."
+word (IS-FIRST) or the last word (IS-LAST), is not a minor skip word (in
+WORD-SKIP), or immediately follows a skip-reset character (one of
+CHARS-SKIP-RESET: : ! ? .), reached by skipping blanks back to PREV-WORD-END."
(or is-first
+ is-last
(not (member word word-skip))
(save-excursion
(and (not (zerop (skip-chars-backward "[:blank:]" prev-word-end)))
@@ -67,22 +68,30 @@ Title case is a capitalization convention where major words are capitalized,
and most minor words are lowercase. Nouns, verbs (including linking verbs),
adjectives, adverbs,pronouns, and all words of four letters or more are
considered major words. Short (i.e., three letters or fewer) conjunctions,
-short prepositions, and all articles are considered minor words."
+short prepositions, and all articles are considered minor words. The first
+and last words are always capitalized, and a word following a sentence-ending
+period (or a colon, exclamation mark, or question mark) restarts
+capitalization even when it is a minor word."
(interactive)
(let ((beg nil)
(end nil)
(prev-word-end nil)
- ;; Allow capitals for skip characters after this, so:
- ;; Warning: An Example
- ;; Capitalizes the `An'.
- (chars-skip-reset '(?: ?! ??))
+ (last-word-start nil)
+ ;; Restart capitalization for a minor word after one of these, so
+ ;; "Warning: An Example" capitalizes the "An" and a sentence-ending
+ ;; period capitalizes the next word ("End. The Next").
+ (chars-skip-reset '(?: ?! ?? ?.))
;; Don't capitalize characters directly after these. e.g.
- ;; "Foo-bar" or "Foo\bar" or "Foo's".
+ ;; "Foo-bar" or "Foo\bar" or "Foo's". A period is here too, so
+ ;; "3.14" and "foo.bar" are left alone; a period followed by a
+ ;; blank still restarts via `chars-skip-reset' above.
(chars-separator '(?\\ ?- ?' ?.))
(word-chars "[:alnum:]")
+ ;; "is" and other linking verbs are major words, so they are not in
+ ;; this minor-word skip list.
(word-skip
(list "a" "an" "and" "as" "at" "but" "by"
- "for" "if" "in" "is" "nor" "of"
+ "for" "if" "in" "nor" "of"
"on" "or" "so" "the" "to" "yet"))
(is-first t))
(cond
@@ -92,6 +101,15 @@ short prepositions, and all articles are considered minor words."
(t
(setq beg (line-beginning-position))
(setq end (line-end-position))))
+ ;; The last word is always capitalized in title case, so find its start
+ ;; once: from END, skip back over any trailing non-word chars, then over
+ ;; the word itself.
+ (setq last-word-start
+ (save-excursion
+ (goto-char end)
+ (skip-chars-backward (concat "^" word-chars) beg)
+ (skip-chars-backward word-chars beg)
+ (point)))
(save-excursion
;; work on uppercased text (e.g., headlines) by downcasing first
(downcase-region beg end)
@@ -112,7 +130,8 @@ short prepositions, and all articles are considered minor words."
(unless (string-equal c-orig c-up)
(let ((word (buffer-substring-no-properties (point) word-end)))
(when (cj/--title-case-capitalize-word-p
- word is-first prev-word-end word-skip chars-skip-reset)
+ word is-first (= (point) last-word-start)
+ prev-word-end word-skip chars-skip-reset)
(delete-region (point) (1+ (point)))
(insert c-up))))))
(goto-char word-end)
diff --git a/modules/custom-comments.el b/modules/custom-comments.el
index a2604a55..73d29b0c 100644
--- a/modules/custom-comments.el
+++ b/modules/custom-comments.el
@@ -35,19 +35,19 @@
;; ------------------------------ Comment Reformat -----------------------------
(defun cj/comment-reformat ()
- "Reformat commented text into a single paragraph."
+ "Reformat the commented text in the active region into a single paragraph.
+Signal a `user-error' when no region is active."
(interactive)
- (if mark-active
- (let ((beg (region-beginning))
- (end (copy-marker (region-end)))
- (orig-fill-column fill-column))
- (uncomment-region beg end)
- (setq fill-column (- fill-column 3))
- (cj/join-line-or-region)
- (comment-region beg end)
- (setq fill-column orig-fill-column )))
- ;; if no region
- (message "No region was selected. Select the comment lines to reformat."))
+ (unless (use-region-p)
+ (user-error "No region selected: select the comment lines to reformat"))
+ (let ((beg (region-beginning))
+ (end (copy-marker (region-end)))
+ ;; Dynamically narrow the fill target for the join, then let it
+ ;; restore itself -- an error mid-join no longer strands fill-column.
+ (fill-column (- fill-column 3)))
+ (uncomment-region beg end)
+ (cj/join-line-or-region)
+ (comment-region beg end)))
;; ======================== Comment Generation Functions =======================
@@ -95,6 +95,14 @@ LENGTH is the total width of the line."
text-length
(if (> text-length 0) 2 0)) ; spaces around text
2))
+ ;; The right side fills the exact remaining width so the line always
+ ;; reaches LENGTH. Keying this off text-length parity (as before) left
+ ;; even-length and empty text two columns short, misaligning stacked
+ ;; dividers of differing text lengths.
+ (right-space (- available-width
+ text-length
+ (if (> text-length 0) 2 0)
+ space-on-each-side))
(min-space 2))
;; Validate we have enough space
(when (< space-on-each-side min-space)
@@ -108,10 +116,8 @@ LENGTH is the total width of the line."
;; Text with spaces
(when (> text-length 0)
(insert " " text " "))
- ;; Right decoration (handle odd-length text)
- (dotimes (_ (if (= (% text-length 2) 0)
- (- space-on-each-side 1)
- space-on-each-side))
+ ;; Right decoration -- fills the exact remaining width so the line reaches LENGTH.
+ (dotimes (_ right-space)
(insert decoration-char))
;; Comment end
(when (not (string-empty-p cmt-end))
diff --git a/modules/custom-datetime.el b/modules/custom-datetime.el
index 0528688c..52e8c2b3 100644
--- a/modules/custom-datetime.el
+++ b/modules/custom-datetime.el
@@ -51,7 +51,7 @@ See `format-time-string' for possible replacements.")
;; ------------------------------- Sortable Time -------------------------------
-(defvar sortable-time-format "%I:%M:%S %p %Z "
+(defvar sortable-time-format "%H:%M:%S %Z "
"Format string used by `cj/insert-sortable-time'.
See `format-time-string' for possible replacements.")
diff --git a/modules/custom-line-paragraph.el b/modules/custom-line-paragraph.el
index d29d4125..5d96fe41 100644
--- a/modules/custom-line-paragraph.el
+++ b/modules/custom-line-paragraph.el
@@ -46,7 +46,10 @@
(when (> (line-number-at-pos) 1)
(join-line))
(end-of-line)
- (newline)))
+ ;; Only add a newline at end of buffer. Doing it unconditionally left a
+ ;; stray blank line when joining a line in the middle of the buffer.
+ (when (eobp)
+ (newline))))
(defun cj/join-paragraph ()
"Join all lines in the current paragraph using `cj/join-line-or-region'."
@@ -67,18 +70,27 @@ produce malformed output silently."
(> (length comment-start) 0))))
(user-error
"Cannot comment in %s: no comment syntax defined" major-mode))
- (let* ((b (if (region-active-p) (region-beginning) (line-beginning-position)))
- (e (if (region-active-p) (region-end) (line-end-position)))
- (lines (split-string (buffer-substring-no-properties b e) "\n")))
+ ;; Normalize the bounds to whole lines: extend to the start of the first
+ ;; line and the end of the last line the region touches. The old open-line
+ ;; loop mishandled a region ending mid-line or at beginning-of-line, either
+ ;; splitting a line or duplicating a stray empty line.
+ (let* ((rb (if (region-active-p) (region-beginning) (point)))
+ (re (if (region-active-p) (region-end) (point)))
+ (beg (save-excursion (goto-char rb) (line-beginning-position)))
+ (end (save-excursion
+ (goto-char re)
+ ;; A region ending exactly at beginning-of-line does not
+ ;; include that line, so step back to the previous line's end.
+ (when (and (> re rb) (bolp))
+ (backward-char))
+ (line-end-position)))
+ (text (buffer-substring-no-properties beg end)))
(save-excursion
- (goto-char e)
- (dolist (line lines)
- (open-line 1)
- (forward-line 1)
- (insert line)
- ;; If the COMMENT prefix argument is non-nil, comment the inserted text
- (when comment
- (comment-region (line-beginning-position) (line-end-position)))))))
+ (goto-char end)
+ (insert "\n" text)
+ ;; Comment the freshly-inserted copy when the COMMENT prefix arg is set.
+ (when comment
+ (comment-region (1+ end) (point))))))
(defun cj/remove-duplicate-lines-region-or-buffer ()
"Remove duplicate lines in the region or buffer, keeping the first occurrence.
@@ -175,9 +187,9 @@ If not on a delimiter, show a message. Respects the current syntax table."
(cb (char-before))
;; Check if on opening paren
(open-p (and ca (eq (char-syntax ca) ?\()))
- ;; Check if on or just after closing paren
- (close-p (or (and ca (eq (char-syntax ca) ?\)))
- (and cb (eq (char-syntax cb) ?\))))))
+ ;; On a closing paren (point sits on it) vs just after one.
+ (on-close-p (and ca (eq (char-syntax ca) ?\))))
+ (after-close-p (and cb (eq (char-syntax cb) ?\)))))
(cond
;; Jump forward from opening
(open-p
@@ -185,12 +197,19 @@ If not on a delimiter, show a message. Respects the current syntax table."
(forward-sexp)
(scan-error
(message "No matching delimiter: %s" (error-message-string err)))))
- ;; Jump backward from closing
- (close-p
- (condition-case err
- (backward-sexp)
- (scan-error
- (message "No matching delimiter: %s" (error-message-string err)))))
+ ;; Jump backward from closing to its matching opener. When point is ON
+ ;; the closer, step past it first so `backward-sexp' spans the whole
+ ;; expression to the opener rather than the last inner sexp. Restore
+ ;; point if the delimiter is unmatched.
+ ((or on-close-p after-close-p)
+ (let ((start (point)))
+ (condition-case err
+ (progn
+ (when on-close-p (forward-char))
+ (backward-sexp))
+ (scan-error
+ (goto-char start)
+ (message "No matching delimiter: %s" (error-message-string err))))))
;; Not on delimiter
(t
(message "Point is not on a delimiter.")))))
diff --git a/modules/custom-ordering.el b/modules/custom-ordering.el
index 4dc5bff8..71477948 100644
--- a/modules/custom-ordering.el
+++ b/modules/custom-ordering.el
@@ -145,8 +145,15 @@ START and END identify the active region."
START and END define the region to operate on.
Returns the transformed string without modifying the buffer."
(cj/--ordering-validate-region start end)
- (let ((lines (split-string (buffer-substring start end) "\n")))
- (mapconcat #'identity (nreverse lines) "\n")))
+ ;; Strip a trailing newline before splitting so it doesn't become a spurious
+ ;; empty line (which reversing would float to the top); reattach it after.
+ ;; Internal blank lines are preserved.
+ (let* ((raw (buffer-substring start end))
+ (trailing-newline (string-suffix-p "\n" raw))
+ (body (if trailing-newline (substring raw 0 -1) raw))
+ (lines (split-string body "\n")))
+ (concat (mapconcat #'identity (nreverse lines) "\n")
+ (if trailing-newline "\n" ""))))
(defun cj/reverse-lines (start end)
"Reverse the order of lines in region between START and END.
@@ -164,20 +171,28 @@ ZERO-PAD when non-nil pads numbers with zeros for alignment.
Example with 100 lines: \"001\", \"002\", ..., \"100\".
Returns the transformed string without modifying the buffer."
(cj/--ordering-validate-region start end)
- (let* ((lines (split-string (buffer-substring start end) "\n"))
+ ;; Strip a trailing newline before splitting so it doesn't become a spurious
+ ;; extra numbered empty line; reattach it after. Internal blank lines are
+ ;; preserved and numbered.
+ (let* ((raw (buffer-substring start end))
+ (trailing-newline (string-suffix-p "\n" raw))
+ (body (if trailing-newline (substring raw 0 -1) raw))
+ (lines (split-string body "\n"))
(line-count (length lines))
(width (if zero-pad (length (number-to-string line-count)) 1))
(format-spec (if zero-pad (format "%%0%dd" width) "%d")))
- (mapconcat
- (lambda (pair)
- (let* ((num (car pair))
- (line (cdr pair))
- (num-str (format format-spec num)))
- (concat (replace-regexp-in-string "N" num-str format-string) line)))
- (cl-loop for line in lines
- for i from 1
- collect (cons i line))
- "\n")))
+ (concat
+ (mapconcat
+ (lambda (pair)
+ (let* ((num (car pair))
+ (line (cdr pair))
+ (num-str (format format-spec num)))
+ (concat (replace-regexp-in-string "N" num-str format-string) line)))
+ (cl-loop for line in lines
+ for i from 1
+ collect (cons i line))
+ "\n")
+ (if trailing-newline "\n" ""))))
(defun cj/number-lines (start end format-string zero-pad)
"Number lines in region between START and END with custom format.
diff --git a/modules/custom-text-enclose.el b/modules/custom-text-enclose.el
index 4d72347d..12d2deaf 100644
--- a/modules/custom-text-enclose.el
+++ b/modules/custom-text-enclose.el
@@ -180,9 +180,14 @@ Returns the indented text without modifying the buffer."
(defun cj/indent-lines-in-region-or-buffer (count use-tabs)
"Indent each line in region or buffer by COUNT characters.
-COUNT is the number of characters to indent (default 4).
-USE-TABS when non-nil (prefix argument) uses tabs instead of spaces."
- (interactive "p\nP")
+COUNT is the numeric prefix argument, defaulting to 4 with no prefix.
+USE-TABS non-nil indents with tabs instead of spaces; interactively it
+follows the buffer's `indent-tabs-mode', so the prefix argument is free to
+mean the count. Call it from Lisp with an explicit USE-TABS to override."
+ (interactive (list (if current-prefix-arg
+ (prefix-numeric-value current-prefix-arg)
+ 4)
+ indent-tabs-mode))
(let* ((bounds (cj/--region-or-buffer-bounds))
(start-pos (car bounds))
(end-pos (cdr bounds))
@@ -224,9 +229,11 @@ Returns the dedented text without modifying the buffer."
(defun cj/dedent-lines-in-region-or-buffer (count)
"Remove up to COUNT leading whitespace characters from each line.
-COUNT is the number of characters to remove (default 4).
+COUNT is the numeric prefix argument, defaulting to 4 with no prefix.
Works on region if active, otherwise entire buffer."
- (interactive "p")
+ (interactive (list (if current-prefix-arg
+ (prefix-numeric-value current-prefix-arg)
+ 4)))
(let* ((bounds (cj/--region-or-buffer-bounds))
(start-pos (car bounds))
(end-pos (cdr bounds))
diff --git a/modules/duet-config.el b/modules/duet-config.el
deleted file mode 100644
index 2dc7ad2e..00000000
--- a/modules/duet-config.el
+++ /dev/null
@@ -1,19 +0,0 @@
-;;; duet-config.el --- DUET dual-pane commander configuration -*- lexical-binding: t -*-
-
-;;; Commentary:
-;; Personal configuration glue for the DUET package, developed locally at
-;; ~/code/duet. Keybindings, defcustom values, and connection storage live
-;; here; the package itself stays free of personal opinions.
-;;
-;; Not yet required from init.el — DUET is a pre-alpha skeleton. Wire it in
-;; once Stage 1 provides usable commands.
-
-;;; Code:
-
-(use-package duet
- :load-path "~/code/duet"
- :ensure nil
- :commands (duet))
-
-(provide 'duet-config)
-;;; duet-config.el ends here
diff --git a/modules/flyspell-and-abbrev.el b/modules/flyspell-and-abbrev.el
index b73bfdf3..ebe4898e 100644
--- a/modules/flyspell-and-abbrev.el
+++ b/modules/flyspell-and-abbrev.el
@@ -197,9 +197,10 @@ Without prefix argument, it's created in the global abbrev table.
Press C-' repeatedly to step through misspellings one at a time."
(interactive "P")
(cj/--require-spell-checker)
- ;; Run flyspell-buffer only if buffer hasn't been checked yet
- (unless (bound-and-true-p flyspell-mode)
- (flyspell-buffer))
+ ;; Enable Flyspell for the buffer type so the mode sticks and the buffer is
+ ;; scanned once. A bare flyspell-buffer here never turned the mode on, so
+ ;; the guard never tripped and every C-' press re-scanned the whole buffer.
+ (cj/flyspell-on-for-buffer-type)
(let ((misspelled-word (cj/flyspell-goto-previous-misspelling (point))))
(if (not misspelled-word)
diff --git a/modules/font-config.el b/modules/font-config.el
index 3aa3d80f..e5afb361 100644
--- a/modules/font-config.el
+++ b/modules/font-config.el
@@ -210,6 +210,15 @@ the fontset repeatedly is harmless, so it can be called from
;; ---------------------------------- Emojify ----------------------------------
;; converts emoji identifiers into emojis; allows for easy emoji entry.
+(defvar emojify-display-style) ;; emojify's, forward-declared for the helper
+
+(defun cj/set-emojify-display-style ()
+ "Set `emojify-display-style' to `image' on a graphical frame, else `unicode'.
+Image emoji only render on a GUI frame. In daemon mode no GUI frame exists when
+emojify loads, so this runs per-frame from `server-after-make-frame-hook';
+otherwise the value would latch to `unicode' and GUI frames never get images."
+ (setq emojify-display-style (if (env-gui-p) 'image 'unicode)))
+
(use-package emojify
:defer 1
:hook ((erc-mode . emojify-mode))
@@ -221,7 +230,11 @@ the fontset repeatedly is harmless, so it can be called from
:config
(setq emojify-show-help nil)
(setq emojify-point-entered-behaviour 'uncover)
- (setq emojify-display-style (if (env-gui-p) 'image 'unicode))
+ ;; In daemon mode `env-gui-p' is nil at :config time (no GUI frame yet), so
+ ;; recompute the display style per-frame; otherwise set it now.
+ (if (daemonp)
+ (add-hook 'server-after-make-frame-hook #'cj/set-emojify-display-style)
+ (cj/set-emojify-display-style))
(setq emojify-emoji-styles '(ascii unicode github))
;; Disable emojify in programming modes
@@ -242,21 +255,24 @@ the fontset repeatedly is harmless, so it can be called from
(let ((font-list (font-family-list)))
(setq font-list (cl-remove-duplicates (cl-sort font-list 'string-lessp :key 'downcase)))
(with-current-buffer "*Available Fonts*"
- (erase-buffer)
- (dolist (font-family font-list)
- (insert (propertize (concat font-family) 'face '(font-lock-keyword-face (:weight bold))))
- (insert (concat "\n"(propertize "Regular: ")))
- (insert (propertize (concat "The quick brown fox jumps over the lazy dog I 1 l ! : ; . , 0 O o [ { ( ) } ] ?")
- 'face `((:family, font-family))))
- (insert (concat "\n" (propertize "Bold: ")))
- (insert (propertize (concat "The quick brown fox jumps over the lazy dog I 1 l ! : ; . , 0 O o [ { ( ) } ] ?")
- 'face `((:family, font-family :weight bold))))
- (insert (concat "\n" (propertize "Italic: ")))
- (insert (propertize (concat "The quick brown fox jumps over the lazy dog I 1 l ! : ; . , 0 O o [ { ( ) } ] ?")
- 'face `((:family, font-family :slant italic))))
- (insert (concat "\n\n"))))
- (move-to-window-line 0)
- (special-mode)))
+ ;; The buffer is left in `special-mode' (read-only) after the first call,
+ ;; so re-running must relax read-only to erase and rewrite it.
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (dolist (font-family font-list)
+ (insert (propertize (concat font-family) 'face '(font-lock-keyword-face (:weight bold))))
+ (insert (concat "\n"(propertize "Regular: ")))
+ (insert (propertize (concat "The quick brown fox jumps over the lazy dog I 1 l ! : ; . , 0 O o [ { ( ) } ] ?")
+ 'face `((:family, font-family))))
+ (insert (concat "\n" (propertize "Bold: ")))
+ (insert (propertize (concat "The quick brown fox jumps over the lazy dog I 1 l ! : ; . , 0 O o [ { ( ) } ] ?")
+ 'face `((:family, font-family :weight bold))))
+ (insert (concat "\n" (propertize "Italic: ")))
+ (insert (propertize (concat "The quick brown fox jumps over the lazy dog I 1 l ! : ; . , 0 O o [ { ( ) } ] ?")
+ 'face `((:family, font-family :slant italic))))
+ (insert (concat "\n\n"))))
+ (move-to-window-line 0)
+ (special-mode))))
(keymap-global-set "C-z F" #'cj/display-available-fonts)
diff --git a/modules/keyboard-compat.el b/modules/keyboard-compat.el
index b72362e7..73138ca5 100644
--- a/modules/keyboard-compat.el
+++ b/modules/keyboard-compat.el
@@ -98,8 +98,7 @@ Meta+Shift+letter triggers M-S-letter keybindings."
(define-key key-translation-map (kbd "M-D") (kbd "M-S-d"))
(define-key key-translation-map (kbd "M-I") (kbd "M-S-i"))
(define-key key-translation-map (kbd "M-C") (kbd "M-S-c"))
- (define-key key-translation-map (kbd "M-B") (kbd "M-S-b"))
- (define-key key-translation-map (kbd "M-K") (kbd "M-S-k"))))
+ (define-key key-translation-map (kbd "M-B") (kbd "M-S-b"))))
;; In daemon mode, no frame exists at startup so env-gui-p returns nil.
;; Use server-after-make-frame-hook to set up translations when the first
diff --git a/modules/keyboard-macros.el b/modules/keyboard-macros.el
index 4e801096..bca36eed 100644
--- a/modules/keyboard-macros.el
+++ b/modules/keyboard-macros.el
@@ -43,7 +43,7 @@
;;; Code:
(require 'subr-x) ;; for string-trim
-(eval-when-compile (require 'user-constants))
+(require 'user-constants) ;; for macros-file, read at runtime
(defvar cj/macros-loaded nil
"Whether saved keyboard macros have been loaded from file.")
@@ -130,15 +130,7 @@ With prefix arg, open the macros file for editing after saving."
(keymap-global-set "C-<f3>" #'cj/kbd-macro-start-or-end)
(keymap-global-set "<f3>" #'call-last-kbd-macro)
(keymap-global-set "M-<f3>" #'cj/save-maybe-edit-macro)
- (keymap-global-set "s-<f3>" #'cj/open-macros-file)
- (add-hook 'kill-emacs-hook #'cj/save-last-kbd-macro-on-exit))
-
-;; Add hook to save any unnamed macros on exit if desired
-(defun cj/save-last-kbd-macro-on-exit ()
- "Save the last keyboard macro before exiting Emacs if it's not saved."
- (when last-kbd-macro
- (when (y-or-n-p "Save last keyboard macro before exiting? ")
- (call-interactively #'cj/save-maybe-edit-macro))))
+ (keymap-global-set "s-<f3>" #'cj/open-macros-file))
;; Auto-call setup after init
(if after-init-time
diff --git a/modules/local-repository.el b/modules/local-repository.el
index e3c7a227..0f2c981c 100644
--- a/modules/local-repository.el
+++ b/modules/local-repository.el
@@ -6,72 +6,28 @@
;; Layer: 4 (Optional).
;; Category: O/D/P.
;; Load shape: eager.
-;; Eager reason: none; local package mirror commands can autoload.
+;; Eager reason: none; the mirror-refresh command can autoload.
;; Top-level side effects: none.
;; 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.
+;; Provides a command to refresh the checked-in local package archive from the
+;; installed packages via elpa-mirror. Adding that archive to package-archives
+;; is owned by early-init.el (see `localrepo-location'); this module only
+;; refreshes it.
;;; Code:
(require 'elpa-mirror nil t) ;; optional; cj/update-localrepo-repository fails at call-time if absent
(declare-function elpamr-create-mirror-for-installed "elpa-mirror")
-
-;; ------------------------------ Utility Function -----------------------------
-
-
-(defun localrepo--car-member (value list)
- "Check if VALUE exists as the car of any cons cell in LIST."
- (member value (mapcar #'car list)))
-
-;; ------------------------------- Customizations ------------------------------
-
-(defgroup localrepo nil
- "Local last-known-good package repository."
- :group 'package)
-
-(defcustom localrepo-repository-id "localrepo"
- "The name used to identify the local repository internally.
-
-Used for the package-archive and package-archive-priorities lists."
- :type 'string
- :group 'localrepo)
-
-(defcustom localrepo-repository-priority 100
- "The value for the local repository in the package-archive-priority list.
-
-A higher value means higher priority. If you want your local packages to be
-preferred, this must be a higher number than any other repositories."
- :type 'integer
- :group 'localrepo)
-
-(defcustom localrepo-repository-location
- (concat user-emacs-directory "/.localrepo")
- "The location of the local repository.
-
-It's a good idea to keep this with the rest of your configuration files and
-keep them in source control."
- :type 'directory
- :group 'localrepo)
+(defvar localrepo-location) ;; defconst in early-init.el: the archive path
(defun cj/update-localrepo-repository ()
- "Update the local repository with currently installed packages."
+ "Update the local repository with currently installed packages.
+Targets `localrepo-location', the archive path early-init.el sets up."
(interactive)
- (elpamr-create-mirror-for-installed localrepo-repository-location t))
-
-(defun localrepo-initialize ()
-"Add the repository to the package archives, then gives it a high priority."
- (unless (localrepo--car-member localrepo-repository-id package-archives)
- (add-to-list 'package-archives
- (cons localrepo-repository-id localrepo-repository-location)))
-
- (unless (localrepo--car-member localrepo-repository-id package-archive-priorities)
- (add-to-list 'package-archive-priorities
- (cons localrepo-repository-id localrepo-repository-priority))))
+ (elpamr-create-mirror-for-installed localrepo-location t))
(provide 'local-repository)
;;; local-repository.el ends here.
diff --git a/modules/lorem-optimum.el b/modules/lorem-optimum.el
index 8aa96345..14f1d666 100644
--- a/modules/lorem-optimum.el
+++ b/modules/lorem-optimum.el
@@ -219,8 +219,22 @@ Builds and caches the keys list lazily if not already cached."
(message "Lorem-optimum learned from file: %s" file))
(defun cj/lipsum (n)
- "Return N words of lorem ipsum."
- (cj/markov-generate cj/lipsum-chain n '("Lorem" "ipsum")))
+ "Return N words of lorem ipsum.
+Interactively, prompt for N and echo the generated words.
+
+Signal a `user-error' when the Markov chain is empty (for example when the
+training file `cj/lipsum-default-file' is missing). Without this, callers
+such as `cj/lipsum-insert' would insert nil and raise a cryptic wrong-type
+error far from the cause. Train the chain with `cj/lipsum-learn-file',
+`cj/lipsum-learn-buffer', or `cj/lipsum-learn-region', or restore the file."
+ (interactive "nNumber of words: ")
+ (let ((text (cj/markov-generate cj/lipsum-chain n '("Lorem" "ipsum"))))
+ (unless (and (stringp text) (not (string-empty-p text)))
+ (user-error "Lorem-optimum chain is empty; train it with cj/lipsum-learn-file or restore %s"
+ cj/lipsum-default-file))
+ (when (called-interactively-p 'any)
+ (message "%s" text))
+ text))
(defun cj/lipsum-insert (n)
"Insert N words of lorem ipsum at point."
diff --git a/modules/mu4e-org-contacts-setup.el b/modules/mu4e-org-contacts-setup.el
deleted file mode 100644
index bfb9b1f2..00000000
--- a/modules/mu4e-org-contacts-setup.el
+++ /dev/null
@@ -1,31 +0,0 @@
-;;; mu4e-org-contacts-setup.el --- Setup mu4e with org-contacts -*- lexical-binding: t; -*-
-;; author: Craig Jennings <c@cjennings.net>
-
-;;; Commentary:
-;;
-;; 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:
-
-(defvar mu4e-compose-complete-only-personal)
-(defvar mu4e-compose-complete-only-after)
-(declare-function cj/activate-mu4e-org-contacts-integration "mu4e-org-contacts-integration")
-
-;; Load the integration module. Activation only runs when the module loaded
-;; cleanly AND mu4e is present; otherwise this file is a no-op so the rest
-;; of the config can load without mu4e installed.
-(when (require 'mu4e-org-contacts-integration nil t)
- (when (featurep 'mu4e)
- (cj/activate-mu4e-org-contacts-integration)))
-
-;; Optional: If you want to use org-contacts as the primary source,
-;; you might want to disable mu4e's contact caching to save memory
-(with-eval-after-load 'mu4e
- ;; Disable mu4e's internal contact collection
- (setq mu4e-compose-complete-only-personal nil)
- (setq mu4e-compose-complete-only-after nil))
-
-(provide 'mu4e-org-contacts-setup)
-;;; mu4e-org-contacts-setup.el ends here \ No newline at end of file
diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el
index 207c286e..d4407b28 100644
--- a/modules/org-agenda-config.el
+++ b/modules/org-agenda-config.el
@@ -50,10 +50,10 @@
:demand t
:config
(setq org-agenda-prefix-format '((agenda . " %i %-25:c%?-12t% s")
- (timeline . " % s")
(todo . " %i %-25:c")
(tags . " %i %-12:c")
(search . " %i %-12:c")))
+ (setq org-agenda-timegrid-use-ampm t) ;; show the agenda time grid in 12-hour am/pm
(setq org-agenda-dim-blocked-tasks 'invisible)
(setq org-agenda-skip-scheduled-if-done nil)
(setq org-agenda-remove-tags t)
diff --git a/modules/org-babel-config.el b/modules/org-babel-config.el
index 79661013..51919da1 100644
--- a/modules/org-babel-config.el
+++ b/modules/org-babel-config.el
@@ -173,8 +173,5 @@ session when working in trusted files, and back on when done."
;; requires ob-racket, not yet in repositories
;; (add-to-list 'org-structure-template-alist '("sicp" . "src racket :lang sicp"))
-;; drop Org’s default footnote list at the end
-(setq org-html-footnote-separator "")
-
(provide 'org-babel-config)
;;; org-babel-config.el ends here.
diff --git a/modules/org-contacts-config.el b/modules/org-contacts-config.el
index 944d75c1..39ff9910 100644
--- a/modules/org-contacts-config.el
+++ b/modules/org-contacts-config.el
@@ -170,29 +170,40 @@ Added: %U"
(require 'system-lib)
+(defun cj/--org-contacts-collect (buffer)
+ "Return an alist of (NAME POSITION INFO) for the contact headings in BUFFER.
+NAME is the heading text, POSITION its buffer position, and INFO the
+EMAIL or PHONE property value (or nil)."
+ (with-current-buffer buffer
+ (org-map-entries
+ (lambda ()
+ (list (nth 4 (org-heading-components))
+ (point)
+ (or (org-entry-get nil "EMAIL")
+ (org-entry-get nil "PHONE"))))
+ nil nil)))
+
(defun cj/org-contacts-find ()
- "Find and open a contact."
+ "Find a contact and jump to its heading.
+Collect the contact headings before prompting, so cancelling the prompt
+leaves point where it was, and jump to the selected heading's stored
+position instead of a text search that could land inside another entry."
(interactive)
- (find-file contacts-file)
- (goto-char (point-min))
- (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)))
+ (let* ((buf (find-file-noselect contacts-file))
+ (alist (cj/--org-contacts-collect buf))
(contact (completing-read
"Find contact: "
(cj/completion-table-annotated
'contact
(lambda (cand)
- (let ((info (cdr (assoc cand alist))))
+ (let ((info (nth 2 (assoc cand alist))))
(when (and info (> (length info) 0))
(concat " " (propertize info 'face
'completions-annotations)))))
- alist))))
- (goto-char (point-min))
- (search-forward contact)
+ alist)
+ nil t)))
+ (switch-to-buffer buf)
+ (goto-char (nth 1 (assoc contact alist)))
(org-fold-show-entry)
(org-reveal)))
diff --git a/modules/org-drill-config.el b/modules/org-drill-config.el
index 29f6130a..f53f36b9 100644
--- a/modules/org-drill-config.el
+++ b/modules/org-drill-config.el
@@ -134,25 +134,42 @@ With a prefix arg OTHER-DIR, prompt for the directory instead of `drill-dir'."
;; --------------------------------- Org Drill ---------------------------------
-(use-package org-drill
- ;; :vc (:url "git@cjennings.net:org-drill.git"
- ;; :branch "main"
- ;; :rev :newest)
- :load-path "~/code/org-drill" ;; local dev checkout — switch back to :vc above when done
- :after (org org-capture)
- :demand t
- :commands (org-drill org-drill-resume)
- :custom
- (org-drill-leech-failure-threshold 50 "leech cards = 50 wrong answers")
- (org-drill-leech-method 'warn "leech cards show warnings")
- (org-drill-use-visible-cloze-face-p t "cloze text shows up in a different font")
- (org-drill-hide-item-headings-p t "don't show heading text")
- (org-drill-maximum-items-per-session 100 "drill sessions end after 100 cards")
- (org-drill-maximum-duration 30 "each drill session can last up to 30 mins")
- (org-drill-add-random-noise-to-intervals-p t "vary the days to repetition slightly")
- (org-drill-text-size-during-session 24 "24-point font for comfortable reading")
- (org-drill-use-variable-pitch t "variable-pitch font for readability")
- (org-drill-hide-modeline-during-session t "hide the modeline for a cleaner display"))
+(defconst cj/org-drill-dev-checkout (expand-file-name "org-drill" "~/code/")
+ "Local org-drill development checkout, preferred when it exists.")
+
+(defun cj/--org-drill-source-keywords (&optional checkout)
+ "Return the use-package source keywords for org-drill.
+With CHECKOUT (default `cj/org-drill-dev-checkout') an existing directory,
+load from it via :load-path. Otherwise install from upstream via :vc, so
+drill still loads on a machine without the dev checkout (bare :load-path +
+:demand t would fail to load there)."
+ (let ((dir (or checkout cj/org-drill-dev-checkout)))
+ (if (file-directory-p dir)
+ (list :load-path dir)
+ (list :vc '(:url "git@cjennings.net:org-drill.git"
+ :branch "main"
+ :rev :newest)))))
+
+;; `use-package' keywords must be literals at macro-expansion, so the
+;; source keyword is spliced in through `eval' at load time (same idiom as
+;; the computed flycheck checker path elsewhere in the config).
+(eval
+ `(use-package org-drill
+ ,@(cj/--org-drill-source-keywords)
+ :after (org org-capture)
+ :demand t
+ :commands (org-drill org-drill-resume)
+ :custom
+ (org-drill-leech-failure-threshold 50 "leech cards = 50 wrong answers")
+ (org-drill-leech-method 'warn "leech cards show warnings")
+ (org-drill-use-visible-cloze-face-p t "cloze text shows up in a different font")
+ (org-drill-hide-item-headings-p t "don't show heading text")
+ (org-drill-maximum-items-per-session 100 "drill sessions end after 100 cards")
+ (org-drill-maximum-duration 30 "each drill session can last up to 30 mins")
+ (org-drill-add-random-noise-to-intervals-p t "vary the days to repetition slightly")
+ (org-drill-text-size-during-session 24 "24-point font for comfortable reading")
+ (org-drill-use-variable-pitch t "variable-pitch font for readability")
+ (org-drill-hide-modeline-during-session t "hide the modeline for a cleaner display")))
(provide 'org-drill-config)
;;; org-drill-config.el ends here.
diff --git a/modules/org-export-config.el b/modules/org-export-config.el
index 5a6f09fc..c3d3294c 100644
--- a/modules/org-export-config.el
+++ b/modules/org-export-config.el
@@ -20,7 +20,6 @@
;; - HTML: Web publishing with HTML5 support
;; - Markdown: README files and web content
;; - ODT: Office documents for LibreOffice/MS Word
-;; - Texinfo: GNU documentation and Info files
;;
;; Extended via Pandoc:
;; - Additional formats: DOCX, self-contained HTML5
@@ -28,7 +27,7 @@
;;
;; Key features:
;; - UTF-8 encoding enforced across all backends
-;; - Subtree export as default scope
+;; - Buffer export as default scope
;;
;; Note: reveal.js presentations are handled by org-reveal-config.el (C-; p)
;;
@@ -68,17 +67,8 @@
:config
(setq org-html-postamble nil)
(setq org-html-html5-fancy t)
- (setq org-html-head-include-default-style nil))
-
-
-(use-package ox-texinfo
- :ensure nil ; Built into Org
- :defer t
- :after ox
- :config
- (setq org-texinfo-coding-system 'utf-8)
- (setq org-texinfo-default-class "info")
- (add-to-list 'org-export-backends 'texinfo))
+ (setq org-html-head-include-default-style nil)
+ (setq org-html-footnote-separator "")) ;; no separator between adjacent footnote refs
(use-package ox-pandoc
:defer t
diff --git a/modules/org-reveal-config.el b/modules/org-reveal-config.el
index be702bf7..f842680b 100644
--- a/modules/org-reveal-config.el
+++ b/modules/org-reveal-config.el
@@ -8,9 +8,10 @@
;; Load shape: eager.
;; Eager reason: none; presentation export is a command-loaded deferral
;; candidate for Phase 4.
-;; Top-level side effects: package configuration via use-package.
-;; Runtime requires: none (configures packages via use-package).
-;; Direct test load: yes.
+;; Top-level side effects: registers a presentation prefix keymap under
+;; cj/custom-keymap; package configuration via use-package.
+;; Runtime requires: keybindings.
+;; Direct test load: yes (requires keybindings explicitly).
;;
;; Integrates ox-reveal for creating reveal.js presentations from Org files.
;;
@@ -28,6 +29,8 @@
;;; Code:
+(require 'keybindings) ;; cj/register-prefix-map, cj/custom-keymap
+
;; Forward declarations for byte-compiler (ox-reveal loaded via use-package)
(defvar org-reveal-root)
(defvar org-reveal-single-file)
@@ -238,17 +241,25 @@ reveal.js headers pre-filled."
;; -------------------------------- Keybindings --------------------------------
-(global-set-key (kbd "C-; p SPC") #'cj/reveal-present)
-(global-set-key (kbd "C-; p e") #'cj/reveal-export)
-(global-set-key (kbd "C-; p p") #'cj/reveal-preview-start)
-(global-set-key (kbd "C-; p s") #'cj/reveal-preview-stop)
-(global-set-key (kbd "C-; p h") #'cj/reveal-insert-header)
-(global-set-key (kbd "C-; p H") #'cj/reveal-remove-headers)
-(global-set-key (kbd "C-; p n") #'cj/reveal-new)
+;; A registered prefix keymap, not raw `global-set-key' chains: binding
+;; "C-; p ..." directly depends on keybindings.el having already made "C-;"
+;; a live prefix (otherwise "non-prefix key" errors), while
+;; `cj/register-prefix-map' binds into `cj/custom-keymap' with no load-order
+;; dependency beyond requiring keybindings.
+(defvar-keymap cj/reveal-map
+ :doc "Keymap for reveal.js presentation commands."
+ "SPC" #'cj/reveal-present
+ "e" #'cj/reveal-export
+ "p" #'cj/reveal-preview-start
+ "s" #'cj/reveal-preview-stop
+ "h" #'cj/reveal-insert-header
+ "H" #'cj/reveal-remove-headers
+ "n" #'cj/reveal-new)
+
+(cj/register-prefix-map "p" cj/reveal-map "presentations")
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
- "C-; p" "presentations"
"C-; p SPC" "present current buffer"
"C-; p e" "export & open"
"C-; p p" "start live preview"
diff --git a/modules/org-roam-config.el b/modules/org-roam-config.el
index eca867df..867f2d99 100644
--- a/modules/org-roam-config.el
+++ b/modules/org-roam-config.el
@@ -34,7 +34,6 @@
;; External variables, declared special so byte-compilation doesn't treat them
;; as free references/assignments. Owned by org and org-roam-dailies.
-(defvar org-agenda-timegrid-use-ampm)
(defvar org-roam-dailies-map)
(defvar org-last-state)
@@ -77,30 +76,27 @@ FILETAGS and TITLE must sit on separate lines so Org parses the
:unnarrowed t)
("v" "v2mom" plain
- (file ,(concat user-emacs-directory "org-roam-templates/v2mom.org"))
+ (file ,(concat roam-dir "templates/v2mom.org"))
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "")
:unnarrowed t)
("r" "recipe" plain
- (file ,(concat user-emacs-directory "org-roam-templates/recipe.org"))
+ (file ,(concat roam-dir "templates/recipe.org"))
:if-new (file+head "recipes/%<%Y%m%d%H%M%S>-${slug}.org" "")
:unnarrowed t)
("t" "topic" plain
- (file ,(concat user-emacs-directory "org-roam-templates/topic.org"))
+ (file ,(concat roam-dir "templates/topic.org"))
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "")
:unnarrowed t)))
:bind (("C-c n l" . org-roam-buffer-toggle)
("C-c n f" . org-roam-node-find)
- ("C-c n p" . cj/org-roam-find-node-project)
("C-c n i" . org-roam-node-insert)
- ("C-c n w" . cj/org-roam-find-node-webclip)
:map org-mode-map
("C-M-i" . completion-at-point))
:config
;; org-log-done is set once in org-config.el (cj/org-todo-settings).
- (setq org-agenda-timegrid-use-ampm t)
;; Don't build the org-refile targets cache here. org-refile-config.el
;; already schedules it on a 5s idle timer; doing it in org-roam's :config
@@ -209,10 +205,17 @@ created in that subdirectory of `org-roam-directory'."
(interactive)
(cj/org-roam-find-node "Recipe" "r" (concat roam-dir "templates/recipe.org") "recipes/"))
+
+(defun cj/org-roam-find-node-project ()
+ "List nodes of type \"Project\" in completing read for selection or creation."
+ (interactive)
+ (cj/org-roam-find-node "Project" "p" (concat roam-dir "templates/project.org")))
+
;; Bound after their defuns (not in the use-package :bind) so the byte-compiler
;; doesn't see both a :bind autoload and the real defun as two definitions.
(keymap-global-set "C-c n r" #'cj/org-roam-find-node-recipe)
(keymap-global-set "C-c n t" #'cj/org-roam-find-node-topic)
+(keymap-global-set "C-c n p" #'cj/org-roam-find-node-project)
;; ---------------------- Org Capture After Finalize Hook ----------------------
@@ -394,36 +397,6 @@ cut stays undoable. A confirmation prompt guards large subtrees (see
(org-roam-db-sync)
(message "'%s' moved to a new org-roam node (%s)." title filename))))
-;; TASK: Need to decide keybindings before implementation and testing
-;; (use-package consult-org-roam
-;; :ensure t
-;; :after org-roam
-;; :init
-;; (require 'consult-org-roam)
-;; ;; Activate the minor mode
-;; (consult-org-roam-mode 1)
-;; :custom
-;; ;; Use `ripgrep' for searching with `consult-org-roam-search'
-;; (consult-org-roam-grep-func #'consult-ripgrep)
-;; ;; Configure a custom narrow key for `consult-buffer'
-;; (consult-org-roam-buffer-narrow-key ?r)
-;; ;; Display org-roam buffers right after non-org-roam buffers
-;; ;; in consult-buffer (and not down at the bottom)
-;; (consult-org-roam-buffer-after-buffers t)
-;; :config
-;; ;; Eventually suppress previewing for certain functions
-;; (consult-customize
-;; consult-org-roam-forward-links
-;; :preview-key "M-.")
-;; :bind
-;; ;; Define some convenient keybindings as an addition
-;; ("C-c n e" . consult-org-roam-file-find)
-;; ("C-c n b" . consult-org-roam-backlinks)
-;; ("C-c n B" . consult-org-roam-backlinks-recursive)
-;; ("C-c n l" . consult-org-roam-forward-links)
-;; ("C-c n r" . consult-org-roam-search))
-
-
;; which-key labels
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
@@ -434,7 +407,6 @@ cut stays undoable. A confirmation prompt guards large subtrees (see
"C-c n r" "roam find recipe"
"C-c n t" "roam find topic"
"C-c n i" "roam insert node"
- "C-c n w" "roam find webclip"
"C-c n I" "roam insert immediate"
"C-c n d" "roam dailies menu"))
diff --git a/modules/org-webclipper.el b/modules/org-webclipper.el
index 40ceada7..217aecfa 100644
--- a/modules/org-webclipper.el
+++ b/modules/org-webclipper.el
@@ -186,22 +186,6 @@ Return the yanked content as a string so templates can insert it."
;; extract the webpage content from the kill ring
(car kill-ring)))
-;; ----------------------------- Webclipper Keymap -----------------------------
-
-;; keymaps shouldn't be required for webclipper
-;; Setup keymaps
-;;
-;; (defun cj/webclipper-setup-keymaps ()
-;; "Setup webclipper keymaps."
-;; (define-prefix-command 'cj/webclipper-map nil
-;; "Keymap for weblipper operations.")
-;; (define-key cj/custom-keymap "c" 'cj/webclipper-map)
-;; (define-key cj/webclipper-map "n" 'cj/move-org-branch-to-roam))
-
-;; ;; Call keymap setup if cj/custom-keymap is already defined
-;; (when (boundp 'cj/custom-keymap)
-;; (cj/webclipper-setup-keymaps))
-
;; Register protocol handler early for external calls
(with-eval-after-load 'org-protocol
(unless (assoc "webclip" org-protocol-protocol-alist)
@@ -211,9 +195,5 @@ Return the yanked content as a string so templates can insert it."
:function cj/org-protocol-webclip
:kill-client t))))
-;; (with-eval-after-load 'cj/custom-keymap
-;; (require 'org-webclipper)
-;; (cj/webclipper-setup-keymaps))
-
(provide 'org-webclipper)
;;; org-webclipper.el ends here
diff --git a/modules/selection-framework.el b/modules/selection-framework.el
index 7f7f9a47..8e3ee252 100644
--- a/modules/selection-framework.el
+++ b/modules/selection-framework.el
@@ -41,7 +41,9 @@
(vertico-cycle t) ; Cycle through candidates
(vertico-count 10) ; Number of candidates to display
(vertico-resize nil) ; Don't resize the minibuffer
- (vertico-sort-function #'vertico-sort-history-alpha) ; History first, then alphabetical
+ ;; Sorting is owned by `vertico-prescient-mode' (frecency). A
+ ;; `vertico-sort-function' set here is overridden inside every vertico
+ ;; session, so it is omitted rather than left as dead config.
:bind (:map vertico-map
("C-j" . vertico-next)
("C-k" . vertico-previous)
diff --git a/modules/show-kill-ring.el b/modules/show-kill-ring.el
deleted file mode 100644
index e65d48b5..00000000
--- a/modules/show-kill-ring.el
+++ /dev/null
@@ -1,122 +0,0 @@
-;;; show-kill-ring.el --- Displays Previous Kill Ring Entries -*- lexical-binding: t; coding: utf-8; -*-
-;; Show Kill Ring
-;; Stolen from Steve Yegge when he wasn't looking
-;; enhancements and bugs added by Craig Jennings <c@cjennings.net>
-;;
-;;; Commentary:
-;; Browse items you've previously killed.
-;; Yank text using C-u, the index, then C-y.
-;;
-;; I've lovingly kept the nice 1970s aesthetic, complete with wood paneling.
-;; Maybe I'll give it a makeover at some point.
-;;
-;;; Code:
-
-(require 'cl-lib)
-
-(defvar show-kill-max-item-size 1000
- "This represents the size of a \='kill ring\=' entry.
-A positive number means to limit the display of \='kill-ring\=' items to
-that number of characters.")
-
-(defun show-kill-ring-exit ()
- "Exit the show-kill-ring buffer."
- (interactive)
- (quit-window t))
-
-(defun show-kill-ring ()
- "Show the current contents of the kill ring in a separate buffer.
-This makes it easy to figure out which prefix to pass to yank."
- (interactive)
- ;; kill existing one, since erasing it doesn't work
- (let ((buf (get-buffer "*Kill Ring*")))
- (and buf (kill-buffer buf)))
-
- (let* ((buf (get-buffer-create "*Kill Ring*"))
- (temp kill-ring)
- (count 1)
- (bar (make-string 32 ?=))
- (bar2 (concat " " bar))
- (item " Item ")
- (yptr nil) (ynum 1))
- (set-buffer buf)
- (erase-buffer)
-
- (show-kill-insert-header)
-
- ;; show each of the items in the kill ring, in order
- (while temp
- ;; insert our little divider
- (insert (concat "\n" bar item (prin1-to-string count) " "
- (if (< count 10) bar2 bar) "\n"))
-
- ;; if this is the yank pointer target, grab it
- (when (equal temp kill-ring-yank-pointer)
- (setq yptr (car temp) ynum count))
-
- ;; insert the item and loop
- (show-kill-insert-item (car temp))
- (cl-incf count)
- (setq temp (cdr temp)))
-
- ;; show info about yank item
- (show-kill-insert-footer yptr ynum)
-
- ;; use define-key instead of local-set-key
- (use-local-map (make-sparse-keymap))
- (define-key (current-local-map) "q" #'show-kill-ring-exit)
-
- ;; show it
- (goto-char (point-min))
- (setq buffer-read-only t)
- (set-buffer-modified-p nil)
- ;; display-buffer rather than pop-to-buffer
- ;; easier for user to C-u (item#) C-y
- ;; while the point is where they want to yank
- (display-buffer buf)))
-
-(defun show-kill-insert-item (item)
- "Insert an ITEM from the kill ring into the current buffer.
-If it's too long, truncate it first."
- (let ((max show-kill-max-item-size))
- (cond
- ((or (not (numberp max))
- (< max 0)
- (< (length item) max))
- (insert item))
- (t
- ;; put ellipsis on its own line if item is longer than 1 line
- (let ((preview (substring item 0 max)))
- (if (< (length item) (- (frame-width) 5))
- (insert (concat preview "..."))
- (insert (concat preview "\n..."))))))))
-
-(defun show-kill-insert-header ()
- "Insert the show-kill-ring header or a notice if the kill ring is empty."
- (if kill-ring
- (insert "Contents of the kill ring:\n")
- (insert "The kill ring is empty")))
-
-(defun show-kill-insert-footer (yptr ynum)
- "Insert final divider and the yank-pointer (YPTR YNUM) info."
- (when kill-ring
- (save-excursion
- (re-search-backward "^\\(=+ Item [0-9]+ =+\\)$"))
- (insert "\n")
- (insert (make-string (length (match-string 1)) ?=))
- ;; Use number-to-string instead of int-to-string
- (insert (concat "\n\nItem " (number-to-string ynum)
- " is the next to be yanked:\n\n"))
- (show-kill-insert-item yptr)
- (insert "\n\nThe prefix arg will yank relative to this item.")))
-
-(defun empty-kill-ring ()
- "Force garbage collection of huge kill ring entries that I don't care about."
- (interactive)
- (setq kill-ring nil)
- (garbage-collect))
-
-(keymap-global-set "M-S-k" #'show-kill-ring) ;; was M-K, overrides kill-sentence
-
-(provide 'show-kill-ring)
-;;; show-kill-ring.el ends here
diff --git a/modules/takuzu-config.el b/modules/takuzu-config.el
new file mode 100644
index 00000000..470aaec8
--- /dev/null
+++ b/modules/takuzu-config.el
@@ -0,0 +1,17 @@
+;;; takuzu-config.el --- Takuzu (Binairo) game configuration -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; Wire the standalone takuzu game package (developed at ~/code/takuzu).
+;; Play with M-x takuzu. Switch to :vc once it is published on GitHub.
+
+;;; Code:
+
+(use-package takuzu
+ :load-path "~/code/takuzu"
+ :commands (takuzu)
+ :custom
+ (takuzu-default-size 6)
+ (takuzu-default-difficulty 'easy))
+
+(provide 'takuzu-config)
+;;; takuzu-config.el ends here
diff --git a/modules/text-config.el b/modules/text-config.el
index dd7bd3ca..a65002a8 100644
--- a/modules/text-config.el
+++ b/modules/text-config.el
@@ -69,7 +69,7 @@
;; edit selection in new buffer, C-c to finish; replaces with modifications
(use-package edit-indirect
- :bind ("M-S-i" . edit-indirect-region)) ;; was M-I
+ :bind ("M-I" . edit-indirect-region))
;; ------------------------------ Prettify Symbols -----------------------------
;; replacing the word l-a-m-b-d-a with a symbol, just because
@@ -118,8 +118,8 @@ everything else, such as `lambda', use the standard boundary check."
;; an easy way to enter diacritical marks
(use-package accent
- :commands accent-company
- :bind ("C-`" . accent-company))
+ :commands accent-menu
+ :bind ("C-`" . accent-menu))
(provide 'text-config)
;;; text-config.el ends here
diff --git a/modules/ui-config.el b/modules/ui-config.el
index fbc3d91c..9ac6faec 100644
--- a/modules/ui-config.el
+++ b/modules/ui-config.el
@@ -29,9 +29,6 @@
;; (i.e., read-only, overwrite, normal)
;; – Option to customize cursor shape with 'cj/set-cursor-type'
-;; • Icons
-;; – Load and enable 'nerd-icons' for UI glyphs
-
;; Customize the transparency and cursor color options at the top of this file.
;;; Code:
@@ -116,11 +113,5 @@ When `cj/enable-transparency' is nil, reset alpha to fully opaque."
;; burst when entering read-only buffers like EPUBs or vterm).
(blink-cursor-mode -1)
-;; --------------------------------- Nerd Icons --------------------------------
-;; use icons from nerd fonts in the Emacs UI
-
-(use-package nerd-icons
- :defer t)
-
(provide 'ui-config)
;;; ui-config.el ends here
diff --git a/modules/ui-theme.el b/modules/ui-theme.el
index 499e71a4..b3cdc634 100644
--- a/modules/ui-theme.el
+++ b/modules/ui-theme.el
@@ -88,8 +88,14 @@ If FILENAME isn't readable, return nil."
(string-trim (buffer-string)))))
(defun cj/theme-write-file-contents (content filename)
- "Write CONTENT to FILENAME.
-If FILENAME isn't writeable, return nil. If successful, return t."
+ "Write CONTENT to FILENAME, creating its parent directory if absent.
+On a fresh machine the `persist/' directory doesn't exist yet, and
+`file-writable-p' returns nil for a file inside a missing directory, so the
+write would silently fail. If FILENAME still isn't writeable, return nil.
+If successful, return t."
+ (let ((dir (file-name-directory filename)))
+ (when (and dir (not (file-directory-p dir)))
+ (ignore-errors (make-directory dir t))))
(when (file-writable-p filename)
(condition-case err
(progn
diff --git a/modules/undead-buffers.el b/modules/undead-buffers.el
index cbd2c0d7..21a04de9 100644
--- a/modules/undead-buffers.el
+++ b/modules/undead-buffers.el
@@ -65,7 +65,10 @@ regexp in `cj/undead-buffer-regexps'."
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'."
+ "Kill BUFFER, or bury it when it is in `cj/undead-buffer-list'.
+With a prefix argument (e.g. \\`C-u'), instead add BUFFER's name to
+`cj/undead-buffer-list' and report it, so the buffer is buried rather than
+killed on later kill attempts."
(interactive "bBuffer to kill or bury: ")
(with-current-buffer buffer
(if current-prefix-arg
@@ -97,12 +100,15 @@ Undead-buffers are buffers in `cj/undead-buffer-list'."
;; Keybinding moved to custom-buffer-file.el (C-; b k)
(defun cj/kill-other-window ()
- "Delete the next window and kill or bury its buffer."
+ "Delete the next window and kill or bury its buffer.
+Signal a `user-error' in a single-window frame, where there is no other
+window and acting would kill the buffer being viewed."
(interactive)
+ (when (one-window-p)
+ (user-error "No other window"))
(other-window 1)
(let ((buf (current-buffer)))
- (unless (one-window-p)
- (delete-window))
+ (delete-window)
(cj/kill-buffer-or-bury-alive buf)))
(keymap-global-set "M-S-o" #'cj/kill-other-window)