summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/ai-config.el17
-rw-r--r--modules/browser-config.el81
-rw-r--r--modules/config-utilities.el25
-rw-r--r--modules/custom-buffer-file.el (renamed from modules/custom-file-buffer.el)21
-rw-r--r--modules/custom-case.el6
-rw-r--r--modules/custom-comments.el13
-rw-r--r--modules/custom-datetime.el9
-rw-r--r--modules/custom-line-paragraph.el11
-rw-r--r--modules/custom-misc.el10
-rw-r--r--modules/custom-ordering.el11
-rw-r--r--modules/custom-text-enclose.el16
-rw-r--r--modules/custom-whitespace.el10
-rw-r--r--modules/diff-config.el8
-rw-r--r--modules/erc-config.el9
-rw-r--r--modules/external-open.el5
-rw-r--r--modules/flycheck-config.el3
-rw-r--r--modules/flyspell-and-abbrev.el6
-rw-r--r--modules/font-config.el7
-rw-r--r--modules/jumper.el252
-rw-r--r--modules/keybindings.el18
-rw-r--r--modules/lorem-optimum.el (renamed from modules/lorem-generator.el)93
-rw-r--r--modules/mail-config.el13
-rw-r--r--modules/mousetrap-mode.el3
-rw-r--r--modules/music-config.el11
-rw-r--r--modules/org-agenda-config.el50
-rw-r--r--modules/org-config.el17
-rw-r--r--modules/org-contacts-config.el9
-rw-r--r--modules/org-drill-config.el8
-rw-r--r--modules/org-gcal-config.el51
-rw-r--r--modules/org-roam-config.el19
-rw-r--r--modules/popper-config.el1
-rw-r--r--modules/prog-general.el17
-rw-r--r--modules/prog-lisp.el5
-rw-r--r--modules/selection-framework.el27
-rw-r--r--modules/system-commands.el138
-rw-r--r--modules/system-utils.el3
-rw-r--r--modules/test-runner.el312
-rw-r--r--modules/text-config.el3
-rw-r--r--modules/vc-config.el11
-rw-r--r--modules/video-audio-recording.el9
-rw-r--r--modules/weather-config.el3
-rw-r--r--modules/wip.el152
42 files changed, 1051 insertions, 442 deletions
diff --git a/modules/ai-config.el b/modules/ai-config.el
index 004750b6..3b89faca 100644
--- a/modules/ai-config.el
+++ b/modules/ai-config.el
@@ -415,5 +415,22 @@ Works for any buffer, whether it's visiting a file or not."
"x" #'cj/gptel-clear-buffer) ;; clears the assistant buffer
(keymap-set cj/custom-keymap "a" cj/ai-keymap)
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-; a" "AI assistant menu"
+ "C-; a B" "switch backend"
+ "C-; a M" "gptel menu"
+ "C-; a d" "delete conversation"
+ "C-; a ." "add buffer"
+ "C-; a f" "add file"
+ "C-; a l" "load conversation"
+ "C-; a m" "change model"
+ "C-; a p" "change prompt"
+ "C-; a &" "rewrite region"
+ "C-; a r" "clear context"
+ "C-; a s" "save conversation"
+ "C-; a t" "toggle window"
+ "C-; a x" "clear buffer"))
+
(provide 'ai-config)
;;; ai-config.el ends here.
diff --git a/modules/browser-config.el b/modules/browser-config.el
index fddc02e6..52c3b8a6 100644
--- a/modules/browser-config.el
+++ b/modules/browser-config.el
@@ -80,19 +80,44 @@ Returns the browser plist if found, nil otherwise."
cj/saved-browser-choice))
(error nil))))
-(defun cj/apply-browser-choice (browser-plist)
- "Apply the browser settings from BROWSER-PLIST."
- (when browser-plist
+(defun cj/--do-apply-browser-choice (browser-plist)
+ "Apply the browser settings from BROWSER-PLIST.
+Returns: \\='success if applied successfully,
+ \\='invalid-plist if browser-plist is nil or missing required keys."
+ (if (null browser-plist)
+ 'invalid-plist
(let ((browse-fn (plist-get browser-plist :function))
(executable (plist-get browser-plist :executable))
(path (plist-get browser-plist :path))
(program-var (plist-get browser-plist :program-var)))
- ;; 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
- (set program-var (or path executable)))
- (message "Default browser set to: %s" (plist-get browser-plist :name)))))
+ (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
+ (set program-var (or path executable)))
+ 'success))))
+
+(defun cj/apply-browser-choice (browser-plist)
+ "Apply the browser settings from BROWSER-PLIST."
+ (pcase (cj/--do-apply-browser-choice browser-plist)
+ ('success (message "Default browser set to: %s" (plist-get browser-plist :name)))
+ ('invalid-plist (message "Invalid browser configuration"))))
+
+(defun cj/--do-choose-browser (browser-plist)
+ "Save and apply BROWSER-PLIST as the default browser.
+Returns: \\='success if browser was saved and applied,
+ \\='save-failed if save operation failed,
+ \\='invalid-plist if browser-plist is invalid."
+ (condition-case _err
+ (progn
+ (cj/save-browser-choice browser-plist)
+ (let ((result (cj/--do-apply-browser-choice browser-plist)))
+ (if (eq result 'success)
+ 'success
+ 'invalid-plist)))
+ (error 'save-failed)))
(defun cj/choose-browser ()
"Interactively choose a browser from available options.
@@ -107,21 +132,39 @@ Persists the choice for future sessions."
(string= (plist-get b :name) choice))
browsers)))
(when selected
- (cj/save-browser-choice selected)
- (cj/apply-browser-choice selected))))))
+ (pcase (cj/--do-choose-browser selected)
+ ('success (message "Default browser set to: %s" (plist-get selected :name)))
+ ('save-failed (message "Failed to save browser choice"))
+ ('invalid-plist (message "Invalid browser configuration"))))))))
;; Initialize: Load saved choice or use first available browser
-(defun cj/initialize-browser ()
- "Initialize browser configuration on startup."
+(defun cj/--do-initialize-browser ()
+ "Initialize browser configuration.
+Returns: (cons \\='loaded browser-plist) if saved choice was loaded,
+ (cons \\='first-available browser-plist) if using first discovered browser,
+ (cons \\='no-browsers nil) if no browsers found."
(let ((saved-choice (cj/load-browser-choice)))
(if saved-choice
- (cj/apply-browser-choice saved-choice)
- ;; No saved choice - try to set first available browser silently
+ (cons 'loaded saved-choice)
+ ;; No saved choice - try to set first available browser
(let ((browsers (cj/discover-browsers)))
- (when browsers
- (cj/apply-browser-choice (car browsers))
- (message "No browser configured. Using %s. Run M-x cj/choose-browser to change."
- (plist-get (car browsers) :name)))))))
+ (if browsers
+ (cons 'first-available (car browsers))
+ (cons 'no-browsers nil))))))
+
+(defun cj/initialize-browser ()
+ "Initialize browser configuration on startup."
+ (let ((result (cj/--do-initialize-browser)))
+ (pcase (car result)
+ ('loaded
+ (cj/--do-apply-browser-choice (cdr result)))
+ ('first-available
+ (let ((browser (cdr result)))
+ (cj/--do-apply-browser-choice browser)
+ (message "No browser configured. Using %s. Run M-x cj/choose-browser to change."
+ (plist-get browser :name))))
+ ('no-browsers
+ (message "No supported browsers found")))))
;; Run initialization
(cj/initialize-browser)
diff --git a/modules/config-utilities.el b/modules/config-utilities.el
index ea92f19a..32018371 100644
--- a/modules/config-utilities.el
+++ b/modules/config-utilities.el
@@ -17,12 +17,27 @@
(keymap-global-set "C-c d" cj/debug-config-keymap)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-c d" "config debugging utils"))
+ (which-key-add-key-based-replacements
+ "C-c d" "config debugging utils"
+ "C-c d p" "profiler menu"
+ "C-c d p s" "start profiler"
+ "C-c d p h" "stop profiler"
+ "C-c d p r" "profiler report"
+ "C-c d t" "toggle debug-on-error"
+ "C-c d b" "benchmark method"
+ "C-c d c" "compilation menu"
+ "C-c d c h" "compile home"
+ "C-c d c d" "delete compiled"
+ "C-c d c ." "compile buffer"
+ "C-c d i" "info menu"
+ "C-c d i b" "info build"
+ "C-c d i p" "info packages"
+ "C-c d i f" "info features"
+ "C-c d r" "reload init"
+ "C-c d a" "reset auth cache"))
;;; --------------------------------- Profiling ---------------------------------
-(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-c d p" "profiler menu."))
(keymap-set cj/debug-config-keymap "p s" #'profiler-start)
(keymap-set cj/debug-config-keymap "p h" #'profiler-stop)
(keymap-set cj/debug-config-keymap "p r" #'profiler-report)
@@ -92,8 +107,6 @@ Recompile natively when supported, otherwise fall back to byte compilation."
(message "Cancelled recompilation of %s" user-emacs-directory))))
(keymap-set cj/debug-config-keymap "c h" 'cj/recompile-emacs-home)
-(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-c d c" "config compilation options."))
(defun cj/delete-emacs-home-compiled-files ()
"Delete all compiled files recursively in \='user-emacs-directory\='."
@@ -214,8 +227,6 @@ Recompile natively when supported, otherwise fall back to byte compilation."
(pop-to-buffer buf)))
(keymap-set cj/debug-config-keymap "i b" 'cj/info-emacs-build)
-(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-c d i" "info on build/features/packages."))
(defvar cj--loaded-file-paths nil
"All file paths that are loaded.")
diff --git a/modules/custom-file-buffer.el b/modules/custom-buffer-file.el
index e0224a32..9438e8ed 100644
--- a/modules/custom-file-buffer.el
+++ b/modules/custom-buffer-file.el
@@ -1,4 +1,4 @@
-;;; custom-file-buffer.el --- Custom Buffer and File Operations -*- coding: utf-8; lexical-binding: t; -*-
+;;; custom-buffer-file.el --- Custom Buffer and File Operations -*- coding: utf-8; lexical-binding: t; -*-
;;
;;; Commentary:
;; This module provides custom buffer and file operations including PostScript
@@ -240,8 +240,21 @@ Do not save the deleted text in the kill ring."
(keymap-set cj/custom-keymap "b" cj/buffer-and-file-map)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; b" "buffer and file menu"))
+ (which-key-add-key-based-replacements
+ "C-; b" "buffer and file menu"
+ "C-; b m" "move file"
+ "C-; b r" "rename file"
+ "C-; b p" "print to PS"
+ "C-; b d" "delete file"
+ "C-; b c" "copy buffer"
+ "C-; b n" "copy buffer name"
+ "C-; b t" "clear to top"
+ "C-; b b" "clear to bottom"
+ "C-; b x" "erase buffer"
+ "C-; b s" "save as"
+ "C-; b l" "copy file link"
+ "C-; b P" "copy file path"))
-(provide 'custom-file-buffer)
-;;; custom-file-buffer.el ends here.
+(provide 'custom-buffer-file)
+;;; custom-buffer-file.el ends here.
diff --git a/modules/custom-case.el b/modules/custom-case.el
index 4fd9ac05..59250ddb 100644
--- a/modules/custom-case.el
+++ b/modules/custom-case.el
@@ -118,7 +118,11 @@ short prepositions, and all articles are considered minor words."
(keymap-set cj/custom-keymap "c" cj/case-map)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; c" "case change menu"))
+ (which-key-add-key-based-replacements
+ "C-; c" "case change menu"
+ "C-; c t" "title case"
+ "C-; c u" "upcase"
+ "C-; c l" "downcase"))
(provide 'custom-case)
;;; custom-case.el ends here.
diff --git a/modules/custom-comments.el b/modules/custom-comments.el
index b4e51b2c..0d83d31b 100644
--- a/modules/custom-comments.el
+++ b/modules/custom-comments.el
@@ -619,7 +619,18 @@ Leverages cj/comment-inline-border."
(keymap-set cj/custom-keymap "C" cj/comment-map)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; C" "code comment menu"))
+ (which-key-add-key-based-replacements
+ "C-; C" "code comment menu"
+ "C-; C r" "reformat comment"
+ "C-; C d" "delete comments"
+ "C-; C c" "inline border"
+ "C-; C -" "hyphen divider"
+ "C-; C s" "simple divider"
+ "C-; C p" "padded divider"
+ "C-; C b" "box"
+ "C-; C h" "heavy box"
+ "C-; C u" "unicode box"
+ "C-; C n" "block banner"))
(provide 'custom-comments)
;;; custom-comments.el ends here.
diff --git a/modules/custom-datetime.el b/modules/custom-datetime.el
index c195ebc2..5b06d81a 100644
--- a/modules/custom-datetime.el
+++ b/modules/custom-datetime.el
@@ -117,7 +117,14 @@ Use `readable-date-format' for formatting."
(keymap-set cj/custom-keymap "d" cj/datetime-map)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; d" "date/time insertion menu"))
+ (which-key-add-key-based-replacements
+ "C-; d" "date/time insertion menu"
+ "C-; d r" "readable date-time"
+ "C-; d s" "sortable date-time"
+ "C-; d t" "sortable time"
+ "C-; d T" "readable time"
+ "C-; d d" "sortable date"
+ "C-; d D" "readable date"))
(provide 'custom-datetime)
;;; custom-datetime.el ends here.
diff --git a/modules/custom-line-paragraph.el b/modules/custom-line-paragraph.el
index 7f0baef9..32f9aaa1 100644
--- a/modules/custom-line-paragraph.el
+++ b/modules/custom-line-paragraph.el
@@ -139,8 +139,15 @@ If the line is empty or contains only whitespace, abort with a message."
(keymap-set cj/custom-keymap "l" cj/line-and-paragraph-map)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; l" "line and paragraph menu")
- (which-key-add-key-based-replacements "C-; l c" "duplicate and comment"))
+ (which-key-add-key-based-replacements
+ "C-; l" "line and paragraph menu"
+ "C-; l j" "join lines"
+ "C-; l J" "join paragraph"
+ "C-; l d" "duplicate"
+ "C-; l c" "duplicate and comment"
+ "C-; l R" "remove duplicates"
+ "C-; l r" "remove matching"
+ "C-; l u" "underscore line"))
(provide 'custom-line-paragraph)
;;; custom-line-paragraph.el ends here.
diff --git a/modules/custom-misc.el b/modules/custom-misc.el
index 2af9c244..be1f26bb 100644
--- a/modules/custom-misc.el
+++ b/modules/custom-misc.el
@@ -152,5 +152,15 @@ to nil."
(keymap-set cj/custom-keymap "SPC" #'cj/switch-to-previous-buffer)
(keymap-set cj/custom-keymap "|" #'display-fill-column-indicator-mode)
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-; )" "jump to paren"
+ "C-; f" "format buffer"
+ "C-; W" "count words"
+ "C-; /" "fraction glyphs"
+ "C-; A" "align regexp"
+ "C-; SPC" "prev buffer"
+ "C-; |" "fill column"))
+
(provide 'custom-misc)
;;; custom-misc.el ends here
diff --git a/modules/custom-ordering.el b/modules/custom-ordering.el
index abc9995a..7d906e75 100644
--- a/modules/custom-ordering.el
+++ b/modules/custom-ordering.el
@@ -253,7 +253,16 @@ Returns the transformed string without modifying the buffer."
(keymap-set cj/custom-keymap "o" cj/ordering-map)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; o" "ordering/sorting menu"))
+ (which-key-add-key-based-replacements
+ "C-; o" "ordering/sorting menu"
+ "C-; o l" "listify"
+ "C-; o j" "JSON array"
+ "C-; o p" "Python list"
+ "C-; o q" "toggle quotes"
+ "C-; o r" "reverse lines"
+ "C-; o n" "number lines"
+ "C-; o A" "alphabetize"
+ "C-; o L" "comma to lines"))
(provide 'custom-ordering)
;;; custom-ordering.el ends here.
diff --git a/modules/custom-text-enclose.el b/modules/custom-text-enclose.el
index ccacdd2d..e93e6dea 100644
--- a/modules/custom-text-enclose.el
+++ b/modules/custom-text-enclose.el
@@ -264,11 +264,23 @@ Works on region if active, otherwise entire buffer."
"a" #'cj/append-to-lines-in-region-or-buffer
"p" #'cj/prepend-to-lines-in-region-or-buffer
"i" #'cj/indent-lines-in-region-or-buffer
- "d" #'cj/dedent-lines-in-region-or-buffer)
+ "d" #'cj/dedent-lines-in-region-or-buffer
+ "I" #'change-inner
+ "O" #'change-outer)
(keymap-set cj/custom-keymap "s" cj/enclose-map)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; s" "text enclose menu"))
+ (which-key-add-key-based-replacements
+ "C-; s" "text enclose menu"
+ "C-; s s" "surround text"
+ "C-; s w" "wrap text"
+ "C-; s u" "unwrap text"
+ "C-; s a" "append to lines"
+ "C-; s p" "prepend to lines"
+ "C-; s i" "indent lines"
+ "C-; s d" "dedent lines"
+ "C-; s I" "change inner"
+ "C-; s O" "change outer"))
(provide 'custom-text-enclose)
;;; custom-text-enclose.el ends here.
diff --git a/modules/custom-whitespace.el b/modules/custom-whitespace.el
index df93459a..d5f8d80c 100644
--- a/modules/custom-whitespace.el
+++ b/modules/custom-whitespace.el
@@ -217,7 +217,15 @@ Operate on the active region designated by START and END."
(keymap-set cj/custom-keymap "w" cj/whitespace-map)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; w" "whitespace menu"))
+ (which-key-add-key-based-replacements
+ "C-; w" "whitespace menu"
+ "C-; w c" "collapse whitespace"
+ "C-; w l" "delete blank lines"
+ "C-; w 1" "single blank line"
+ "C-; w d" "delete all whitespace"
+ "C-; w -" "hyphenate whitespace"
+ "C-; w t" "untabify"
+ "C-; w T" "tabify"))
(provide 'custom-whitespace)
;;; custom-whitespace.el ends here.
diff --git a/modules/diff-config.el b/modules/diff-config.el
index 382b2250..45c2a778 100644
--- a/modules/diff-config.el
+++ b/modules/diff-config.el
@@ -48,6 +48,14 @@
(add-hook 'ediff-mode-hook #'cj/ediff-hook)
(add-hook 'ediff-after-quit-hook-internal #'winner-undo))
+;; which-key labels
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-c D" "ediff menu"
+ "C-c D f" "ediff files"
+ "C-c D b" "ediff buffers"
+ "C-c D r" "ediff revision"
+ "C-c D D" "ediff directories"))
(provide 'diff-config)
;;; diff-config.el ends here
diff --git a/modules/erc-config.el b/modules/erc-config.el
index 1c189fa3..e7efb33f 100644
--- a/modules/erc-config.el
+++ b/modules/erc-config.el
@@ -183,7 +183,14 @@ Auto-adds # prefix if missing. Offers completion from configured channels."
(keymap-set cj/custom-keymap "E" cj/erc-keymap)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; E" "ERC chat menu"))
+ (which-key-add-key-based-replacements
+ "C-; E" "ERC chat menu"
+ "C-; E C" "connect server"
+ "C-; E c" "join channel"
+ "C-; E b" "switch buffer"
+ "C-; E l" "list servers"
+ "C-; E q" "quit channel"
+ "C-; E Q" "quit server"))
;; Main ERC configuration
(use-package erc
diff --git a/modules/external-open.el b/modules/external-open.el
index 41d842fb..8c4db810 100644
--- a/modules/external-open.el
+++ b/modules/external-open.el
@@ -111,6 +111,11 @@
(keymap-global-set "C-c x o" #'cj/open-this-file-with)
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-c x" "external open menu"
+ "C-c x o" "open file with"))
+
;; -------------------- Open Files With Default File Handler -------------------
(defun cj/find-file-auto (orig-fun &rest args)
diff --git a/modules/flycheck-config.el b/modules/flycheck-config.el
index d7f1ad39..ea19f08f 100644
--- a/modules/flycheck-config.el
+++ b/modules/flycheck-config.el
@@ -94,5 +94,8 @@ Runs flycheck-prose-on-demand if in an org-buffer."
;; trigger immediate check
(flycheck-buffer)))
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements "C-; ?" "list errors"))
+
(provide 'flycheck-config)
;;; flycheck-config.el ends here
diff --git a/modules/flyspell-and-abbrev.el b/modules/flyspell-and-abbrev.el
index 379fc7b2..d12a1794 100644
--- a/modules/flyspell-and-abbrev.el
+++ b/modules/flyspell-and-abbrev.el
@@ -239,5 +239,11 @@ Press C-' repeatedly to step through misspellings one at a time."
;;;###autoload (keymap-set global-map "C-c f" #'cj/flyspell-toggle)
;;;###autoload (keymap-set global-map "C-'" #'cj/flyspell-then-abbrev)
+;; which-key labels
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-c f" "flyspell toggle"
+ "C-'" "flyspell then abbrev"))
+
(provide 'flyspell-and-abbrev)
;;; flyspell-and-abbrev.el ends here.
diff --git a/modules/font-config.el b/modules/font-config.el
index eea09da6..ddd4497f 100644
--- a/modules/font-config.el
+++ b/modules/font-config.el
@@ -284,5 +284,12 @@ If FRAME is nil, uses the selected frame."
"<~" "<~~" "</" "</>" "~@" "~-" "~>" "~~" "~~>" "%%"))
(global-ligature-mode t))
+;; which-key labels
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-c E" "emojify menu"
+ "C-c E i" "insert emoji"
+ "C-c E l" "list emojis"))
+
(provide 'font-config)
;;; font-config.el ends here
diff --git a/modules/jumper.el b/modules/jumper.el
index 7a3991d0..67d930aa 100644
--- a/modules/jumper.el
+++ b/modules/jumper.el
@@ -10,24 +10,76 @@
;; Jumper provides a simple way to store and jump between locations
;; in your codebase without needing to remember register assignments.
+;;
+;; 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.
;;; Code:
-(defgroup jumper nil
- "Quick navigation between stored locations."
- :group 'convenience)
+(require 'cl-lib)
-(defcustom jumper-prefix-key "M-SPC"
+(defvar jumper-prefix-key "M-SPC"
"Prefix key for jumper commands.
+Note that using M-SPC will override the default binding to just-one-space.")
-Note that using M-SPC will override the default binding to just-one-space."
- :type 'string
- :group 'jumper)
-
-(defcustom jumper-max-locations 10
- "Maximum number of locations to store."
- :type 'integer
- :group 'jumper)
+(defvar jumper-max-locations 10
+ "Maximum number of locations to store.")
;; Internal variables
(defvar jumper--registers (make-vector jumper-max-locations nil)
@@ -50,12 +102,10 @@ Note that using M-SPC will override the default binding to just-one-space."
"Check if current location is already stored."
(let ((key (jumper--location-key))
(found nil))
- (dotimes (i
- jumper--next-index found)
+ (dotimes (i jumper--next-index found)
(let* ((reg (aref jumper--registers i))
- (pos (get-register reg))
- (marker (and pos (registerv-data pos))))
- (when marker
+ (marker (get-register reg)))
+ (when (and marker (markerp marker))
(save-current-buffer
(set-buffer (marker-buffer marker))
(save-excursion
@@ -70,9 +120,8 @@ Note that using M-SPC will override the default binding to just-one-space."
(defun jumper--format-location (index)
"Format location at INDEX for display."
(let* ((reg (aref jumper--registers index))
- (pos (get-register reg))
- (marker (and pos (registerv-data pos))))
- (when marker
+ (marker (get-register reg)))
+ (when (and marker (markerp marker))
(save-current-buffer
(set-buffer (marker-buffer marker))
(save-excursion
@@ -86,49 +135,83 @@ Note that using M-SPC will override the default binding to just-one-space."
(min (+ (line-beginning-position) 40)
(line-end-position)))))))))
+(defun jumper--do-store-location ()
+ "Store current location in the next free register.
+Returns: \\='already-exists if location is already stored,
+ \\='no-space if all registers are full,
+ register character if successfully stored."
+ (cond
+ ((jumper--location-exists-p) 'already-exists)
+ ((not (jumper--register-available-p)) 'no-space)
+ (t
+ (let ((reg (+ ?0 jumper--next-index)))
+ (point-to-register reg)
+ (aset jumper--registers jumper--next-index reg)
+ (setq jumper--next-index (1+ jumper--next-index))
+ reg))))
+
(defun jumper-store-location ()
"Store current location in the next free register."
(interactive)
- (if (jumper--location-exists-p)
- (message "Location already stored")
- (if (jumper--register-available-p)
- (let ((reg (+ ?0 jumper--next-index)))
- (point-to-register reg)
- (aset jumper--registers jumper--next-index reg)
- (setq jumper--next-index (1+ jumper--next-index))
- (message "Location stored in register %c" reg))
- (message "Sorry - all jump locations are filled!"))))
+ (pcase (jumper--do-store-location)
+ ('already-exists (message "Location already stored"))
+ ('no-space (message "Sorry - all jump locations are filled!"))
+ (reg (message "Location stored in register %c" reg))))
+
+(defun jumper--do-jump-to-location (target-idx)
+ "Jump to location at TARGET-IDX.
+TARGET-IDX: -1 for last location, 0-9 for stored locations, nil for toggle.
+Returns: \\='no-locations if no locations stored,
+ \\='already-there if at the only location (toggle case),
+ \\='jumped if successfully jumped."
+ (cond
+ ((= jumper--next-index 0) 'no-locations)
+ ;; Toggle behavior when target-idx is nil and only 1 location
+ ((and (null target-idx) (= jumper--next-index 1))
+ (if (jumper--location-exists-p)
+ 'already-there
+ (let ((reg (aref jumper--registers 0)))
+ (point-to-register jumper--last-location-register)
+ (jump-to-register reg)
+ 'jumped)))
+ ;; Jump to specific target
+ (t
+ (if (= target-idx -1)
+ ;; Jumping to last location - don't overwrite it
+ (jump-to-register jumper--last-location-register)
+ ;; Jumping to stored location - save current for "last"
+ (progn
+ (point-to-register jumper--last-location-register)
+ (jump-to-register (aref jumper--registers target-idx))))
+ 'jumped)))
(defun jumper-jump-to-location ()
"Jump to a stored location."
(interactive)
- (if (= jumper--next-index 0)
- (message "No locations stored")
- (if (= jumper--next-index 1)
- ;; Special case for one location - toggle behavior
- (let ((reg (aref jumper--registers 0)))
- (if (jumper--location-exists-p)
- (message "You're already at the stored location")
- (point-to-register jumper--last-location-register)
- (jump-to-register reg)
- (message "Jumped to location")))
- ;; Multiple locations - use completing-read
- (let* ((locations
- (cl-loop for i from 0 below jumper--next-index
- for fmt = (jumper--format-location i)
- when fmt collect (cons fmt i)))
- ;; Add last location if available
- (last-pos (get-register jumper--last-location-register))
- (locations (if last-pos
- (cons (cons "[z] Last location" -1) locations)
- locations))
- (choice (completing-read "Jump to: " locations nil t))
- (idx (cdr (assoc choice locations))))
- (point-to-register jumper--last-location-register)
- (if (= idx -1)
- (jump-to-register jumper--last-location-register)
- (jump-to-register (aref jumper--registers idx)))
- (message "Jumped to location")))))
+ (cond
+ ;; No locations
+ ((= jumper--next-index 0)
+ (message "No locations stored"))
+ ;; Single location - toggle
+ ((= jumper--next-index 1)
+ (pcase (jumper--do-jump-to-location nil)
+ ('already-there (message "You're already at the stored location"))
+ ('jumped (message "Jumped to location"))))
+ ;; Multiple locations - prompt user
+ (t
+ (let* ((locations
+ (cl-loop for i from 0 below jumper--next-index
+ for fmt = (jumper--format-location i)
+ when fmt collect (cons fmt i)))
+ ;; Add last location if available
+ (last-pos (get-register jumper--last-location-register))
+ (locations (if last-pos
+ (cons (cons "[z] Last location" -1) locations)
+ locations))
+ (choice (completing-read "Jump to: " locations nil t))
+ (idx (cdr (assoc choice locations))))
+ (jumper--do-jump-to-location idx)
+ (message "Jumped to location")))))
(defun jumper--reorder-registers (removed-idx)
"Reorder registers after removing the one at REMOVED-IDX."
@@ -139,30 +222,39 @@ Note that using M-SPC will override the default binding to just-one-space."
(aset jumper--registers i next-reg))))
(setq jumper--next-index (1- jumper--next-index)))
+(defun jumper--do-remove-location (index)
+ "Remove location at INDEX.
+Returns: \\='no-locations if no locations stored,
+ \\='cancelled if index is -1,
+ t if successfully removed."
+ (cond
+ ((= jumper--next-index 0) 'no-locations)
+ ((= index -1) 'cancelled)
+ (t
+ (jumper--reorder-registers index)
+ t)))
+
(defun jumper-remove-location ()
"Remove a stored location."
(interactive)
(if (= jumper--next-index 0)
- (message "No locations stored")
- (let* ((locations
- (cl-loop for i from 0 below jumper--next-index
- for fmt = (jumper--format-location i)
- when fmt collect (cons fmt i)))
- (locations (cons (cons "Cancel" -1) locations))
- (choice (completing-read "Remove location: " locations nil t))
- (idx (cdr (assoc choice locations))))
- (if (= idx -1)
- (message "Operation cancelled")
- (jumper--reorder-registers idx)
- (message "Location removed")))))
-
-(defvar jumper-map
- (let ((map (make-sparse-keymap)))
- (define-key map (kbd "SPC") #'jumper-store-location)
- (define-key map (kbd "j") #'jumper-jump-to-location)
- (define-key map (kbd "d") #'jumper-remove-location)
- map)
- "Keymap for jumper commands.")
+ (message "No locations stored")
+ (let* ((locations
+ (cl-loop for i from 0 below jumper--next-index
+ for fmt = (jumper--format-location i)
+ when fmt collect (cons fmt i)))
+ (locations (cons (cons "Cancel" -1) locations))
+ (choice (completing-read "Remove location: " locations nil t))
+ (idx (cdr (assoc choice locations))))
+ (pcase (jumper--do-remove-location idx)
+ ('cancelled (message "Operation cancelled"))
+ ('t (message "Location removed"))))))
+
+(defvar-keymap jumper-map
+ :doc "Keymap for jumper commands"
+ "SPC" #'jumper-store-location
+ "j" #'jumper-jump-to-location
+ "d" #'jumper-remove-location)
(defun jumper-setup-keys ()
"Setup default keybindings for jumper."
@@ -172,5 +264,13 @@ Note that using M-SPC will override the default binding to just-one-space."
;; Call jumper-setup-keys when the package is loaded
(jumper-setup-keys)
+;; which-key integration
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "M-SPC" "jumper menu"
+ "M-SPC SPC" "store location"
+ "M-SPC j" "jump to location"
+ "M-SPC d" "remove location"))
+
(provide 'jumper)
;;; jumper.el ends here.
diff --git a/modules/keybindings.el b/modules/keybindings.el
index 1f8867ef..1eff621c 100644
--- a/modules/keybindings.el
+++ b/modules/keybindings.el
@@ -68,12 +68,22 @@ Errors if VAR is unbound, not a non-empty string, or the file does not exist."
;; Bind it under the prefix map.
(keymap-set cj/jump-map key fn))))
-;; Bind the prefix globally (user-reserved prefix).
-(keymap-global-set "C-c j" cj/jump-map)
+;; Bind the prefix to custom keymap
+(keymap-set cj/custom-keymap "j" cj/jump-map)
-;; nicer prefix label in which-key
+;; which-key labels
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-c j" "Jump to common files."))
+ (which-key-add-key-based-replacements
+ "C-; j" "jump to files menu"
+ "C-; j r" "jump to reference"
+ "C-; j s" "jump to schedule"
+ "C-; j i" "jump to inbox"
+ "C-; j c" "jump to contacts"
+ "C-; j m" "jump to macros"
+ "C-; j n" "jump to reading notes"
+ "C-; j w" "jump to webclipped"
+ "C-; j g" "jump to gcal"
+ "C-; j I" "jump to emacs init"))
;; ---------------------------- Keybinding Discovery ---------------------------
diff --git a/modules/lorem-generator.el b/modules/lorem-optimum.el
index 6f0520c6..6ccca55f 100644
--- a/modules/lorem-generator.el
+++ b/modules/lorem-optimum.el
@@ -1,4 +1,4 @@
-;;; lorem-generator.el --- Fake Latin Text Generator -*- coding: utf-8; lexical-binding: t; -*-
+;;; lorem-optimum.el --- Fake Latin Text Generator -*- coding: utf-8; lexical-binding: t; -*-
;;
;; Author: Craig Jennings
;; Version: 0.5
@@ -24,6 +24,19 @@
(require 'cl-lib)
+;;; Configuration
+
+(defvar cj/lipsum-training-file "assets/liber-primus.txt"
+ "Default training file name (relative to `user-emacs-directory`).")
+
+(defvar cj/lipsum-default-file
+ (expand-file-name cj/lipsum-training-file user-emacs-directory)
+ "Default training file for cj-lipsum.
+
+This should be a plain UTF-8 text file with hundreds of Latin words
+or sentences. By default it points to the file specified in
+`cj/lipsum-training-file` relative to `user-emacs-directory`.")
+
(cl-defstruct (cj/markov-chain
(:constructor cj/markov-chain-create))
"An order-two Markov chain."
@@ -31,25 +44,45 @@
(keys nil))
(defun cj/markov-tokenize (text)
- "Split TEXT into tokens: words and punctuation separately."
- (let ((case-fold-search nil))
- (split-string text "\\b" t "[[:space:]\n]+")))
-
+ "Split TEXT into tokens: words and punctuation separately.
+Returns a list of words and punctuation marks as separate tokens."
+ (let ((tokens '())
+ (pos 0)
+ (len (length text)))
+ (while (< pos len)
+ (cond
+ ;; Skip whitespace
+ ((string-match-p "[[:space:]]" (substring text pos (1+ pos)))
+ (setq pos (1+ pos)))
+ ;; Match word (sequence of alphanumeric characters)
+ ((string-match "\\`\\([[:alnum:]]+\\)" (substring text pos))
+ (let ((word (match-string 1 (substring text pos))))
+ (push word tokens)
+ (setq pos (+ pos (length word)))))
+ ;; Match punctuation (single character)
+ ((string-match "\\`\\([[:punct:]]\\)" (substring text pos))
+ (let ((punct (match-string 1 (substring text pos))))
+ (push punct tokens)
+ (setq pos (+ pos (length punct)))))
+ ;; Skip any other character
+ (t (setq pos (1+ pos)))))
+ (nreverse tokens)))
(defun cj/markov-learn (chain text)
"Add TEXT into the Markov CHAIN with tokenized input."
- (let* ((words (cj/markov-tokenize text))
+ (let* ((word-list (cj/markov-tokenize text))
+ ;; Convert to vector for O(1) access instead of O(n) with nth
+ (words (vconcat word-list))
(len (length words)))
(cl-loop for i from 0 to (- len 3)
- for a = (nth i words)
- for b = (nth (1+ i) words)
- for c = (nth (+ i 2) words)
+ for a = (aref words i)
+ for b = (aref words (1+ i))
+ for c = (aref words (+ i 2))
do (let* ((bigram (list a b))
(nexts (gethash bigram (cj/markov-chain-map chain))))
(puthash bigram (cons c nexts)
(cj/markov-chain-map chain)))))
- (setf (cj/markov-chain-keys chain)
- (cl-loop for k being the hash-keys of (cj/markov-chain-map chain)
- collect k)))
+ ;; Invalidate cached keys after learning new data
+ (setf (cj/markov-chain-keys chain) nil))
(defun cj/markov-fix-capitalization (sentence)
"Capitalize the first word and the first word after .!? in SENTENCE."
@@ -94,7 +127,7 @@
(defun cj/markov-generate (chain n &optional start)
"Generate a sentence of N tokens from CHAIN."
- (when (cj/markov-chain-keys chain)
+ (when (> (hash-table-count (cj/markov-chain-map chain)) 0)
(let* ((state (or (and start
(gethash start (cj/markov-chain-map chain))
start)
@@ -116,8 +149,16 @@
(cj/markov-join-tokens tokens))))
(defun cj/markov-random-key (chain)
- (nth (random (length (cj/markov-chain-keys chain)))
- (cj/markov-chain-keys chain)))
+ "Return a random bigram key from CHAIN.
+Builds and caches the keys list lazily if not already cached."
+ (unless (cj/markov-chain-keys chain)
+ ;; Lazily build keys list only when needed
+ (setf (cj/markov-chain-keys chain)
+ (cl-loop for k being the hash-keys of (cj/markov-chain-map chain)
+ collect k)))
+ (let ((keys (cj/markov-chain-keys chain)))
+ (when keys
+ (nth (random (length keys)) keys))))
(defun cj/markov-next-word (chain bigram)
(let ((candidates (gethash bigram (cj/markov-chain-map chain))))
@@ -182,6 +223,7 @@
(or (cj/markov-next-word cj/lipsum-chain state)
(cadr (cj/markov-random-key cj/lipsum-chain))))))
collect (replace-regexp-in-string "^[[:punct:]]+\\|[[:punct:]]+$" "" w))))
+ ;; Filter empty strings from generated words
(setq words (cl-remove-if #'string-empty-p words))
(mapconcat
(lambda (word idx)
@@ -204,23 +246,6 @@ Defaults: MIN=30, MAX=80."
(let ((len (+ min (random (1+ (- max min))))))
(insert (cj/lipsum len) "\n\n")))))
-;;; Customization
-
-(defgroup cj-lipsum nil
- "Pseudo-Latin lorem ipsum text generator."
- :prefix "cj/lipsum-"
- :group 'text)
-
-(defcustom cj/lipsum-default-file
- (expand-file-name "latin.txt"
- (file-name-directory (or load-file-name buffer-file-name)))
- "Default training file for cj-lipsum.
-
-This should be a plain UTF-8 text file with hundreds of Latin words
-or sentences. By default it points to the bundled `latin.txt`."
- :type 'file
- :group 'cj-lipsum)
-
;;; Initialization: train on default file
(defun cj/lipsum--init ()
"Initialize cj-lipsum by learning from `cj/lipsum-default-file`."
@@ -231,5 +256,5 @@ or sentences. By default it points to the bundled `latin.txt`."
(cj/lipsum--init)
-(provide 'lorem-generator)
-;;; lorem-generator.el ends here.
+(provide 'lorem-optimum)
+;;; lorem-optimum.el ends here.
diff --git a/modules/mail-config.el b/modules/mail-config.el
index c65e5342..1d5a14ea 100644
--- a/modules/mail-config.el
+++ b/modules/mail-config.el
@@ -283,9 +283,9 @@ Prompts user for the action when executing."
;; user composes org mode; recipient receives html
(use-package org-msg
- :ensure nil ;; loading locally for fixes
+ ;; :vc (:url "https://github.com/cjennings/org-msg" :rev :newest)
+ :load-path "/home/cjennings/code/org-msg"
:defer 1
- :load-path "~/code/org-msg/"
:after (org mu4e)
:preface
(defvar-keymap cj/email-map
@@ -294,7 +294,10 @@ Prompts user for the action when executing."
"d" #'org-msg-attach-delete)
(keymap-set cj/custom-keymap "e" cj/email-map)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; e" "email menu"))
+ (which-key-add-key-based-replacements
+ "C-; e" "email menu"
+ "C-; e a" "attach file"
+ "C-; e d" "delete attachment"))
:bind
;; more intuitive keybinding for attachments
(:map org-msg-edit-mode-map
@@ -342,5 +345,9 @@ Prompts user for the action when executing."
(advice-add #'mu4e-compose-wide-reply
:after (lambda (&rest _) (org-msg-edit-mode)))
+;; which-key labels
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements "C-c m" "mu4e email"))
+
(provide 'mail-config)
;;; mail-config.el ends here
diff --git a/modules/mousetrap-mode.el b/modules/mousetrap-mode.el
index fa9ee6dd..76c08c79 100644
--- a/modules/mousetrap-mode.el
+++ b/modules/mousetrap-mode.el
@@ -62,5 +62,8 @@ with or without C-, M-, S- modifiers."
(keymap-global-set "C-c M" #'mouse-trap-mode)
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements "C-c M" "mouse trap mode"))
+
(provide 'mousetrap-mode)
;;; mousetrap-mode.el ends here.
diff --git a/modules/music-config.el b/modules/music-config.el
index 90feb7eb..902fbd9c 100644
--- a/modules/music-config.el
+++ b/modules/music-config.el
@@ -366,7 +366,16 @@ Dirs added recursively."
(keymap-set cj/custom-keymap "m" cj/music-map)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; m" "music menu"))
+ (which-key-add-key-based-replacements
+ "C-; m" "music menu"
+ "C-; m m" "toggle playlist"
+ "C-; m M" "show playlist"
+ "C-; m a" "add music"
+ "C-; m r" "create radio"
+ "C-; m SPC" "pause"
+ "C-; m s" "stop"
+ "C-; m p" "playlist mode"
+ "C-; m x" "shuffle"))
(use-package emms
:defer t
diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el
index c86ac6a3..211ff4fe 100644
--- a/modules/org-agenda-config.el
+++ b/modules/org-agenda-config.el
@@ -54,7 +54,7 @@
(window-height . fit-window-to-buffer)))
;; reset s-left/right each time org-agenda is enabled
- (add-hook 'org-agenda-mode-hook (lambda ()
+s (add-hook 'org-agenda-mode-hook (lambda ()
(local-set-key (kbd "s-<right>") #'org-agenda-todo-nextset)
(local-set-key (kbd "s-<left>")
#'org-agenda-todo-previousset)))
@@ -68,7 +68,6 @@
(defun cj/add-files-to-org-agenda-files-list (directory)
"Search for files named \\='todo.org\\=' add them to org-project-files.
-
DIRECTORY is a string of the path to begin the search."
(interactive "D")
(setq org-agenda-files
@@ -81,10 +80,9 @@ DIRECTORY is a string of the path to begin the search."
;; agenda targets is the schedule, contacts, project todos,
;; inbox, and org roam projects.
(defun cj/build-org-agenda-list ()
- "Rebuilds the org agenda list without checking org-roam for projects.
-
+ "Rebuilds the org agenda list.
Begins with the inbox-file, schedule-file, and contacts-file.
-Then adds all todo.org files from projects-dir and code-dir.
+Then adds all todo.org files from projects-dir.
Reports elapsed time in the messages buffer."
(interactive)
(let ((start-time (current-time)))
@@ -105,7 +103,6 @@ Reports elapsed time in the messages buffer."
(defun cj/todo-list-all-agenda-files ()
"Displays an \\='org-agenda\\=' todo list.
-
The contents of the agenda will be built from org-project-files and org-roam
files that have project in their filetag."
(interactive)
@@ -118,7 +115,6 @@ files that have project in their filetag."
(defun cj/todo-list-from-this-buffer ()
"Displays an \\='org-agenda\\=' todo list built from the current buffer.
-
If the current buffer isn't an org buffer, inform the user."
(interactive)
(if (eq major-mode 'org-mode)
@@ -153,7 +149,6 @@ If the current buffer isn't an org buffer, inform the user."
(defun cj/org-agenda-skip-subtree-if-not-overdue ()
"Skip an agenda subtree if it is not an overdue deadline or scheduled task.
-
An entry is considered overdue if it has a scheduled or deadline date strictly
before today, is not marked as done, and is not a habit."
(let* ((subtree-end (save-excursion (org-end-of-subtree t)))
@@ -176,7 +171,6 @@ before today, is not marked as done, and is not a habit."
(defun cj/org-skip-subtree-if-priority (priority)
"Skip an agenda subtree if it has a priority of PRIORITY.
-
PRIORITY may be one of the characters ?A, ?B, or ?C."
(let ((subtree-end (save-excursion (org-end-of-subtree t)))
(pri-value (* 1000 (- org-lowest-priority priority)))
@@ -187,7 +181,6 @@ PRIORITY may be one of the characters ?A, ?B, or ?C."
(defun cj/org-skip-subtree-if-keyword (keywords)
"Skip an agenda subtree if it has a TODO keyword in KEYWORDS.
-
KEYWORDS must be a list of strings."
(let ((subtree-end (save-excursion (org-end-of-subtree t))))
(if (member (org-get-todo-state) keywords)
@@ -224,12 +217,10 @@ KEYWORDS must be a list of strings."
(defun cj/main-agenda-display ()
"Display the main daily org-agenda view.
-
This uses all org-agenda targets and presents three sections:
- All unfinished priority A tasks
- Today's schedule, including habits with consistency graphs
- All priority B and C unscheduled/undeadlined tasks
-
The agenda is rebuilt from all sources before display, including:
- inbox-file and schedule-file
- Org-roam nodes tagged as \"Project\"
@@ -263,9 +254,11 @@ This allows a line to show in an agenda without being scheduled or a deadline."
;; Install CHIME from GitHub using use-package :vc (Emacs 29+)
(use-package chime
- :vc (:url "https://github.com/cjennings/chime.el" :rev :newest)
- :after (alert org-agenda)
:demand t
+ ;; :vc (:url "https://github.com/cjennings/chime.el" :rev :newest) ;; using latest on github
+ :after (alert org-agenda)
+ :ensure nil ;; using local version
+ :load-path "~/code/chime.el"
:bind
("C-c A" . chime-check)
:config
@@ -273,13 +266,29 @@ This allows a line to show in an agenda without being scheduled or a deadline."
;; This gives two notifications per event without any after-event notifications
(setq chime-alert-time '(5 0))
- ;; Modeline display: show upcoming events within 60 minutes
- (setq chime-modeline-lookahead 120)
+ ;; Modeline display: show upcoming events within 2 hours
+ (setq chime-enable-modeline t)
+ (setq chime-modeline-lookahead 180)
(setq chime-modeline-format " ⏰ %s")
- ;; Chime sound: plays when notifications appear
- (setq chime-play-sound t)
- ;; Uses bundled chime.wav by default
+ ;; Tooltip settings: show up to 20 upcoming events (regardless of how far in future)
+ ;; chime-modeline-tooltip-lookahead defaults to 525600 (1 year) - effectively unlimited
+ (setq chime-modeline-tooltip-max-events 20)
+
+ ;; Modeline content: show title and countdown only (omit event time)
+ (setq chime-notification-text-format "%t (%u)")
+
+ ;; Time-until format: compact style like " in 10m" or " in 1h 37m"
+ (setq chime-time-left-format-short " in %mm") ; Under 1 hour: " in 10m"
+ (setq chime-time-left-format-long " in %hh %mm") ; 1 hour+: " in 1h 37m"
+ (setq chime-time-left-format-at-event "right now")
+
+ ;; Title truncation: limit long event titles to 15 characters
+ ;; This affects only the title, not the icon or countdown
+ (setq chime-max-title-length 25) ; "Very Long Me... ( in 10m)"
+
+ ;; Chime sound: disabled
+ (setq chime-play-sound nil)
;; Notification settings
(setq chime-notification-title "Reminder")
@@ -296,6 +305,9 @@ This allows a line to show in an agenda without being scheduled or a deadline."
;; Enable chime-mode automatically
(chime-mode 1))
+;; which-key labels
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements "C-c A" "chime check"))
(provide 'org-agenda-config)
;;; org-agenda-config.el ends here
diff --git a/modules/org-config.el b/modules/org-config.el
index 0249973f..753b1092 100644
--- a/modules/org-config.el
+++ b/modules/org-config.el
@@ -16,7 +16,7 @@
:init
(defvar-keymap cj/org-table-map
:doc "org table operations.")
- (keymap-global-set "C-c t" cj/org-table-map)
+ (keymap-set cj/custom-keymap "T" cj/org-table-map)
:bind
("C-c c" . org-capture)
("C-c a" . org-agenda)
@@ -266,5 +266,20 @@ the current buffer's cache. Useful when encountering parsing errors like
(message "Cleared org-element cache for current buffer"))
(user-error "Current buffer is not in org-mode"))))
+;; which-key labels for org-table-map
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-; T" "org table menu"
+ "C-; T r" "table row"
+ "C-; T r i" "insert row"
+ "C-; T r d" "delete row"
+ "C-; T c" "table column"
+ "C-; T c i" "insert column"
+ "C-; T c d" "delete column"
+ ;; org global bindings
+ "C-c a" "org agenda"
+ "C-c c" "org capture"
+ "C-c l" "org store link"))
+
(provide 'org-config)
;;; org-config.el ends here
diff --git a/modules/org-contacts-config.el b/modules/org-contacts-config.el
index df4e18f1..adb99db4 100644
--- a/modules/org-contacts-config.el
+++ b/modules/org-contacts-config.el
@@ -212,5 +212,14 @@ module provides more sophisticated completion."
;; Bind the org-contacts map to the C-c C prefix
(keymap-global-set "C-c C" cj/org-contacts-map)
+;; which-key labels
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-c C" "contacts menu"
+ "C-c C f" "find contact"
+ "C-c C n" "new contact"
+ "C-c C e" "insert email"
+ "C-c C v" "view all contacts"))
+
(provide 'org-contacts-config)
;;; org-contacts-config.el ends here
diff --git a/modules/org-drill-config.el b/modules/org-drill-config.el
index f18760c7..08047e3a 100644
--- a/modules/org-drill-config.el
+++ b/modules/org-drill-config.el
@@ -70,7 +70,13 @@
(keymap-set cj/custom-keymap "D" cj/drill-map)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; D" "org-drill menu")))
+ (which-key-add-key-based-replacements
+ "C-; D" "org-drill menu"
+ "C-; D s" "start drill"
+ "C-; D e" "edit drill file"
+ "C-; D c" "capture question"
+ "C-; D r" "refile to drill"
+ "C-; D R" "resume drill")))
(provide 'org-drill-config)
;;; org-drill-config.el ends here.
diff --git a/modules/org-gcal-config.el b/modules/org-gcal-config.el
index ed0831b8..0c309a0e 100644
--- a/modules/org-gcal-config.el
+++ b/modules/org-gcal-config.el
@@ -10,6 +10,9 @@
;; - Automatic removal of cancelled events, but with TODOs added for visibility
;; - System timezone configuration via functions in host-environment
;; - No notifications on syncing
+;; - Events are managed by Org (changes in org file push back to Google Calendar)
+;; This is controlled by org-gcal-managed-newly-fetched-mode and
+;; org-gcal-managed-update-existing-mode set to "org"
;; - Initial automatic sync post Emacs startup. No auto resync'ing.
;; (my calendar doesn't change hourly and I want fewer distractions and slowdowns).
;; if you need it: https://github.com/kidd/org-gcal.el?tab=readme-ov-file#sync-automatically-at-regular-times
@@ -35,6 +38,11 @@
(require 'host-environment)
(require 'user-constants)
+;; Forward declare org-gcal internal variables and functions
+(eval-when-compile
+ (defvar org-gcal--sync-lock))
+(declare-function org-gcal-reload-client-id-secret "org-gcal")
+
(defun cj/org-gcal-clear-sync-lock ()
"Clear the org-gcal sync lock.
Useful when a sync fails and leaves the lock in place, preventing future syncs."
@@ -42,7 +50,24 @@ Useful when a sync fails and leaves the lock in place, preventing future syncs."
(setq org-gcal--sync-lock nil)
(message "org-gcal sync lock cleared"))
+(defun cj/org-gcal-convert-all-to-org-managed ()
+ "Convert all org-gcal events in current buffer to Org-managed.
+
+Changes all events with org-gcal-managed property from `gcal' to `org',
+enabling bidirectional sync so changes push back to Google Calendar."
+ (interactive)
+ (let ((count 0))
+ (save-excursion
+ (goto-char (point-min))
+ (while (re-search-forward "^:org-gcal-managed: gcal$" nil t)
+ (replace-match ":org-gcal-managed: org")
+ (setq count (1+ count))))
+ (when (> count 0)
+ (save-buffer))
+ (message "Converted %d event(s) to Org-managed" count)))
+
(use-package org-gcal
+ :vc (:url "https://github.com/cjennings/org-gcal" :rev :newest)
:defer t ;; unless idle timer is set below
:bind (("C-; g" . org-gcal-sync)
("C-; G" . cj/org-gcal-clear-sync-lock))
@@ -71,6 +96,10 @@ Useful when a sync fails and leaves the lock in place, preventing future syncs."
(setq org-gcal-remove-api-cancelled-events t) ;; auto-remove cancelled events
(setq org-gcal-update-cancelled-events-with-todo t) ;; todo cancelled events for visibility
+ ;; Enable bidirectional sync - treat events as Org-managed so changes push back
+ (setq org-gcal-managed-newly-fetched-mode "org") ;; New events from GCal are Org-managed
+ (setq org-gcal-managed-update-existing-mode "org") ;; Existing events become Org-managed
+
:config
;; Enable plstore passphrase caching after org-gcal loads
(require 'plstore)
@@ -80,7 +109,21 @@ Useful when a sync fails and leaves the lock in place, preventing future syncs."
(setq org-gcal-local-timezone (cj/detect-system-timezone))
;; Reload client credentials (should already be loaded by org-gcal, but ensure it's set)
- (org-gcal-reload-client-id-secret))
+ (org-gcal-reload-client-id-secret)
+
+ ;; Auto-save gcal files after sync completes
+ (defun cj/org-gcal-save-files-after-sync (&rest _)
+ "Save all org-gcal files after sync completes."
+ (dolist (entry org-gcal-fetch-file-alist)
+ (let* ((file (cdr entry))
+ (buffer (get-file-buffer file)))
+ (when (and buffer (buffer-modified-p buffer))
+ (with-current-buffer buffer
+ (save-buffer)
+ (message "Saved %s after org-gcal sync" (file-name-nondirectory file)))))))
+
+ ;; Advise org-gcal--sync-unlock which is called when sync completes
+ (advice-add 'org-gcal--sync-unlock :after #'cj/org-gcal-save-files-after-sync))
;; Set up automatic initial sync on boot with error handling
;;(run-with-idle-timer
@@ -90,5 +133,11 @@ Useful when a sync fails and leaves the lock in place, preventing future syncs."
;; (org-gcal-sync)
;; (error (message "org-gcal: Initial sync failed: %s" err)))))
+;; which-key labels
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-; g" "gcal sync"
+ "C-; G" "clear sync lock"))
+
(provide 'org-gcal-config)
;;; org-gcal-config.el ends here
diff --git a/modules/org-roam-config.el b/modules/org-roam-config.el
index 07098743..a6b42ce7 100644
--- a/modules/org-roam-config.el
+++ b/modules/org-roam-config.el
@@ -19,6 +19,7 @@
;; ---------------------------------- Org Roam ---------------------------------
(use-package org-roam
+ :defer 1
:commands (org-roam-node-find org-roam-node-insert org-roam-db-autosync-mode)
:config
;; Enable autosync mode after org-roam loads
@@ -85,7 +86,9 @@
(add-to-list 'org-after-todo-state-change-hook
(lambda ()
(when (and (member org-state org-done-keywords)
- (not (member org-last-state org-done-keywords)))
+ (not (member org-last-state org-done-keywords))
+ ;; Don't run for gcal.org - it's managed by org-gcal
+ (not (string= (buffer-file-name) (expand-file-name gcal-file))))
(cj/org-roam-copy-todo-to-today)))))
;; ------------------------- Org Roam Insert Immediate -------------------------
@@ -294,5 +297,19 @@ title."
;; Message to user
(message "'%s' added as an org-roam node." title)))
+;; which-key labels
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-c n" "org-roam menu"
+ "C-c n l" "roam buffer toggle"
+ "C-c n f" "roam find node"
+ "C-c n p" "roam find project"
+ "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"))
+
(provide 'org-roam-config)
;;; org-roam-config.el ends here.
diff --git a/modules/popper-config.el b/modules/popper-config.el
index b0f503e8..d9a9d9b0 100644
--- a/modules/popper-config.el
+++ b/modules/popper-config.el
@@ -26,6 +26,7 @@
'("\\*Messages\\*"
"Output\\*$"
"\\*Async Shell Command\\*"
+ "\\*Async-native-compile-log\\*"
help-mode
compilation-mode))
(add-to-list 'display-buffer-alist
diff --git a/modules/prog-general.el b/modules/prog-general.el
index f6ebfe09..d8d9627d 100644
--- a/modules/prog-general.el
+++ b/modules/prog-general.el
@@ -264,12 +264,8 @@ If no such file exists there, display a message."
("C-c s n" . yas-new-snippet)
("C-c s e" . yas-visit-snippet-file)
:config
- (setq yas-snippet-dirs '(snippets-dir)))
-
-(use-package ivy-yasnippet
- :after yasnippet
- :bind
- ("C-c s i" . ivy-yasnippet))
+ (setq yas-snippet-dirs (list snippets-dir))
+ (yas-reload-all))
;; --------------------- Display Color On Color Declaration --------------------
;; display the actual color as highlight to color hex code
@@ -400,6 +396,15 @@ If no such file exists there, display a message."
"1.5 sec" nil 'delete-windows-on
(get-buffer-create "*compilation*"))))))
+;; which-key labels
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-c s" "snippets menu"
+ "C-c s n" "new snippet"
+ "C-c s e" "edit snippet"
+ "C-c s i" "insert snippet"
+ "C-c p" "projectile menu"
+ "C-c C-s" "symbol overlay"))
(provide 'prog-general)
;;; prog-general.el ends here
diff --git a/modules/prog-lisp.el b/modules/prog-lisp.el
index 7693c253..cfa015ae 100644
--- a/modules/prog-lisp.el
+++ b/modules/prog-lisp.el
@@ -97,9 +97,12 @@
:commands (with-mock mocklet mocklet-function)) ;; mock/stub framework
;; --------------------------------- Elisp Lint --------------------------------
+;; Comprehensive linting for Emacs Lisp code (indentation, whitespace, etc.)
+;; Used by chime.el 'make lint' target for code quality checks
(use-package elisp-lint
- :commands (elisp-lint-file elisp-lint-directory))
+ :ensure t
+ :commands (elisp-lint-file elisp-lint-directory elisp-lint-files-batch))
;; ------------------------------ Package Tooling ------------------------------
diff --git a/modules/selection-framework.el b/modules/selection-framework.el
index 66ca1cbd..a89afc02 100644
--- a/modules/selection-framework.el
+++ b/modules/selection-framework.el
@@ -27,7 +27,6 @@
(vertico-resize nil) ; Don't resize the minibuffer
(vertico-sort-function #'vertico-sort-history-alpha) ; History first, then alphabetical
:bind (:map vertico-map
- ;; Match ivy's C-j C-k behavior
("C-j" . vertico-next)
("C-k" . vertico-previous)
("C-l" . vertico-insert) ; Insert current candidate
@@ -128,7 +127,7 @@
;; Use Consult for completion-at-point
(setq completion-in-region-function #'consult-completion-in-region))
-(global-unset-key (kbd "C-s"))
+;; Override default search with consult-line
(keymap-global-set "C-s" #'consult-line)
;; Consult integration with Embark
@@ -152,10 +151,10 @@
(use-package orderless
:demand t
:custom
- (completion-styles '(orderless))
+ (completion-styles '(orderless basic))
(completion-category-defaults nil)
- (completion-category-overrides '((file (styles partial-completion))
- (multi-category (styles orderless))))
+ (completion-category-overrides '((file (styles partial-completion orderless basic))
+ (multi-category (styles orderless basic))))
(orderless-matching-styles '(orderless-literal
orderless-regexp
orderless-initialism
@@ -183,16 +182,10 @@
nil
(window-parameters (mode-line-format . none)))))
-;; this typo causes crashes
-;; (add-to-list 'display-buffer-alist
-;; '("\\=\\*Embark Collect \\(Live\\|Completions\\)\\*"
-;; nil
-;; (window-parameters (mode-line-format . none)))))
-
;; --------------------------- Consult Integration ----------------------------
;; Additional integrations for specific features
-;; Yasnippet integration - replaces ivy-yasnippet
+;; Yasnippet integration
(use-package consult-yasnippet
:after yasnippet
:bind ("C-c s i" . consult-yasnippet))
@@ -204,7 +197,7 @@
("C-c ! c" . consult-flycheck)))
;; ---------------------------------- Company ----------------------------------
-;; In-buffer completion (retained from original configuration)
+;; In-buffer completion for text and code
(use-package company
:demand t
@@ -259,5 +252,13 @@
:config
(company-prescient-mode))
+;; which-key labels
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-c h" "consult history"
+ "C-c s i" "insert snippet"
+ "M-g" "goto menu"
+ "M-s" "search menu"))
+
(provide 'selection-framework)
;;; selection-framework.el ends here
diff --git a/modules/system-commands.el b/modules/system-commands.el
new file mode 100644
index 00000000..fb8c0611
--- /dev/null
+++ b/modules/system-commands.el
@@ -0,0 +1,138 @@
+;;; system-commands.el --- System power and session management -*- lexical-binding: t; coding: utf-8; -*-
+;; author: Craig Jennings <c@cjennings.net>
+;;
+;;; Commentary:
+;;
+;; System commands for logout, lock, suspend, shutdown, reboot, and Emacs
+;; exit/restart. Provides both a keymap (C-; !) and a completing-read menu.
+;;
+;; Commands include:
+;; - Logout (terminate user session)
+;; - Lock screen (slock)
+;; - Suspend (systemctl suspend)
+;; - Shutdown (systemctl poweroff)
+;; - Reboot (systemctl reboot)
+;; - Exit Emacs (kill-emacs)
+;; - Restart Emacs (via systemctl --user restart emacs.service)
+;;
+;; Dangerous commands (logout, suspend, shutdown, reboot) require confirmation.
+;;
+;;; Code:
+
+(eval-when-compile (require 'keybindings))
+(eval-when-compile (require 'subr-x))
+(require 'rx)
+
+;; ------------------------------ System Commands ------------------------------
+
+(defun cj/system-cmd--resolve (cmd)
+ "Return (values symbol-or-nil command-string label) for CMD."
+ (cond
+ ((symbolp cmd)
+ (let ((val (and (boundp cmd) (symbol-value cmd))))
+ (unless (and (stringp val) (not (string-empty-p val)))
+ (user-error "Variable %s is not a non-empty string" cmd))
+ (list cmd val (symbol-name cmd))))
+ ((stringp cmd)
+ (let ((s (string-trim cmd)))
+ (when (string-empty-p s) (user-error "Command string is empty"))
+ (list nil s "command")))
+ (t (user-error "Error: cj/system-cmd expects a string or a symbol"))))
+
+(defun cj/system-cmd (cmd)
+ "Run CMD (string or symbol naming a string) detached via the shell.
+Shell expansions like $(...) are supported. Output is silenced.
+If CMD is deemed dangerous, ask for confirmation."
+ (interactive (list (read-shell-command "System command: ")))
+ (pcase-let ((`(,sym ,cmdstr ,label) (cj/system-cmd--resolve cmd)))
+ (when (and sym (get sym 'cj/system-confirm)
+ (memq (read-char-choice
+ (format "Run %s now (%s)? (Y/n) " label cmdstr)
+ '(?y ?Y ?n ?N ?\r ?\n ?\s))
+ '(?n ?N)))
+ (user-error "Aborted"))
+ (let ((proc (start-process-shell-command "cj/system-cmd" nil
+ (format "nohup %s >/dev/null 2>&1 &" cmdstr))))
+ (set-process-query-on-exit-flag proc nil)
+ (set-process-sentinel proc #'ignore)
+ (message "Running %s..." label))))
+
+(defmacro cj/defsystem-command (name var cmdstr &optional confirm)
+ "Define VAR with CMDSTR and interactive command NAME to run it.
+If CONFIRM is non-nil, mark VAR to always require confirmation."
+ (declare (indent defun))
+ `(progn
+ (defvar ,var ,cmdstr)
+ ,(when confirm `(put ',var 'cj/system-confirm t))
+ (defun ,name ()
+ ,(format "Run %s via `cj/system-cmd'." var)
+ (interactive)
+ (cj/system-cmd ',var))))
+
+;; Define system commands
+(cj/defsystem-command cj/system-cmd-logout logout-cmd "loginctl terminate-user $(whoami)" t)
+(cj/defsystem-command cj/system-cmd-lock lockscreen-cmd "slock")
+(cj/defsystem-command cj/system-cmd-suspend suspend-cmd "systemctl suspend" t)
+(cj/defsystem-command cj/system-cmd-shutdown shutdown-cmd "systemctl poweroff" t)
+(cj/defsystem-command cj/system-cmd-reboot reboot-cmd "systemctl reboot" t)
+
+(defun cj/system-cmd-exit-emacs ()
+ "Exit Emacs server and all clients."
+ (interactive)
+ (when (memq (read-char-choice
+ "Exit Emacs? (Y/n) "
+ '(?y ?Y ?n ?N ?\r ?\n ?\s))
+ '(?n ?N))
+ (user-error "Aborted"))
+ (kill-emacs))
+
+(defun cj/system-cmd-restart-emacs ()
+ "Restart Emacs server after saving buffers."
+ (interactive)
+ (when (memq (read-char-choice
+ "Restart Emacs? (Y/n) "
+ '(?y ?Y ?n ?N ?\r ?\n ?\s))
+ '(?n ?N))
+ (user-error "Aborted"))
+ (save-some-buffers)
+ ;; Start the restart process before killing Emacs
+ (run-at-time 0.5 nil
+ (lambda ()
+ (call-process-shell-command
+ "systemctl --user restart emacs.service && emacsclient -c"
+ nil 0)))
+ (run-at-time 1 nil #'kill-emacs)
+ (message "Restarting Emacs..."))
+
+(defvar-keymap cj/system-command-map
+ :doc "Keymap for system commands."
+ "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)
+(keymap-set cj/custom-keymap "!" cj/system-command-map)
+
+(defun cj/system-command-menu ()
+ "Present system commands via \='completing-read\='."
+ (interactive)
+ (let* ((commands '(("Logout System" . cj/system-cmd-logout)
+ ("Lock Screen" . cj/system-cmd-lock)
+ ("Suspend System" . cj/system-cmd-suspend)
+ ("Shutdown System" . cj/system-cmd-shutdown)
+ ("Reboot System" . cj/system-cmd-reboot)
+ ("Exit Emacs" . cj/system-cmd-exit-emacs)
+ ("Restart Emacs" . cj/system-cmd-restart-emacs)))
+ (choice (completing-read "System command: " commands nil t)))
+ (when-let ((cmd (alist-get choice commands nil nil #'equal)))
+ (call-interactively cmd))))
+
+(keymap-set cj/custom-keymap "!" #'cj/system-command-menu)
+
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements "C-; !" "system commands"))
+
+(provide 'system-commands)
+;;; system-commands.el ends here
diff --git a/modules/system-utils.el b/modules/system-utils.el
index 6e51c32c..eef20718 100644
--- a/modules/system-utils.el
+++ b/modules/system-utils.el
@@ -43,6 +43,9 @@
(message "Error occurred during evaluation: %s" (error-message-string err)))))
(keymap-global-set "C-c b" #'cj/eval-buffer-with-confirmation-or-error-message)
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements "C-c b" "eval buffer"))
+
;;; ---------------------------- Edit A File With Sudo ----------------------------
(use-package sudo-edit
diff --git a/modules/test-runner.el b/modules/test-runner.el
index b66efc81..125a8d20 100644
--- a/modules/test-runner.el
+++ b/modules/test-runner.el
@@ -2,26 +2,75 @@
;; author: Craig Jennings <c@cjennings.net>
;;
;;; Commentary:
-;; Provides utilities for running ERT tests with focus/unfocus workflow
+
+;; 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:
;;
-;; Tests should be located in the Projectile project test directories,
-;; typically "test" or "tests" under the project root.
-;; Falls back to =~/.emacs.d/tests= if not in a Projectile project.
+;; - 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
;;
-;; The default mode is to load and run all tests.
+;; SPECIAL BEHAVIORS:
;;
-;; To focus on running a specific set of test files:
-;; - Toggle the mode to "focus" mode
-;; - Add specific test files to the list of tests in "focus"
-;; - Running tests (smartly) will now just run those tests
+;; - 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
;;
-;; Don't forget to run all tests again in default mode at least once before finishing.
+;; 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
;;
;;; Code:
(require 'ert)
(require 'cl-lib)
+;;; External Variables and Functions
+
+(defvar cj/custom-keymap) ; Defined in init.el
+(declare-function projectile-project-root "projectile" ())
+
;;; Variables
(defvar cj/test-global-directory nil
@@ -35,7 +84,7 @@ Each element is a filename (without path) to run.")
(defvar cj/test-mode 'all
"Current test execution mode.
-Either 'all (run all tests) or 'focused (run only focused tests).")
+Either \\='all (run all tests) or \\='focused (run only focused tests).")
(defvar cj/test-last-results nil
"Results from the last test run.")
@@ -45,8 +94,9 @@ Either 'all (run all tests) or 'focused (run only focused tests).")
(defun cj/test--get-test-directory ()
"Return the test directory path for the current project.
-If in a Projectile project, prefers a 'test' or 'tests' directory inside the project root.
-Falls back to =cj/test-global-directory= if not found or not in a project."
+If in a Projectile project, prefers \\='test or \\='tests directory
+inside the project root. Falls back to `cj/test-global-directory'
+if not found or not in a project."
(require 'projectile)
(let ((project-root (ignore-errors (projectile-project-root))))
(if (not (and project-root (file-directory-p project-root)))
@@ -60,12 +110,31 @@ Falls back to =cj/test-global-directory= if not found or not in a project."
(t cj/test-global-directory))))))
(defun cj/test--get-test-files ()
- "Return a list of test file names (without path) in the appropriate test directory."
+ "Return list of test file names (without path) in test directory."
(let ((dir (cj/test--get-test-directory)))
(when (file-directory-p dir)
(mapcar #'file-name-nondirectory
(directory-files dir t "^test-.*\\.el$")))))
+(defun cj/test--do-load-files (_dir files)
+ "Load test FILES from DIR.
+Returns: (cons \\='success loaded-count) on success,
+ (cons \\='error (list failed-files errors)) on errors."
+ (let ((loaded-count 0)
+ (errors '()))
+ (dolist (file files)
+ (condition-case err
+ (progn
+ (load-file file)
+ (setq loaded-count (1+ loaded-count)))
+ (error
+ (push (cons (file-name-nondirectory file)
+ (error-message-string err))
+ errors))))
+ (if (null errors)
+ (cons 'success loaded-count)
+ (cons 'error (list loaded-count (nreverse errors))))))
+
(defun cj/test-load-all ()
"Load all test files from the appropriate test directory."
(interactive)
@@ -73,19 +142,26 @@ Falls back to =cj/test-global-directory= if not found or not in a project."
(let ((dir (cj/test--get-test-directory)))
(unless (file-directory-p dir)
(user-error "Test directory %s does not exist" dir))
- (let ((test-files (directory-files dir t "^test-.*\\.el$"))
- (loaded-count 0))
- (dolist (file test-files)
- (condition-case err
- (progn
- (load-file file)
- (setq loaded-count (1+ loaded-count))
- (message "Loaded test file: %s" (file-name-nondirectory file)))
- (error
- (message "Error loading %s: %s"
- (file-name-nondirectory file)
- (error-message-string err)))))
- (message "Loaded %d test file(s)" loaded-count))))
+ (let ((test-files (directory-files dir t "^test-.*\\.el$")))
+ (pcase (cj/test--do-load-files dir test-files)
+ (`(success . ,count)
+ (message "Loaded %d test file(s)" count))
+ (`(error ,count ,errors)
+ (dolist (err errors)
+ (message "Error loading %s: %s" (car err) (cdr err)))
+ (message "Loaded %d test file(s) with %d error(s)" count (length errors)))))))
+
+(defun cj/test--do-focus-add (filename available-files focused-files)
+ "Add FILENAME to focused test files.
+AVAILABLE-FILES is the list of all available test files.
+FOCUSED-FILES is the current list of focused files.
+Returns: \\='success if added successfully,
+ \\='already-focused if file is already focused,
+ \\='not-available if file is not in available-files."
+ (cond
+ ((not (member filename available-files)) 'not-available)
+ ((member filename focused-files) 'already-focused)
+ (t 'success)))
(defun cj/test-focus-add ()
"Select test file(s) to add to the focused list."
@@ -105,27 +181,63 @@ Falls back to =cj/test-global-directory= if not found or not in a project."
unfocused-files
nil t)
(user-error "All test files are already focused"))))
- (push selected cj/test-focused-files)
- (message "Added to focus: %s" selected)
- (when (called-interactively-p 'interactive)
- (cj/test-view-focused))))))
+ (pcase (cj/test--do-focus-add selected available-files cj/test-focused-files)
+ ('success
+ (push selected cj/test-focused-files)
+ (message "Added to focus: %s" selected)
+ (when (called-interactively-p 'interactive)
+ (cj/test-view-focused)))
+ ('already-focused
+ (message "Already focused: %s" selected))
+ ('not-available
+ (user-error "File not available: %s" selected)))))))
+
+(defun cj/test--do-focus-add-file (filepath testdir focused-files)
+ "Validate and add FILEPATH to focused list.
+TESTDIR is the test directory path.
+FOCUSED-FILES is the current list of focused files.
+Returns: \\='success if added successfully,
+ \\='no-file if filepath is nil,
+ \\='not-in-testdir if file is not inside test directory,
+ \\='already-focused if file is already focused.
+Second value is the relative filename if successful."
+ (cond
+ ((null filepath) (cons 'no-file nil))
+ ((not (string-prefix-p (file-truename testdir) (file-truename filepath)))
+ (cons 'not-in-testdir nil))
+ (t
+ (let ((relative (file-relative-name filepath testdir)))
+ (if (member relative focused-files)
+ (cons 'already-focused relative)
+ (cons 'success relative))))))
(defun cj/test-focus-add-this-buffer-file ()
"Add the current buffer's file to the focused test list."
(interactive)
(let ((file (buffer-file-name))
(dir (cj/test--get-test-directory)))
- (unless file
- (user-error "Current buffer is not visiting a file"))
- (unless (string-prefix-p (file-truename dir) (file-truename file))
- (user-error "File is not inside the test directory: %s" dir))
- (let ((relative (file-relative-name file dir)))
- (if (member relative cj/test-focused-files)
- (message "Already focused: %s" relative)
- (push relative cj/test-focused-files)
- (message "Added to focus: %s" relative)
- (when (called-interactively-p 'interactive)
- (cj/test-view-focused))))))
+ (pcase (cj/test--do-focus-add-file file dir cj/test-focused-files)
+ (`(no-file . ,_)
+ (user-error "Current buffer is not visiting a file"))
+ (`(not-in-testdir . ,_)
+ (user-error "File is not inside the test directory: %s" dir))
+ (`(already-focused . ,relative)
+ (message "Already focused: %s" relative))
+ (`(success . ,relative)
+ (push relative cj/test-focused-files)
+ (message "Added to focus: %s" relative)
+ (when (called-interactively-p 'interactive)
+ (cj/test-view-focused))))))
+
+(defun cj/test--do-focus-remove (filename focused-files)
+ "Remove FILENAME from FOCUSED-FILES.
+Returns: \\='success if removed successfully,
+ \\='empty-list if focused-files is empty,
+ \\='not-found if filename is not in focused-files."
+ (cond
+ ((null focused-files) 'empty-list)
+ ((not (member filename focused-files)) 'not-found)
+ (t 'success)))
(defun cj/test-focus-remove ()
"Remove a test file from the focused list."
@@ -135,11 +247,17 @@ Falls back to =cj/test-global-directory= if not found or not in a project."
(let ((selected (completing-read "Remove from focus: "
cj/test-focused-files
nil t)))
- (setq cj/test-focused-files
- (delete selected cj/test-focused-files))
- (message "Removed from focus: %s" selected)
- (when (called-interactively-p 'interactive)
- (cj/test-view-focused)))))
+ (pcase (cj/test--do-focus-remove selected cj/test-focused-files)
+ ('success
+ (setq cj/test-focused-files
+ (delete selected cj/test-focused-files))
+ (message "Removed from focus: %s" selected)
+ (when (called-interactively-p 'interactive)
+ (cj/test-view-focused)))
+ ('not-found
+ (message "File not in focused list: %s" selected))
+ ('empty-list
+ (user-error "No focused files to remove"))))))
(defun cj/test-focus-clear ()
"Clear all focused test files."
@@ -161,55 +279,69 @@ Returns a list of test name symbols defined in the file."
(push (match-string 1) test-names)))
test-names))
+(defun cj/test--do-get-focused-tests (focused-files test-dir)
+ "Get test names from FOCUSED-FILES in TEST-DIR.
+Returns: (cons \\='success (list test-names loaded-count)) if successful,
+ (cons \\='no-tests nil) if no tests found,
+ (cons \\='empty-list nil) if focused-files is empty."
+ (if (null focused-files)
+ (cons 'empty-list nil)
+ (let ((all-test-names '())
+ (loaded-count 0))
+ (dolist (file focused-files)
+ (let ((full-path (expand-file-name file test-dir)))
+ (when (file-exists-p full-path)
+ (load-file full-path)
+ (setq loaded-count (1+ loaded-count))
+ (let ((test-names (cj/test--extract-test-names full-path)))
+ (setq all-test-names (append all-test-names test-names))))))
+ (if (null all-test-names)
+ (cons 'no-tests nil)
+ (cons 'success (list all-test-names loaded-count))))))
+
(defun cj/test-run-focused ()
"Run only the focused test files."
(interactive)
- (if (null cj/test-focused-files)
- (user-error "No focused files set. Use =cj/test-focus-add' first")
- (let ((all-test-names '())
- (loaded-count 0)
- (dir (cj/test--get-test-directory)))
- ;; Load the focused files and collect their test names
- (dolist (file cj/test-focused-files)
- (let ((full-path (expand-file-name file dir)))
- (when (file-exists-p full-path)
- (load-file full-path)
- (setq loaded-count (1+ loaded-count))
- ;; Extract test names from this file
- (let ((test-names (cj/test--extract-test-names full-path)))
- (setq all-test-names (append all-test-names test-names))))))
- (if (null all-test-names)
- (message "No tests found in focused files")
- ;; Build a regexp that matches any of our test names
- (let ((pattern (regexp-opt all-test-names)))
- (message "Running %d test(s) from %d focused file(s)"
- (length all-test-names) loaded-count)
- ;; Run only the tests we found
- (ert (concat "^" pattern "$")))))))
+ (let ((dir (cj/test--get-test-directory)))
+ (pcase (cj/test--do-get-focused-tests cj/test-focused-files dir)
+ (`(empty-list . ,_)
+ (user-error "No focused files set. Use =cj/test-focus-add' first"))
+ (`(no-tests . ,_)
+ (message "No tests found in focused files"))
+ (`(success ,test-names ,loaded-count)
+ (let ((pattern (regexp-opt test-names)))
+ (message "Running %d test(s) from %d focused file(s)"
+ (length test-names) loaded-count)
+ (ert (concat "^" pattern "$")))))))
(defun cj/test--ensure-test-dir-in-load-path ()
- "Ensure the directory returned by cj/test--get-test-directory is in `load-path`."
+ "Ensure test directory is in `load-path'."
(let ((dir (cj/test--get-test-directory)))
(when (and dir (file-directory-p dir))
(add-to-list 'load-path dir))))
+(defun cj/test--extract-test-at-pos ()
+ "Extract test name at current position.
+Returns: test name symbol if found, nil otherwise."
+ (save-excursion
+ (beginning-of-defun)
+ (condition-case nil
+ (let ((form (read (current-buffer))))
+ (when (and (listp form)
+ (eq (car form) 'ert-deftest)
+ (symbolp (cadr form)))
+ (cadr form)))
+ (error nil))))
+
(defun cj/run-test-at-point ()
"Run the ERT test at point.
If point is inside an `ert-deftest` definition, run that test only.
Otherwise, message that no test is found."
(interactive)
- (let ((original-point (point)))
- (save-excursion
- (beginning-of-defun)
- (condition-case nil
- (let ((form (read (current-buffer))))
- (if (and (listp form)
- (eq (car form) 'ert-deftest)
- (symbolp (cadr form)))
- (ert (cadr form))
- (message "Not in an ERT test method.")))
- (error (message "No ERT test methods found at point."))))
- (goto-char original-point)))
+ (let ((test-name (cj/test--extract-test-at-pos)))
+ (if test-name
+ (ert test-name)
+ (message "Not in an ERT test method."))))
(defun cj/test-run-all ()
"Load and run all tests."
@@ -218,7 +350,7 @@ Otherwise, message that no test is found."
(ert t))
(defun cj/test-toggle-mode ()
- "Toggle between 'all and 'focused test execution modes."
+ "Toggle between \\='all and \\='focused test execution modes."
(interactive)
(setq cj/test-mode (if (eq cj/test-mode 'all) 'focused 'all))
(message "Test mode: %s" cj/test-mode))
@@ -252,8 +384,20 @@ Otherwise, message that no test is found."
"t" #'cj/test-toggle-mode)
(keymap-set cj/custom-keymap "t" cj/testrunner-map)
+
+;; which-key integration
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; t" "test runner menu"))
+ (which-key-add-key-based-replacements
+ "C-; t" "test runner menu"
+ "C-; t L" "load all tests"
+ "C-; t R" "run all tests"
+ "C-; t r" "run smart"
+ "C-; t ." "run test at point"
+ "C-; t a" "add to focus"
+ "C-; t b" "add buffer to focus"
+ "C-; t c" "clear focus"
+ "C-; t v" "view focused"
+ "C-; t t" "toggle mode"))
(provide 'test-runner)
;;; test-runner.el ends here
diff --git a/modules/text-config.el b/modules/text-config.el
index 730e36a3..29db9e0b 100644
--- a/modules/text-config.el
+++ b/modules/text-config.el
@@ -46,8 +46,7 @@
;; change inner and outer, just like in vim.
(use-package change-inner
- :bind (("C-c i" . change-inner)
- ("C-c o" . change-outer)))
+ :commands (change-inner change-outer))
;; ------------------------------ Delete Selection -----------------------------
;; delete the region on character insertion
diff --git a/modules/vc-config.el b/modules/vc-config.el
index 3b116cc1..a936e890 100644
--- a/modules/vc-config.el
+++ b/modules/vc-config.el
@@ -131,7 +131,16 @@
(keymap-set cj/custom-keymap "v" cj/vc-map)
(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements "C-; v" "version control menu"))
+ (which-key-add-key-based-replacements
+ "C-; v" "version control menu"
+ "C-; v d" "goto diff hunks"
+ "C-; v c" "create issue"
+ "C-; v f" "forge pull"
+ "C-; v i" "list issues"
+ "C-; v n" "next hunk"
+ "C-; v p" "previous hunk"
+ "C-; v r" "list pull requests"
+ "C-; v t" "git timemachine"))
(provide 'vc-config)
;;; vc-config.el ends here.
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el
index fa4c2926..73f782f6 100644
--- a/modules/video-audio-recording.el
+++ b/modules/video-audio-recording.el
@@ -227,5 +227,14 @@ Otherwise use the default location in `audio-recordings-dir'."
(keymap-set cj/custom-keymap "r" cj/record-map)
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-; r" "recording menu"
+ "C-; r v" "start video"
+ "C-; r V" "stop video"
+ "C-; r a" "start audio"
+ "C-; r A" "stop audio"
+ "C-; r l" "adjust levels"))
+
(provide 'video-audio-recording)
;;; video-audio-recording.el ends here.
diff --git a/modules/weather-config.el b/modules/weather-config.el
index 526a0b41..31fb1b70 100644
--- a/modules/weather-config.el
+++ b/modules/weather-config.el
@@ -11,9 +11,8 @@
;; ----------------------------------- Wttrin ----------------------------------
(use-package wttrin
+ :vc (:url "https://github.com/cjennings/emacs-wttrin" :rev :newest)
:defer t
- :load-path ("~/code/wttrin")
- :ensure nil ;; local package
:preface
;; dependency for wttrin
(use-package xterm-color
diff --git a/modules/wip.el b/modules/wip.el
index 80b3295d..93c799fb 100644
--- a/modules/wip.el
+++ b/modules/wip.el
@@ -14,133 +14,6 @@
;;
;;; Code:
-(eval-when-compile (require 'user-constants))
-(eval-when-compile (require 'keybindings))
-(eval-when-compile (require 'subr-x)) ;; for system commands
-(require 'rx) ;; for system commands
-
-;; ------------------------------ System Commands ------------------------------
-
-(defun cj/system-cmd--resolve (cmd)
- "Return (values symbol-or-nil command-string label) for CMD."
- (cond
- ((symbolp cmd)
- (let ((val (and (boundp cmd) (symbol-value cmd))))
- (unless (and (stringp val) (not (string-empty-p val)))
- (user-error "Variable %s is not a non-empty string" cmd))
- (list cmd val (symbol-name cmd))))
- ((stringp cmd)
- (let ((s (string-trim cmd)))
- (when (string-empty-p s) (user-error "Command string is empty"))
- (list nil s "command")))
- (t (user-error "Error: cj/system-cmd expects a string or a symbol"))))
-
-(defun cj/system-cmd (cmd)
- "Run CMD (string or symbol naming a string) detached via the shell.
-Shell expansions like $(...) are supported. Output is silenced.
-If CMD is deemed dangerous, ask for confirmation."
- (interactive (list (read-shell-command "System command: ")))
- (pcase-let ((`(,sym ,cmdstr ,label) (cj/system-cmd--resolve cmd)))
- (when (and sym (get sym 'cj/system-confirm)
- (memq (read-char-choice
- (format "Run %s now (%s)? (Y/n) " label camdstr)
- '(?y ?Y ?n ?N ?\r ?\n ?\s))
- '(?n ?N)))
- (user-error "Aborted"))
- (let ((proc (start-process-shell-command "cj/system-cmd" nil
- (format "nohup %s >/dev/null 2>&1 &" cmdstr))))
- (set-process-query-on-exit-flag proc nil)
- (set-process-sentinel proc #'ignore)
- (message "Running %s..." label))))
-
-(defmacro cj/defsystem-command (name var cmdstr &optional confirm)
- "Define VAR with CMDSTR and interactive command NAME to run it.
-If CONFIRM is non-nil, mark VAR to always require confirmation."
- (declare (indent defun))
- `(progn
- (defvar ,var ,cmdstr)
- ,(when confirm `(put ',var 'cj/system-confirm t))
- (defun ,name ()
- ,(format "Run %s via `cj/system-cmd'." var)
- (interactive)
- (cj/system-cmd ',var))))
-
-;; Define system commands
-(cj/defsystem-command cj/system-cmd-logout logout-cmd "loginctl terminate-user $(whoami)" t)
-(cj/defsystem-command cj/system-cmd-lock lockscreen-cmd "slock")
-(cj/defsystem-command cj/system-cmd-suspend suspend-cmd "systemctl suspend" t)
-(cj/defsystem-command cj/system-cmd-shutdown shutdown-cmd "systemctl poweroff" t)
-(cj/defsystem-command cj/system-cmd-reboot reboot-cmd "systemctl reboot" t)
-
-(defun cj/system-cmd-exit-emacs ()
- "Exit Emacs server and all clients."
- (interactive)
- (when (memq (read-char-choice
- "Exit Emacs? (Y/n) "
- '(?y ?Y ?n ?N ?\r ?\n ?\s))
- '(?n ?N))
- (user-error "Aborted"))
- (kill-emacs))
-
-(defun cj/system-cmd-restart-emacs ()
- "Restart Emacs server after saving buffers."
- (interactive)
- (when (memq (read-char-choice
- "Restart Emacs? (Y/n) "
- '(?y ?Y ?n ?N ?\r ?\n ?\s))
- '(?n ?N))
- (user-error "Aborted"))
- (save-some-buffers)
- ;; Start the restart process before killing Emacs
- (run-at-time 0.5 nil
- (lambda ()
- (call-process-shell-command
- "systemctl --user restart emacs.service && emacsclient -c"
- nil 0)))
- (run-at-time 1 nil #'kill-emacs)
- (message "Restarting Emacs..."))
-
-;; (defvar-keymap cj/system-command-map
-;; :doc "Keymap for system commands."
-;; "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)
-;; (keymap-set cj/custom-keymap "!" cj/system-command-map)
-
-(defun cj/system-command-menu ()
- "Present system commands via \='completing-read\='."
- (interactive)
- (let* ((commands '(("Logout System" . cj/system-cmd-logout)
- ("Lock Screen" . cj/system-cmd-lock)
- ("Suspend System" . cj/system-cmd-suspend)
- ("Shutdown System" . cj/system-cmd-shutdown)
- ("Reboot System" . cj/system-cmd-reboot)
- ("Exit Emacs" . cj/system-cmd-exit-emacs)
- ("Restart Emacs" . cj/system-cmd-restart-emacs)))
- (choice (completing-read "System command: " commands nil t)))
- (when-let ((cmd (alist-get choice commands nil nil #'equal)))
- (call-interactively cmd))))
-
-(keymap-set cj/custom-keymap "!" #'cj/system-command-menu)
-
-
-;; --------------------------- Org Upcoming Modeline ---------------------------
-
-;; (use-package org-upcoming-modeline
-;; :after org
-;; :load-path "~/code/org-upcoming-modeline/org-upcoming-modeline.el"
-;; :config
-;; (setq org-upcoming-modeline-keep-late 300)
-;; (setq org-upcoming-modeline-ignored-keywords '("DONE" "CANCELLED" "FAILED"))
-;; (setq org-upcoming-modeline-trim 30)
-;; (setq org-upcoming-modeline-days-ahead 5)
-;; (setq org-upcoming-modeline-format (lambda (ms mh) (format "📅 %s %s" ms mh)))
-;; (org-upcoming-modeline-mode))
-
;; ----------------------------------- Efrit -----------------------------------
;; not working as of Wednesday, September 03, 2025 at 12:44:09 AM CDT
@@ -183,30 +56,5 @@ If CONFIRM is non-nil, mark VAR to always require confirmation."
:bind ("M-p" . pomm)
:commands (pomm pomm-third-time))
-;; ----------------------------------- Popper ----------------------------------
-
-;; (use-package popper
-;; :bind (("C-`" . popper-toggle)
-;; ("M-`" . popper-cycle)
-;; ("C-M-`" . popper-toggle-type))
-;; :custom
-;; (popper-display-control-nil)
-;; :init
-;; (setq popper-reference-buffers
-;; '("\\*Messages\\*"
-;; "Output\\*$"
-;; "\\*Async Shell Command\\*"
-;; ;; "\\*scratch\\*"
-;; help-mode
-;; compilation-mode))
-;; (add-to-list 'display-buffer-alist
-;; '(popper-display-control-p ; Predicate to match popper buffers
-;; (display-buffer-in-side-window)
-;; (side . bottom)
-;; (slot . 0)
-;; (window-height . 0.5))) ; Half the frame height
-;; (popper-mode +1)
-;; (popper-echo-mode +1))
-
(provide 'wip)
;;; wip.el ends here.