summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2024-04-07 13:41:34 -0500
committerCraig Jennings <c@cjennings.net>2024-04-07 13:41:34 -0500
commit754bbf7a25a8dda49b5d08ef0d0443bbf5af0e36 (patch)
treef1190704f78f04a2b0b4c977d20fe96a828377f1 /modules
new repository
Diffstat (limited to 'modules')
-rw-r--r--modules/ai-config.el51
-rw-r--r--modules/calibredb-epub-config.el103
-rw-r--r--modules/config-utilities.el112
-rw-r--r--modules/custom-functions.el506
-rw-r--r--modules/dashboard-config.el141
-rw-r--r--modules/diff-config.el53
-rw-r--r--modules/dirvish-config.el183
-rw-r--r--modules/elfeed-config.el150
-rw-r--r--modules/epa-config.el30
-rw-r--r--modules/eradio-config.el36
-rw-r--r--modules/erc-config.el109
-rw-r--r--modules/eshell-vterm-config.el152
-rw-r--r--modules/eww-config.el28
-rw-r--r--modules/flycheck-config.el47
-rw-r--r--modules/flyspell-config.el191
-rw-r--r--modules/font-config.el200
-rw-r--r--modules/games-config.el69
-rw-r--r--modules/graphviz-config.el17
-rw-r--r--modules/help-utils.el89
-rw-r--r--modules/host-environment.el48
-rw-r--r--modules/httpd-config.el26
-rw-r--r--modules/keybindings.el132
-rw-r--r--modules/latex-config.el53
-rw-r--r--modules/ledger-config.el74
-rw-r--r--modules/local-repository.el50
-rw-r--r--modules/mail-config.el210
-rw-r--r--modules/markdown-config.el47
-rw-r--r--modules/modeline-config.el36
-rw-r--r--modules/org-agenda-config.el254
-rw-r--r--modules/org-appearance-config.el64
-rw-r--r--modules/org-babel-config.el88
-rw-r--r--modules/org-capture-config.el107
-rw-r--r--modules/org-config.el154
-rw-r--r--modules/org-contacts-config.el30
-rw-r--r--modules/org-drill-config.el88
-rw-r--r--modules/org-export-config.el39
-rw-r--r--modules/org-refile-config.el62
-rw-r--r--modules/org-roam-config.el192
-rw-r--r--modules/pdf-config.el51
-rw-r--r--modules/prog-c.el32
-rw-r--r--modules/prog-comments.el136
-rw-r--r--modules/prog-general.el203
-rw-r--r--modules/prog-go.el85
-rw-r--r--modules/prog-lisp.el128
-rw-r--r--modules/prog-lsp.el55
-rw-r--r--modules/prog-python.el74
-rw-r--r--modules/prog-shell.el14
-rw-r--r--modules/prog-training.el30
-rw-r--r--modules/prog-webdev.el79
-rw-r--r--modules/prog-yaml.el18
-rw-r--r--modules/reconcile-open-repos.el72
-rw-r--r--modules/selection-framework.el155
-rw-r--r--modules/show-kill-ring.el106
-rw-r--r--modules/system-defaults.el195
-rw-r--r--modules/system-utils.el311
-rw-r--r--modules/telegram-config.el48
-rw-r--r--modules/test-code.el170
-rw-r--r--modules/text-config.el102
-rw-r--r--modules/tramp-config.el52
-rw-r--r--modules/treesitter-config.el38
-rw-r--r--modules/ui-config.el76
-rw-r--r--modules/ui-navigation.el165
-rw-r--r--modules/ui-theme.el110
-rw-r--r--modules/vc-config.el97
-rw-r--r--modules/wrap-up.el30
65 files changed, 6653 insertions, 0 deletions
diff --git a/modules/ai-config.el b/modules/ai-config.el
new file mode 100644
index 00000000..14380a2b
--- /dev/null
+++ b/modules/ai-config.el
@@ -0,0 +1,51 @@
+;;; ai-config.el --- Configuration for AI Integrations -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;; There are several workflows available. Here are the ones I use most.
+
+;; - Launch GPTel and chat with the AI in a separate buffer.
+;; Chatting is fine, but it can mean cutting and pasting code back and forth
+;; between buffers.
+
+;; - Select a region and launch GPTel.
+;; The region is automatically inserted into the buffer making it easy to
+;; simply ask a question after it's read the code.
+
+;; Note that you can save a file, then turn on gptel-mode to resume your
+;; conversation.
+
+;; Remember that sending the message requires C-<return>.
+
+;;; Code:
+
+;; ----------------------------------- GPTel -----------------------------------
+;; integration with ChatGPT and other large language models.
+
+(use-package gptel
+ :defer .5
+ :bind
+ ("C-h G" . gptel)
+ (:map gptel-mode-map
+ ("C-<return>" . gptel-send))
+ :custom
+ ;; (gptel-model "gpt-3.5-turbo-16k") ;; next best alternative
+ (gptel-model "gpt-4")
+ (gptel-default-mode 'org-mode)
+ :config
+ (setq gptel-directives
+ '((default
+ . "You are a large language model living in Emacs and a careful and
+ knowledgeable emacs-lisp programmer. Respond accurately and concisely.")
+ (programming
+ . "You are a large language model and a careful programmer. Provide code
+ and only code as output without any additional text, prompt or note.")
+ (writing
+ . "You are a large language model and a writing assistant. Respond concisely.")
+ (chat
+ . "You are a large language model and a conversation partner. Respond concisely.")))
+
+ (setq gptel-api-key (auth-source-pick-first-password :host "api.openai.com")))
+
+(provide 'ai-config)
+;;; ai-config.el ends here.
diff --git a/modules/calibredb-epub-config.el b/modules/calibredb-epub-config.el
new file mode 100644
index 00000000..92e65e2a
--- /dev/null
+++ b/modules/calibredb-epub-config.el
@@ -0,0 +1,103 @@
+;;; calibredb-epub-config --- Functionality for Ebook Management and Display -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;; Note: Calibre virtual library functionality works as designed, but not as I
+;; want. I had hoped to simply view a virtual library defined by it's tag.
+;; Instead, it searches for the library keywords within the description as well,
+;; turning up many books that aren't remotely related. I've overwritten the
+;; virtual-library functionality to simply filter by tag, and given that the
+;; "l" keybinding in the calibredb-search-mode-map.
+
+;;; Code:
+
+;; -------------------------- CalibreDB Ebook Manager --------------------------
+
+(use-package calibredb
+ :defer 1
+ :commands calibredb
+ :bind
+ ("M-B" . calibredb)
+ ;; override virtual libraries to filter-by-tag
+ (:map calibredb-search-mode-map
+ ("l" . calibredb-filter-by-tag))
+ :config
+ ;; basic config
+ (setq calibredb-root-dir "~/sync/books/")
+ (setq calibredb-library-alist '(("~/sync/books/")))
+ (setq calibredb-db-dir (expand-file-name "metadata.db" calibredb-root-dir))
+ (setq calibredb-program "/usr/bin/calibredb")
+ (setq calibredb-preferred-format "epub")
+
+ ;; search window display
+ (setq calibredb-size-show nil)
+ (setq calibredb-order "asc")
+ (setq calibredb-id-width 7))
+
+;; ------------------------------ Nov Epub Reader ------------------------------
+
+(use-package nov
+ :defer .5
+ :after (visual-fill-column)
+ :mode ("\\.epub\\'" . nov-mode)
+;; :hook (nov-mode . cj/nov-apply-preferences)
+ :bind
+ (:map nov-mode-map
+ ("m" . cj/bookmark-set-and-save)
+ ("b" . bookmark-bmenu-list)
+ ("r" . nov-render-document)
+ ("l" . recenter-top-bottom)
+ ("d" . sdcv-search)
+ ("." . cj/forward-paragraph-and-center)
+ ("<" . nov-history-back)
+ (">" . nov-history-forward)
+ ("," . backward-paragraph)
+ ("z" . (lambda () (interactive) (cj/open-file-with-command "zathura")))
+ ("e" . (lambda () (interactive) (cj/open-file-with-command "evince")))
+ ("t" . nov-goto-toc)))
+
+(defun cj/forward-paragraph-and-center()
+ "Forward one paragraph and center the page."
+ (interactive)
+ (forward-paragraph)
+ (recenter))
+
+(defun cj/nov-apply-preferences ()
+ "Apply preferences after nove-mode has launched.
+Meant to be called via the nov-mode hook to apply font and display preferences
+ when displaying epub files."
+ (interactive)
+ (face-remap-add-relative 'variable-pitch :height 180) ;; increase the size for both variable...
+ (face-remap-add-relative 'fixed-pitch :height 180) ;; ...and fixed-pitch fonts for readability
+ (setq nov-text-width 115) ;; narrow text width
+ (when (require 'visual-fill-column nil t) ;; if visual-fill-column isn't already loaded, do it now.
+ (setq-local visual-fill-column-center-text t ;; center the text
+ visual-fill-column-width (+ nov-text-width 10)) ;; helps avoid truncation of long word
+ (hl-line-mode)
+ (visual-fill-column-mode 1) ;; wrap lines according to fill-column
+ (nov-render-document))) ;; re-render the epub
+
+(defun cj/nov-center-images ()
+ "Center the images in an nov document.
+To be called immediately after nov renders the html via
+the nov-post-html-render-hook."
+ (let* ((pixel-buffer-width (shr-pixel-buffer-width))
+ match)
+ (save-excursion
+ (goto-char (point-min))
+ (while (setq match (text-property-search-forward
+ 'display nil
+ (lambda (_ p) (eq (car-safe p) 'image))))
+ (when-let ((size (car (image-size
+ (prop-match-value match) 'pixels)))
+ ((> size 150))
+ (center-pixel (floor (- pixel-buffer-width size) 2))
+ (center-pos (floor center-pixel (frame-char-width))))
+ (beginning-of-line)
+ (indent-to center-pos)
+ (end-of-line))))))
+(add-hook 'nov-post-html-render-hook 'cj/nov-center-images)
+
+(provide 'calibredb-epub-config)
+;;; calibredb-epub-config.el ends here
diff --git a/modules/config-utilities.el b/modules/config-utilities.el
new file mode 100644
index 00000000..4e0fd923
--- /dev/null
+++ b/modules/config-utilities.el
@@ -0,0 +1,112 @@
+;;; config-utilities --- Config Hacking Utilities -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+;; Convenience utilities for working on Emacs configuration.
+
+;;; Code:
+
+;; ------------------------------ Reload Init File -----------------------------
+;; it does what it says it does.
+
+(defun cj/reload-init-file ()
+ "Reload the init file. Useful when modifying Emacs config."
+ (interactive)
+ (load-file user-init-file))
+
+;; ---------------------------- Recompile Emacs Home ---------------------------
+;; deletes all .elc and .eln files in user-emacs-directory, then compiles
+;; all emacs-lisp files natively if supported, or byte-compiles them if not.
+
+(defun cj/recompile-emacs-home()
+ "Delete all compiled files in Emacs home recursively before recompilation.
+Will recompile natively if supported, or byte-compiled if not."
+ (interactive)
+ (let* ((native-comp-supported (boundp 'native-compile-async))
+ (elt-dir (expand-file-name (if native-comp-supported "eln" "elc") user-emacs-directory))
+ (message-format (format "Please confirm recursive %s recompilation of %%s: " (if native-comp-supported "native" "byte")))
+ (compile-message (format "%scompiling all emacs-lisp files in %%s" (if native-comp-supported "Natively " "Byte-"))))
+ (if (yes-or-no-p (format message-format user-emacs-directory))
+ (progn
+ (message "Deleting all compiled files in %s" user-emacs-directory)
+ (dolist (file (directory-files-recursively user-emacs-directory "\\(\\.elc\\|\\.eln\\)$"))
+ (delete-file file))
+ (when (file-directory-p elt-dir)
+ (delete-directory elt-dir t t))
+ (message compile-message user-emacs-directory)
+ (if native-comp-supported
+ (let ((comp-async-report-warnings-errors nil))
+ (native-compile-async user-emacs-directory 'recursively))
+ (byte-recompile-directory user-emacs-directory 0)))
+ (message "Cancelled recompilation of %s" user-emacs-directory))))
+
+
+;; ---------------------- Delete Emacs Home Compiled Files ---------------------
+;; removes all compiled files and deletes the eln directory
+
+(defun cj/delete-emacs-home-compiled-files ()
+ "Delete all compiled files recursively in \\='user-emacs-directory\\='."
+ (interactive)
+ (message "Deleting compiled files under %s. This may take a while." user-emacs-directory)
+ (require 'find-lisp) ;; make sure the package is required
+ (mapc (lambda (path)
+ (when (or (string-suffix-p ".elc" path)
+ (string-suffix-p ".eln" path))
+ (delete-file path)))
+ (find-lisp-find-files user-emacs-directory ""))
+ (message "Done. Compiled files removed under %s" user-emacs-directory))
+
+
+;; ---------------------- List Loaded Packages ---------------------
+;; you don't really need an explanation for this function, do you?
+
+(defvar cj--loaded-file-paths nil
+ "All file paths that are loaded.")
+(defvar cj--loaded-packages-buffer "*loaded-packages*"
+ "Buffer name for data about loaded packages.")
+(defvar cj--loaded-features-buffer "*loaded-features*"
+ "Buffer name for data about loaded features.")
+
+(defun cj/list-loaded-packages()
+ "List all currently loaded packages."
+ (interactive)
+ (with-current-buffer (get-buffer-create cj--loaded-packages-buffer)
+ (erase-buffer)
+ (pop-to-buffer (current-buffer))
+
+ (insert "* Live Packages Exploration\n\n")
+ (insert (format "%s total packages currently loaded\n"
+ (length cj--loaded-file-paths)))
+
+ ;; Extract data from builtin variable `load-history'.
+ (setq cj--loaded-file-paths
+ (seq-filter #'stringp
+ (mapcar #'car load-history)))
+ (cl-sort cj--loaded-file-paths 'string-lessp)
+ (cl-loop for file in cj--loaded-file-paths
+ do (insert "\n" file))
+
+ (goto-char (point-min))))
+
+;; ---------------------------- List Loaded Features ---------------------------
+;; this function's also self-explanatory
+
+(defun cj/list-loaded-features()
+ "List all currently loaded features."
+ (interactive)
+ (with-current-buffer (get-buffer-create cj--loaded-features-buffer)
+ (erase-buffer)
+ (pop-to-buffer (current-buffer))
+
+ (insert (format "\n** %d features currently loaded\n"
+ (length features)))
+
+ (let ((features-vec (apply 'vector features)))
+ (cl-sort features-vec 'string-lessp)
+ (cl-loop for x across features-vec
+ do (insert (format " - %-25s: %s\n" x
+ (locate-library (symbol-name x))))))
+ (goto-char (point-min))))
+
+(provide 'config-utilities)
+;;; config-utilities.el ends here
diff --git a/modules/custom-functions.el b/modules/custom-functions.el
new file mode 100644
index 00000000..fe617ed1
--- /dev/null
+++ b/modules/custom-functions.el
@@ -0,0 +1,506 @@
+;;; custom-functions.el --- My Custom Functions and Keymaps -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+;;These are custom utility functions which I use frequently. They are bound to a
+;;personal keymap with a prefix of "C-;" created at the end of this file.
+
+;;; Code:
+
+(use-package subr-x
+ :ensure nil) ;; built-in
+(use-package expand-region
+ :demand t)
+
+;; ------------------------ Jump To Matching Parentheses -----------------------
+;; shows you the other matching parenthesis by jumping to it.
+
+(defun cj/jump-to-matching-paren ()
+ "If on a parenthesis, jump to it's match. Otherwise, complain."
+ (interactive)
+ (cond ((looking-at "\\s\(\\|\\s\{\\|\\s\[")
+ (forward-list))
+ ((looking-back "\\s\)\\|\\s\}\\|\\s\\]")
+ (backward-list))
+ (t (message "Cursor doesn't follow parenthesis, so there's no match."))))
+
+;; ---------------------------- Join Line Or Region ----------------------------
+;; joins all selected lines and fixes up the whitespace.
+
+(defun cj/join-line-or-region (beg end)
+ "Apply \='join-line\=' over the marked region or join with previous line.
+Region indicated with BEG and END."
+ (interactive "r")
+ ;; in region
+ (if mark-active
+ (let ((beg (region-beginning))
+ (end (copy-marker (region-end))))
+ (goto-char beg)
+ ;; apply join lines until point => end
+ (while (< (point) end)
+ (join-line 1))
+ (goto-char end)
+ (newline)))
+ ;; outside region
+ (join-line)(newline))
+
+;; ------------------------------- Join Paragraph ------------------------------
+;; expands the region to the paragraph, then joins lines and fixes whitespace.
+
+(defun cj/join-paragraph ()
+ "Mark all text in a paragraph then run cj/join-line-or-region."
+ (interactive)
+ (er/mark-paragraph) ;; from package expand region
+ (cj/join-line-or-region)
+ (forward-line))
+
+;; ---------------------- Count Words In Buffer Or Region ----------------------
+;; minibuffer messages the number of words in the buffer (or region if selected).
+
+(defun cj/count-words-buffer-or-region ()
+ "Count the number of words in buffer or region.
+Displays result as a message in the minibuffer and *Messasges* buffer."
+ (interactive)
+ (let ((begin (point-min))
+ (end (point-max))
+ (area_type "the buffer"))
+ (when mark-active
+ (setq begin (region-beginning)
+ end (region-end)
+ area_type "the region"))
+ (message (format "There are %d words in %s." (count-words begin end) area_type))))
+
+;; -------------------------- Duplicate Line Or Region -------------------------
+;; duplicates the current line on a new line below. With "C-u" the new line's
+;; commented. when a region is selected, the whole region is duplicated.
+
+(defun cj/duplicate-line-or-region (&optional comment)
+ "Duplicate the line or region below.
+Comment the duplicated line if prefix argument COMMENT is passed."
+ (interactive "P")
+ (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")))
+ (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)))))))
+
+;; ---------------- Remove Duplicate Lines From Region Or Buffer ---------------
+;; removes all duplicate lines from the region or buffer
+
+(defun cj/remove-duplicate-lines-from-region-or-buffer (start end)
+ "Find duplicate lines in region START to END keeping the first occurrence.
+If no region is selected, operate on the whole buffer."
+ (interactive "*r\nP")
+ (save-excursion
+ (unless (region-active-p)
+ (setq start (point-min) end (point-max)))
+ (setq end (copy-marker end))
+ (while
+ (progn
+ (goto-char start)
+ (re-search-forward "^\\(.*\\)\n\\(\\(.*\n\\)*\\)\\1\n" end t))
+ (replace-match "\\1\n\\2"))))
+
+;; -------------------------- Format Region Or Buffer --------------------------
+;; reindent, untabify, and delete trailing whitespace across region or buffer
+
+(defun cj/format-region-or-buffer ()
+ "Reformat the region or the entire buffer.
+If a region is selected, delete trailing whitespace, then indent and untabify
+the region. If no region is selected, perform the same actions across the
+buffer."
+
+ (interactive)
+ (let (start-pos end-pos)
+ (if (use-region-p)
+ (progn
+ (setq start-pos (region-beginning))
+ (setq end-pos (region-end)))
+ (setq start-pos (point-min))
+ (setq end-pos (point-max)))
+ (save-excursion
+ (delete-trailing-whitespace start-pos end-pos)
+ (indent-region start-pos end-pos nil)
+ (untabify start-pos end-pos))))
+
+;; --------------------------- Arrayify / Unarrayify ---------------------------
+;; unquoted text on newlines to quoted comma separated strings (and vice-versa).
+
+(defun cj/arrayify (start end quote)
+ "Turn unquoted text on newlines into quoted comma-separated strings.
+START and END indicate the region selected.
+QUOTE is the characters used for quotations (i.e, \=' or \")"
+ (interactive "r\nMQuote: ")
+ (let ((insertion
+ (mapconcat
+ (lambda (x) (format "%s%s%s" quote x quote))
+ (split-string (buffer-substring start end)) ", ")))
+ (delete-region start end)
+ (insert insertion)))
+
+(defun cj/unarrayify (start end)
+ "Turn quoted comma-separated strings into unquoted text on newlines.
+START and END indicate the region selected."
+ (interactive "r")
+ (let ((insertion
+ (mapconcat
+ (lambda (x) (replace-regexp-in-string "[\"']" "" x))
+ (split-string (buffer-substring start end) ", ") "\n")))
+ (delete-region start end)
+ (insert insertion)))
+
+;; ----------------------- Comma Separated Text To Lines -----------------------
+;; like arrayify, just without the quotes
+
+(defun cj/comma-separated-text-to-lines ()
+ "Breaks up text between commas in a region and places each text on its own line."
+ (interactive)
+ (if (not (region-active-p))
+ (error "No region selected"))
+
+ (let ((beg (region-beginning))
+ (end (region-end))
+ (text (buffer-substring-no-properties (region-beginning) (region-end))))
+ (with-temp-buffer
+ (insert text)
+ (goto-char (point-min))
+ (while (search-forward "," nil t)
+ (replace-match "\n" nil t))
+ (delete-trailing-whitespace)
+ (setq text (buffer-string)))
+
+ (delete-region beg end)
+ (goto-char beg)
+ (insert text)))
+
+;; ----------------------- Alphabetize And Replace Region ----------------------
+;; sorts selected words into alphabetical order, then replaces the region.
+
+(defun cj/alphabetize-and-replace-region ()
+ "Alphabetize strings (words/tokens) in region replacing the original region.
+The result will be comma separated."
+ (interactive)
+ (let ((start (region-beginning))
+ (end (region-end))
+ (string (buffer-substring-no-properties (region-beginning) (region-end))))
+ (delete-region start end)
+ (goto-char start)
+ (insert
+ (mapconcat #'identity
+ (sort (split-string string "[[:space:],]+" t)
+ #'string-lessp)
+ ", "))))
+
+;; --------------------- Wrap Region As Markdown Code Block --------------------
+;; wrap the selection in triple backslash and indicate the language for markdown
+
+(defun cj/wrap-region-as-code-span (start end)
+ "Wraps the region between START and END with triple backticks and descriptor.
+Triple backicks are often used to indicate a code-span block in markdown.
+User is prompted for the optional descriptor."
+ (interactive "r")
+ (let ((lang (read-string "Descriptor (e.g., code, bash, python): ")))
+ (save-excursion
+ (goto-char end)
+ (unless (bolp) (insert "\n"))
+ (insert "```\n")
+ (goto-char start)
+ (insert (concat "```" lang "\n")))))
+
+;; -------------------- Append To Lines In Region Or Buffer --------------------
+;; append characters to the end of all lines in the region or the whole buffer.
+
+(defun cj/append-to-lines-in-region-or-buffer (str)
+ "Prompt for STR and append it to the end of each line in region or buffer."
+ (interactive "sEnter string to append: ")
+ (let ((start-pos (if (use-region-p)
+ (region-beginning)
+ (point-min)))
+ (end-pos (if (use-region-p)
+ (region-end)
+ (point-max))))
+ (save-excursion
+ (goto-char start-pos)
+ (while (< (point) end-pos)
+ (move-end-of-line 1)
+ (insert str)
+ (forward-line 1)))))
+
+;; ------------------------------ Hyphenate Region -----------------------------
+;; hyphenates any empty space in a region; complains if there's no Region
+
+(defun cj/hyphenate-region (start end)
+ "Hyphenate all continuous whitespace in the region.
+START and END represent the region selected."
+ (interactive "*r")
+ (if (use-region-p)
+ (save-excursion
+ (save-restriction
+ (narrow-to-region start end)
+ (goto-char (point-min))
+ (while (re-search-forward "[ \t\n\r]+" nil t)
+ (replace-match "-"))))
+ (message "No region; nothing to hyphenate.")))
+
+;; ----------------------------- Title Case Region -----------------------------
+;; a literate version of capitalize region for prose text.
+
+(defun cj/title-case-region ()
+ "Capitalize the region in title case format.
+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."
+ (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 '(?: ?! ??))
+ ;; Don't capitalize characters directly after these. e.g.
+ ;; "Foo-bar" or "Foo\bar" or "Foo's".
+ (chars-separator '(?\\ ?- ?' ?.))
+
+ (word-chars "[:alnum:]")
+ (word-skip
+ (list "a" "an" "and" "as" "at" "but" "by"
+ "for" "if" "in" "is" "nor" "of"
+ "on" "or" "so" "the" "to" "yet"))
+ (is-first t))
+ (cond
+ ((region-active-p)
+ (setq beg (region-beginning))
+ (setq end (region-end)))
+ (t
+ (setq beg (line-beginning-position))
+ (setq end (line-end-position))))
+ (save-excursion
+ ;; work on uppercased text (e.g., headlines) by downcasing first
+ (downcase-region beg end)
+
+ (goto-char beg)
+
+ (while (< (point) end)
+ (setq prev-word-end (point))
+ (skip-chars-forward (concat "^" word-chars) end)
+ (let ((word-end
+ (save-excursion
+ (skip-chars-forward word-chars end)
+ (point))))
+
+ (unless (memq (char-before (point)) chars-separator)
+ (let* ((c-orig (char-to-string (char-after (point))))
+ (c-up (capitalize c-orig)))
+ (unless (string-equal c-orig c-up)
+ (let ((word (buffer-substring-no-properties (point) word-end)))
+ (when
+ (or
+ ;; Always allow capitalization.
+ is-first
+ ;; If it's not a skip word, allow.
+ (not (member word word-skip))
+ ;; Check the beginning of the previous word doesn't reset first.
+ (save-excursion
+ (and
+ (not (zerop (skip-chars-backward "[:blank:]" prev-word-end)))
+ (memq (char-before (point)) chars-skip-reset))))
+ (delete-region (point) (1+ (point)))
+ (insert c-up))))))
+ (goto-char word-end)
+ (setq is-first nil))))))
+;; replace the capitalize-region keybinding to call title-case
+(global-set-key [remap capitalize-region] 'cj/title-case-region)
+
+;; --------------------------- Buffer Strip Control M --------------------------
+;; remove windows carriage return control characters from the buffer
+
+(defun buffer-strip-ctrl-m ()
+ "Remove ^M from the current buffer."
+ (interactive)
+ (save-excursion
+ (goto-char (point-min))
+ (while (search-forward "^M" nil t)
+ (replace-match "" nil t))))
+
+;; ------------------------------ Insert Date Time -----------------------------
+;; insert a sortable or a readable datestamp or timestamp
+
+(defvar readable-date-time-format "%A, %B %d, %Y at %I:%M:%S %p %Z "
+ "Format of date to insert with `insert-readable-date-time' func.
+See help of `format-time-string' for possible replacements")
+
+(defun cj/insert-readable-date-time ()
+ "Insert the current date and time into current buffer.
+Uses `readable-date-time-format' for the formatting the date/time."
+ (interactive)
+ (insert (format-time-string readable-date-time-format (current-time))))
+
+(defvar sortable-date-time-format "%Y-%m-%d %a @ %H:%M:%S %z "
+ "Format of date to insert with `insert-current-date-time' func.
+See help of `format-time-string' for possible replacements")
+
+(defun cj/insert-sortable-date-time ()
+ "Insert the current date and time into current buffer.
+Uses `sortable-date-time-format' for the formatting the date/time."
+ (interactive)
+ (insert (format-time-string sortable-date-time-format (current-time))))
+
+(defvar sortable-time-format "%I:%M:%S %p %Z "
+ "Time format to insert with `insert-current-time' func.
+See help of `format-time-string' for possible replacements")
+
+(defun cj/insert-sortable-time ()
+ "Insert the current time into current buffer.
+Uses `sortable-time-format' for the formatting the date/time."
+ (interactive)
+ (insert (format-time-string sortable-time-format (current-time))))
+
+(defvar sortable-date-format "%Y-%m-%d "
+ "Time format to insert with `insert-current-time' func.
+See help of `format-time-string' for possible replacements")
+
+(defun cj/insert-sortable-date ()
+ "Insert the current time into current buffer.
+Uses `sortable-time-format' for the formatting the date/time."
+ (interactive)
+ (insert (format-time-string sortable-date-format (current-time))))
+
+;; -------------------------- Copy Link To Source File -------------------------
+;; find the source file for the current buffer and place it's URL in the clipboard
+
+(defun cj/copy-link-to-source-file ()
+ "Copy the full file:// path of the underlying source file to the kill ring."
+ (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))))
+
+;; ------------------------- Buffer And File Operations ------------------------
+;; move, rename, or delete the underlying source file for the current buffer.
+
+;; MOVE BUFFER + FILE
+(defun cj/move-buffer-and-file (dir)
+ "Move both current buffer and the file it visits to DIR."
+ (interactive "DMove buffer and file (to new directory): ")
+ (let* ((name (buffer-name))
+ (filename (buffer-file-name))
+ (dir
+ (if (string-match dir "\\(?:/\\|\\\\)$")
+ (substring dir 0 -1) dir))
+ (newname (concat dir "/" name)))
+ (if (not filename)
+ (message "Buffer '%s' is not visiting a file!" name)
+ (progn (copy-file filename newname 1) (delete-file filename)
+ (set-visited-file-name newname) (set-buffer-modified-p nil) t))))
+(global-set-key (kbd "C-x x m") 'cj/move-buffer-and-file)
+
+;; RENAME BUFFER + FILE
+(defun cj/rename-buffer-and-file (new-name)
+ "Rename both current buffer and the file it visits to NEW-NAME."
+ (interactive
+ (list (read-string "Rename buffer and file (to new name): "
+ (file-name-nondirectory (buffer-file-name)))))
+ (let ((name (buffer-name))
+ (filename (buffer-file-name)))
+ (if (not filename)
+ (message "Buffer '%s' is not visiting a file!" name)
+ (if (get-buffer new-name)
+ (message "A buffer named '%s' already exists!" new-name)
+ (progn
+ (rename-file filename new-name 1)
+ (rename-buffer new-name)
+ (set-visited-file-name new-name)
+ (set-buffer-modified-p nil))))))
+(global-set-key (kbd "C-x x r") 'cj/rename-buffer-and-file)
+
+;; DELETE BUFFER + FILE
+(defun cj/delete-buffer-and-file ()
+ "Kill the current buffer and delete the file it visits."
+ (interactive)
+ (let ((filename (buffer-file-name)))
+ (when filename
+ (if (vc-backend filename)
+ (vc-delete-file filename)
+ (progn
+ (delete-file filename t)
+ (message "Deleted file %s" filename)
+ (kill-buffer))))))
+(global-set-key (kbd "C-x x d") 'cj/delete-buffer-and-file)
+
+;; ------------------------------- Ordinal Suffix ------------------------------
+;; add the proper ordinal to a number (e.g., 1st, 2nd, 3rd, 4th).
+;; Stolen from `diary.el' (`diary-ordinal-suffix').
+
+(defun ordinal-suffix (n)
+ "Ordinal suffix for N. That is, `st', `nd', `rd', or `th', as appropriate."
+ (if (or (memq (% n 100) '(11 12 13)) (< 3 (% n 10)))
+ "th"
+ (aref ["th" "st" "nd" "rd"] (% n 10))))
+
+;; -------------------------------- Align-Regexp -------------------------------
+;; the built-in align regexp shouldn't use tabs
+
+(defadvice align-regexp (around align-regexp-with-spaces activate)
+ "Avoid tabs when aligning text."
+ (let ((indent-tabs-mode nil))
+ ad-do-it))
+
+;; ------------------------------ Personal Keymap ------------------------------
+;; a keymap to use the above functions. prefix key: "C-;"
+
+(global-unset-key (kbd "C-;"))
+(defvar personal-keymap
+ (let ((map (make-sparse-keymap)))
+ ;; un/arrayify
+ (define-key map "a" 'cj/arrayify)
+ (define-key map "A" 'cj/unarrayify)
+ ;; de/duplicate lines
+ (define-key map "d" 'cj/duplicate-line-or-region)
+ (define-key map "D" 'cj/remove-duplicate-lines-from-region-or-buffer)
+
+ (define-key map ")" #'cj/jump-to-matching-paren)
+ (define-key map "-" #'cj/hyphenate-whitespace-in-region)
+ (define-key map "p" 'cj/append-to-lines-in-region-or-buffer)
+ (define-key map "1" 'cj/alphabetize-and-replace-regibon)
+ (define-key map "C" 'display-fill-column-indicator-mode)
+ (define-key map "w" 'cj/wrap-region-as-code-span)
+ (define-key map "f" 'cj/format-region-or-buffer)
+ (define-key map "h" 'cj/hyphenate-region)
+ (define-key map "j" 'cj/join-line-or-region)
+ (define-key map "J" 'cj/join-paragraph)
+ (define-key map "r" 'align-regexp)
+ (define-key map "l" 'downcase-dwim)
+ (define-key map "u" 'cj/title-case-region)
+ (define-key map "U" 'upcase-region)
+ (define-key map "#" 'cj/count-words-buffer-or-region)
+ map)
+ "My personal key map.")
+(global-set-key (kbd "C-;") personal-keymap)
+
+;; timestamp insertion
+(global-set-key (kbd "C-; i h") 'cj/insert-readable-date-time)
+(global-set-key (kbd "C-; i s") 'cj/insert-sortable-date-time)
+(global-set-key (kbd "C-; i t") 'cj/insert-sortable-time)
+(global-set-key (kbd "C-; i d") 'cj/insert-sortable-date)
+;; buffer and file operations
+(global-set-key (kbd "C-; b r") 'cj/rename-buffer-and-file)
+(global-set-key (kbd "C-; b d") 'cj/delete-buffer-and-file)
+(global-set-key (kbd "C-; b m") 'cj/move-buffer-and-file)
+;; copy link to source file
+(global-set-key (kbd "C-; b l") 'cj/copy-link-to-source-file)
+
+(provide 'custom-functions)
+;;; custom-functions.el ends here.
diff --git a/modules/dashboard-config.el b/modules/dashboard-config.el
new file mode 100644
index 00000000..6340c32b
--- /dev/null
+++ b/modules/dashboard-config.el
@@ -0,0 +1,141 @@
+;;; dashboard-config.el --- Dashboard Configuration -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;; Note:
+;; Nerd-Icons Cheat Sheet: https://www.nerdfonts.com/cheat-sheet
+
+;;; Code:
+
+;; ------------------------ Dashboard Bookmarks Override -----------------------
+;; overrides the bookmark insertion from the dashboard package to provide an
+;; option that only shows the bookmark name, avoiding the path. Paths are often
+;; too long and the truncation options aren't aesthetically pleasing. Should be
+;; accompanied by the setting (setq dashboard-bookmarks-show-path nil) in
+;; config.
+
+(defcustom dashboard-bookmarks-item-format "%s"
+ "Format to use when showing the base of the file name."
+ :type 'string
+ :group 'dashboard)
+
+(defun dashboard-insert-bookmarks (list-size)
+ "Add the list of LIST-SIZE items of bookmarks."
+ (require 'bookmark)
+ (dashboard-insert-section
+ "Bookmarks:"
+ (dashboard-subseq (bookmark-all-names) list-size)
+ list-size
+ 'bookmarks
+ (dashboard-get-shortcut 'bookmarks)
+ `(lambda (&rest _) (bookmark-jump ,el))
+ (if-let* ((filename el)
+ (path (bookmark-get-filename el))
+ (path-shorten (dashboard-shorten-path path 'bookmarks)))
+ (cl-case dashboard-bookmarks-show-path
+ (`align
+ (unless dashboard--bookmarks-cache-item-format
+ (let* ((len-align (dashboard--align-length-by-type 'bookmarks))
+ (new-fmt (dashboard--generate-align-format
+ dashboard-bookmarks-item-format len-align)))
+ (setq dashboard--bookmarks-cache-item-format new-fmt)))
+ (format dashboard--bookmarks-cache-item-format filename path-shorten))
+ (`nil filename)
+ (t (format dashboard-bookmarks-item-format filename path-shorten)))
+ el)))
+
+;; ----------------------------- Display Dashboard -----------------------------
+;; convenience function to redisplay dashboard
+
+(defun cj/display-dashboard ()
+ "Display dashboard, create if it doesn't exist."
+ (interactive)
+ (dired-sidebar-hide-sidebar) ;; hide dired-sidebar if displaying
+ (get-buffer-create "*dashboard*")
+ (pop-to-buffer "*dashboard*")
+ (delete-other-windows))
+(global-set-key (kbd "<f4>") 'cj/display-dashboard)
+
+;; --------------------------------- Dashboard ---------------------------------
+;; a useful startup screen for Emacs
+
+(use-package dashboard
+ :demand t ;; needed to startup quickly
+ :custom
+ (dashboard-projects-backend 'projectile)
+
+ (dashboard-item-generators
+ '((projects . dashboard-insert-projects)
+ (bookmarks . dashboard-insert-bookmarks)))
+
+ (dashboard-items '((projects . 5)
+ (bookmarks . 7)))
+
+ (dashboard-startupify-list
+ '(dashboard-insert-banner
+ dashboard-insert-banner-title
+ dashboard-insert-newline
+ dashboard-insert-newline
+ dashboard-insert-navigator
+ dashboard-insert-init-info
+ dashboard-insert-newline
+ dashboard-insert-newline
+ dashboard-insert-items
+ dashboard-insert-newline))
+ :config
+
+ ;; == general
+ (dashboard-setup-startup-hook) ;; run dashboard post emacs init
+ (setq initial-buffer-choice (lambda () (get-buffer "*dashboard*"))) ;; display dashboard on startup
+ (setq dashboard-display-icons-p t) ;; display icons on both GUI and terminal
+ (setq dashboard-icon-type 'nerd-icons) ;; use `nerd-icons' package
+ (setq dashboard-center-content t) ;; horizontally center dashboard content
+ (setq dashboard-bookmarks-show-path nil) ;; don't show paths in bookmarks
+ (setq dashboard-set-footer nil) ;; don't show footer and quotes
+
+ ;; == banner
+ (custom-set-faces '(dashboard-banner-logo-title
+ ((t(:slant oblique
+ :height 170
+ :family "Merriweather")))))
+
+ (setq dashboard-startup-banner (concat user-emacs-directory "assets/M-x_butterfly.png"))
+ (setq dashboard-banner-logo-title "Emacs: The Editor That Saves Your Soul")
+
+ ;; == navigation
+ (setq dashboard-set-navigator t)
+ (setq dashboard-navigator-buttons
+ `(((,(nerd-icons-faicon "nf-fa-envelope")
+ "Email" "Mu4e Email Client"
+ (lambda (&rest _) (mu4e)))
+
+ (,(nerd-icons-faicon "nf-fae-book_open_o")
+ "Ebooks" "Calibre Ebook Reader"
+ (lambda (&rest _) (calibredb)))
+
+ (,(nerd-icons-mdicon "nf-md-school")
+ "Flashcards" "Org-Drill"
+ (lambda (&rest _) (cj/drill-start)))
+
+ (,(nerd-icons-faicon "nf-fa-rss_square")
+ "Feeds" "Elfeed Feed Reader"
+ (lambda (&rest _) (elfeed-dashboard)))
+
+ (,(nerd-icons-faicon "nf-fa-comments")
+ "IRC" "Emacs Relay Chat"
+ (lambda (&rest _) (cj/erc-start-or-switch)))
+
+ (,(nerd-icons-faicon "nf-fae-telegram")
+ "Telegram" "Telega Chat Client"
+ (lambda (&rest _) (telega)))
+
+ (,(nerd-icons-faicon "nf-fa-folder_o")
+ "Files" "Dirvish File Manager"
+ (lambda (&rest _) (dirvish user-home-dir))))))
+
+ ;; == content
+ (setq dashboard-show-shortcuts nil) ;; don't show dashboard item abbreviations
+ ) ;; end use-package dashboard
+
+(provide 'dashboard-config)
+;;; dashboard-config.el ends here.
diff --git a/modules/diff-config.el b/modules/diff-config.el
new file mode 100644
index 00000000..aaef8dbf
--- /dev/null
+++ b/modules/diff-config.el
@@ -0,0 +1,53 @@
+;;; ediff-config.el --- diff Configuration -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; highly useful setup for configuring ediff
+;; https://oremacs.com/2015/01/17/setting-up-ediff/
+
+;;; Code:
+
+;; -------------------------------- Csetq Macro --------------------------------
+;; a macro that allows a setq for custom variables. uses the variable's set
+;; property without writing customizations to emacs init.
+
+(defmacro csetq (variable value)
+ `(funcall (or (get ',variable 'custom-set)
+ 'set-default)
+ ',variable ,value))
+
+;; ------------------------------- Ediff Settings ------------------------------
+;; ediff configuration. note the dired-ediff-files function in dirvish.
+
+;; lose the control panel
+(csetq ediff-window-setup-function 'ediff-setup-windows-plain)
+
+;; only split horizontally
+(csetq ediff-split-window-function 'split-window-horizontally)
+
+;; ignore whitespace in diffs
+(csetq ediff-diff-options "-w")
+
+;; only highlight the current diff:
+(setq-default ediff-highlight-all-diffs 'nil)
+
+;; use j and k for next and previous diffs
+(defun cj/ediff-hook ()
+ (ediff-setup-keymap)
+ (define-key ediff-mode-map "j" 'ediff-next-difference)
+ (define-key ediff-mode-map "k" 'ediff-previous-difference))
+(add-hook 'ediff-mode-hook 'cj/ediff-hook)
+
+;; restore the window setup after quitting
+(winner-mode)
+(add-hook 'ediff-after-quit-hook-internal 'winner-undo)
+
+;; ----------------------------------- Ztree -----------------------------------
+;; diff two directories
+
+(use-package ztree
+ :defer .5
+ :bind
+ ("C-c D" . ztree-diff))
+
+(provide 'diff-config)
+;;; diff-config.el ends here.
diff --git a/modules/dirvish-config.el b/modules/dirvish-config.el
new file mode 100644
index 00000000..e089f010
--- /dev/null
+++ b/modules/dirvish-config.el
@@ -0,0 +1,183 @@
+;;; dirvish-config.el --- Configuration for Tramp and the Dired File Manager -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;; DIRVISH NOTES:
+;; access the quick access directories by pressing 'g' (for "go")
+
+;;; Code:
+
+;; ----------------------------------- Dired -----------------------------------
+
+(use-package dired
+ :ensure nil ;; built-in
+ :defer .5
+ :bind
+ (:map dired-mode-map
+ ([remap dired-summary] . which-key-show-major-mode)
+ ("E" . wdired-change-to-wdired-mode) ;; edit names/properties in buffer
+ ("e" . cj/dired-ediff-files)) ;; ediff files
+ :custom
+ (dired-use-ls-dired nil) ;; non GNU FreeBSD doesn't support a "--dired" switch
+ :config
+ (setq dired-listing-switches "-hl --almost-all --group-directories-first")
+ (setq dired-dwim-target t)
+ (setq dired-clean-up-buffers-too t) ;; offer to kill buffers associated deleted files and dirs
+ (setq dired-clean-confirm-killing-deleted-buffers t) ;; don't ask whether to kill buffers associated with deleted files.
+ (setq dired-kill-when-opening-new-dired-buffer t) ;; stop littering buffers while navigating a directory tree
+ (setq dired-recursive-copies (quote always)) ;; “always” means no asking
+ (setq dired-recursive-deletes (quote top))) ;; “top” means ask once
+
+;; (add-hook 'dired-mode-hook 'auto-revert-mode) ;; auto revert dired when files change
+
+
+;; ------------------------ Dired Mark All Visible Files -----------------------
+;; convenience function to mark all visible files in dired
+
+(defun cj/dired-mark-all-visible-files ()
+ "Mark all visible files for deletion in Dired mode."
+ (interactive)
+ (save-excursion
+ (goto-char (point-min))
+ (while (not (eobp))
+ (if (not (looking-at "^. d"))
+ (dired-mark 1))
+ (forward-line 1))))
+
+;; ---------------------------------- Dirvish ----------------------------------
+
+(use-package dirvish
+ :defer .5
+ :after dired
+ :custom
+ (dirvish-quick-access-entries
+ '(("h" "~/" "home")
+ ("rsb" "/sshx:cjennings@wolf.usbx.me:/home/cjennings/" "seedbox")
+ ("rcj" "/sshx:cjennings@cjennings.net:~" "cjennings.net")
+ ("dn" "~/downloads/" "downloads")
+ ("lt" "~/.local/share/Trash" "trash")
+ ("cj" "~/code/cjennings.net" "cjennings.net")
+ ("co" "~/code" "code")
+ ("df" "~/.dotfiles/" "dotfiles")
+ ("dr" "~/sync/org/drill/" "org drill files")
+ ("dt" "~/downloads/torrents/complete/" "torrents")
+ ("dx" "~/documents/" "documents")
+ ("gc" "~/code/golangcourse" "golang course")
+ ("mp" "~/sync/playlists/" "playlists")
+ ("mv" "~/magic/video/" "magic/video")
+ ("mx" "~/music/" "music")
+ ("my" "~/magic/youtube/" "magic/youtube")
+ ("or" "~/sync/org/" "sync")
+ ("pr" "~/projects/" "projects")
+ ("ps" "~/pictures/screenshots/" "screenshots")
+ ("pw" "~/pictures/wallpaper" "wallpaper")
+ ("px" "~/pictures/" "pictures")
+ ("tg" "~/sync/org/text.games" "text games")
+ ("vx" "~/videos/" "videos")))
+ (dirvish-attributes '(vscode-icon file-size))
+ (dirvish-override-dired-mode t)
+ (dirvish-preview-dispatchers '(image gif video audio epub pdf archive))
+ :hook (dirvish-setup . dirvish-emerge-mode)
+ :config
+ (setq dirvish-use-mode-line nil)
+ (setq dirvish-use-header-line nil)
+ :bind
+ (("C-x d" . dirvish)
+ ("C-x C-d" . dirvish)
+ ("C-x D" . dirvish-override-dired-mode)
+ ("<f11>" . dirvish-side)
+
+ :map dirvish-mode-map ; note: Dirvish inherits `dired-mode-map'
+ ("g" . dirvish-quick-access)
+ ("G" . revert-buffer)
+ ("bg" . (lambda () (interactive) ; set background image
+ (shell-command (concat "nitrogen --save --set-zoom-fill "
+ (dired-file-name-at-point) " >>/dev/null 2>&1" ))))
+ ("Z" . (lambda () (interactive) (cj/open-file-with-command "zathura")))
+ ("p" . (lambda () (interactive) (cj/open-file-with-command "gimp")))
+ ("<left>" . dired-up-directory)
+ ("<right>" . dired-find-file)
+ ("f" . dirvish-file-info-menu)
+ ("y" . dirvish-yank-menu)
+ ("N" . dirvish-narrow)
+ ("M" . cj/dired-mark-all-visible-files)
+ ("s" . dirvish-quicksort) ; remapped `dired-sort-toggle-or-edit'
+ ("v" . dirvish-vc-menu) ; remapped `dired-view-file'
+ ("TAB" . dirvish-subtree-toggle)
+ ("C-." . dirvish-history-go-forward)
+ ("C-," . dirvish-history-go-backward)
+ ("M-l" . dirvish-ls-switches-menu)
+ ("M-m" . dirvish-mark-menu)
+ ("M-t" . dirvish-layout-toggle)
+ ("M-s" . dirvish-setup-menu)
+ ("M-e" . dirvish-emerge-menu)))
+
+;; -------------------------------- VSCode-Icons -------------------------------
+
+(use-package vscode-icon
+ :defer .5
+ :commands (vscode-icon-for-file))
+
+;; -------------------------------- Dired Rsync --------------------------------
+
+(use-package dired-rsync
+ :after dired
+ :bind (:map dired-mode-map
+ ("r" . dired-rsync)))
+
+;; ---------------------------- Dired Hide Dotfiles ----------------------------
+
+(use-package dired-hide-dotfiles
+ :after dired
+ :bind
+ (:map dired-mode-map
+ ("h" . dired-hide-dotfiles-mode)))
+
+;; ------------------------------- Dired Sidebar -------------------------------
+
+(use-package dired-sidebar
+ :after (dired projectile)
+ :bind (("<f11>" . dired-sidebar-toggle-sidebar))
+ :commands (dired-sidebar-toggle-sidebar)
+ :init
+ (add-hook 'dired-sidebar-mode-hook
+ (lambda ()
+ (unless (file-remote-p default-directory)
+ (auto-revert-mode))))
+ :config
+ (push 'toggle-window-split dired-sidebar-toggle-hidden-commands) ; don't allow splitting dired window when it's showing
+ (push 'rotate-windows dired-sidebar-toggle-hidden-commands) ; don't allow rotating windows when sidebar is showing
+ (setq dired-sidebar-subtree-line-prefix " ") ; two spaces give simple and aesthetic indentation
+ (setq dired-sidebar-no-delete-other-windows t) ; don't close when calling 'delete other windows'
+ (setq dired-sidebar-theme 'vscode) ; fancy icons, please
+ (setq dired-sidebar-use-custom-font 'nil) ; keep the same font as the rest of Emacs
+ (setq dired-sidebar-delay-auto-revert-updates 'nil) ; don't delay auto-reverting
+ (setq dired-sidebar-pop-to-sidebar-on-toggle-open 'nil)) ; don't jump to sidebar when it's toggled on
+
+;; ----------------------------- Dired Ediff Files -----------------------------
+;; mark two files within dired, then ediff them within Emacs
+
+(defun cj/dired-ediff-files ()
+ "Ediff two selected files within Dired."
+ (interactive)
+ (let ((files (dired-get-marked-files))
+ (wnd (current-window-configuration)))
+ (if (<= (length files) 2)
+ (let ((file1 (car files))
+ (file2 (if (cdr files)
+ (cadr files)
+ (read-file-name
+ "file: "
+ (dired-dwim-target-directory)))))
+ (if (file-newer-than-file-p file1 file2)
+ (ediff-files file2 file1)
+ (ediff-files file1 file2))
+ (add-hook 'ediff-after-quit-hook-internal
+ (lambda ()
+ (setq ediff-after-quit-hook-internal nil)
+ (set-window-configuration wnd))))
+ (error "No more than 2 files should be marked"))))
+
+(provide 'dirvish-config)
+;;; dirvish-config.el ends here
diff --git a/modules/elfeed-config.el b/modules/elfeed-config.el
new file mode 100644
index 00000000..37a0e12f
--- /dev/null
+++ b/modules/elfeed-config.el
@@ -0,0 +1,150 @@
+;;; elfeed-config --- Settings and Enhancements to the Elfeed RSS Feed Reader -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;; ------------------------------- Elfeed Config -------------------------------
+
+(use-package elfeed
+ :bind
+ (:map elfeed-show-mode-map
+ ("w" . eww-open-in-new-buffer))
+ (:map elfeed-search-mode-map
+ ("w" . cj/elfeed-eww-open) ;; opens in eww
+ ("b" . cj/elfeed-browser-open) ;; opens in external browser
+ ("d" . cj/elfeed-youtube-dl) ;; async download with yt-dlp and tsp
+ ("p" . cj/play-with-mpv) ;; async play with mpv
+ ("R" . cj/elfeed-mark-all-as-read) ;; capital marks all as read, since upper case marks one as read
+ ("U" . cj/elfeed-mark-all-as-unread)) ;; capital marks all as unread, since lower case marks one as unread
+ :config
+ (setq elfeed-db-directory "~/sync/org/ElfeedDB")
+ (setq-default elfeed-search-title-max-width 150)
+ (setq-default elfeed-search-title-min-width 80)
+ (setq-default elfeed-search-filter "+mustread +unread"))
+
+;; ---------------------------- Elfeed Org Feed List ---------------------------
+
+(use-package elfeed-org
+ :defer .5
+ :after elfeed
+ :config
+ (setq rmh-elfeed-org-files (list (concat sync-dir "elfeed-feeds.org")))
+ (elfeed-org))
+
+;; ------------------------------ Elfeed Dashboard -----------------------------
+
+(use-package elfeed-dashboard
+ :defer .5
+ :bind
+ ("M-R" . elfeed-dashboard)
+ :config
+ (setq elfeed-dashboard-file (concat user-emacs-directory "elfeed-dashboard.org"))
+ ;; update feed counts on elfeed-quit
+ (advice-add 'elfeed-search-quit-window :after #'elfeed-dashboard-update-links))
+
+;; ------------------------------ Elfeed Functions -----------------------------
+
+(defun cj/elfeed-open ()
+ "Open Elfeed, update all feeds, then move to the first entry."
+ (interactive)
+ (elfeed)
+ (elfeed-update)
+ (elfeed-search-update--force))
+
+;; -------------------------- Elfeed Filter Functions --------------------------
+
+(defun cj/elfeed-mark-all-as-read ()
+ "Temove the 'unread' tag from all elfeed entries visible in the elfeed search buffer."
+ (interactive)
+ (mark-whole-buffer)
+ (elfeed-search-untag-all-unread))
+
+(defun cj/elfeed-mark-all-as-unread ()
+ "Add the 'unread' tag from all elfeed entries visible in the elfeed search buffer."
+ (interactive)
+ (mark-whole-buffer)
+ (elfeed-search-tag-all 'unread))
+
+(defun cj/elfeed-set-filter-and-update(filterstring)
+ "Set the Elfeed filter to 'FILTERSTRING' and update the the buffer."
+ (interactive "P")
+ (setq elfeed-search-filter filterstring)
+ (elfeed-search-update--force)
+ (elfeed-search-first-entry))
+
+;; -------------------------- Elfeed Browser Functions -------------------------
+
+(defun cj/elfeed-eww-open (&optional use-generic-p)
+ "Open currently selected entry with EWW."
+ (interactive "P")
+ (let ((entries (elfeed-search-selected)))
+ (cl-loop for entry in entries
+ do (elfeed-untag entry 'unread)
+ when (elfeed-entry-link entry)
+ do
+ (add-hook 'eww-after-render-hook #'cj/eww-readable-nonce)
+ (eww-browse-url it))
+ (mapc #'elfeed-search-update-entry entries)
+ (unless (use-region-p) (forward-line))))
+
+;; hook for cj/elfeed-eww-open to open entry in eww readable mode
+;; https://emacs.stackexchange.com/questions/36284/how-to-open-eww-in-readable-mode/47757
+(defun cj/eww-readable-nonce ()
+ "Once-off call to eww-readable after EWW is done rendering."
+ (unwind-protect
+ (progn
+ (eww-readable)
+ (goto-char (point-min)))
+ (remove-hook 'eww-after-render-hook #'cj/eww-readable-nonce)))
+
+(defun cj/elfeed-browser-open (&optional use-generic-p)
+ "Open the currently selected entry with default browser."
+ (interactive "P")
+ (let ((entries (elfeed-search-selected)))
+ (cl-loop for entry in entries
+ do (elfeed-untag entry 'unread)
+ when (elfeed-entry-link entry)
+ do (browse-url-default-browser it))
+ (mapc #'elfeed-search-update-entry entries)
+ (unless (use-region-p) (forward-line))))
+
+;; --------------------- Elfeed Plan And Download Functions --------------------
+
+(defun cj/elfeed-youtube-dl (&optional use-generic-p)
+ "Youtube-DL link."
+ (interactive "P")
+ (let ((entries (elfeed-search-selected)))
+ (cl-loop for entry in entries
+ do (elfeed-untag entry 'unread)
+ when (elfeed-entry-link entry)
+ do (cj/yt-dl-it it))
+ (mapc #'elfeed-search-update-entry entries)
+ (unless (use-region-p) (forward-line))))
+
+(defun cj/yt-dl-it (url)
+ "Downloads the URL in an async shell."
+ (let ((default-directory "~/videos"))
+ (save-window-excursion
+ (async-shell-command (format "tsp yt-dlp --add-metadata -ic -o '%%(channel)s-%%(title)s.%%(ext)s' '%s'" url)))))
+
+(defun cj/play-with-mpv (&optional use-generic-p)
+ "MPV link."
+ (interactive "P")
+ (let ((entries (elfeed-search-selected)))
+ (cl-loop for entry in entries
+ do (elfeed-untag entry 'unread)
+ when (elfeed-entry-link entry)
+ do (cj/mpv-play-it it))
+ (mapc #'elfeed-search-update-entry entries)
+ (unless (use-region-p) (forward-line))))
+
+(defun cj/mpv-play-it (url)
+ "Play the URL with mpv in an async shell."
+ (async-shell-command (format "mpv '%s'" url)))
+
+
+
+(provide 'elfeed-config)
+;;; elfeed-config.el ends here
diff --git a/modules/epa-config.el b/modules/epa-config.el
new file mode 100644
index 00000000..8d9d8f13
--- /dev/null
+++ b/modules/epa-config.el
@@ -0,0 +1,30 @@
+;;; epa-config.el --- EasyPG Configuration -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+;; -------------------------------- Auth Sources -------------------------------
+;; auth sources settings
+
+(use-package auth-source
+ :ensure nil ;; built in
+ :defer .5
+ :config
+ (setq auth-sources '((:source "~/.authinfo.gpg")))
+ (setenv "GPG_AGENT_INFO" nil) ;; emacs use internal prompt, not gpg agent
+ (setq auth-source-debug t)) ;; echo debug info to Messages
+
+;; ----------------------------- Easy PG Assistant -----------------------------
+;; Key management, cryptographic operations on regions and files, dired
+;; integration, and automatic encryption/decryption of *.gpg files.
+
+(use-package epa
+ :ensure nil ;; built-in
+ :defer .5
+ :config
+ (setq epg-gpg-program "gpg2")) ;; force use gpg2 (not gpg v.1)
+
+(provide 'epa-config)
+;;; epa-config.el ends here.
diff --git a/modules/eradio-config.el b/modules/eradio-config.el
new file mode 100644
index 00000000..8dee7a23
--- /dev/null
+++ b/modules/eradio-config.el
@@ -0,0 +1,36 @@
+;;; eradio-config --- Simple Internet Radio Setup -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+(use-package eradio
+ :bind
+ ("C-c r p" . eradio-play)
+ ("C-c r s" . eradio-stop)
+ ("C-c r <SPC>" . eradio-toggle)
+ :config
+ (setq eradio-player '("mpv" "--no-video" "--no-terminal"))
+ (setq eradio-channels
+ '(("BAGeL Radio (alternative)" . "https://ais-sa3.cdnstream1.com/2606_128.mp3")
+ ("Blues Music Fan Radio (blues)" . "http://ais-sa2.cdnstream1.com/1992_128.mp3")
+ ("Blues Radio (blues)" . "http://cast3.radiohost.ovh:8352/")
+ ("Concertzender Baroque (classical)" . "http://streams.greenhost.nl:8080/barok")
+ ("Groove Salad (somafm)" . "https://somafm.com/groovesalad130.pls")
+ ("Indie Pop Rocks (somafm)" . "https://somafm.com/indiepop130.pls")
+ ("KDFC Classical (classical)" . "http://128.mp3.pls.kdfc.live/")
+ ("Radio Caprice Classical Lute (classical)" . "http://79.120.12.130:8000/lute")
+ ("Radio Swiss Classic German (classical)" . "http://stream.srg-ssr.ch/m/rsc_de/mp3_128")
+ ("Radio Caprice Acoustic Blues (blues)" . "http://79.111.14.76:8000/acousticblues")
+ ("Radio Caprice Delta Blues (blues)" . "http://79.120.77.11:8002/deltablues")
+ ("Seven Inch Soul (somafm)" . "https://somafm.com/nossl/7soul.pls")
+ ("Space Station Soma (somafm)" . "https://somafm.com/spacestation.pls")
+ ("Suburbs of Goa (somafm)" . "https://somafm.com/suburbsofgoa.pls")
+ ("Sunday Baroque (classical)" . "http://wshu.streamguys.org/wshu-baroque")
+ ("Underground 80s (somafm)" . "https://somafm.com/u80s256.pls")
+ ("Venice Classic Radio (classical)" . "https://www.veniceclassicradio.eu/live1/128.m3u")
+ ("WWOZ New Orleans (jazz/blues)" . "https://www.wwoz.org/listen/hi"))))
+
+(provide 'eradio-config)
+;;; eradio-config.el ends here
diff --git a/modules/erc-config.el b/modules/erc-config.el
new file mode 100644
index 00000000..ddc8f737
--- /dev/null
+++ b/modules/erc-config.el
@@ -0,0 +1,109 @@
+;;; erc-config --- Preferences for Emacs Relay Chat (IRC Client) -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;; The main entry point into ERC is the C-c C-c keybinding.
+;; Channels in erc-autojoin-channels-alist will open in background.
+;; After connecting, switch to any ERC buffer with C-c C-c.
+;; Quit current channel with C-c C-p; Quit ERC altogether with C-c C-q
+
+;;; Code:
+
+;; ------------------------------------ ERC ------------------------------------
+;; Emacs Relay Chat - an IRC client
+
+(use-package erc
+ :defer 1
+ :ensure nil ;; built-in
+ :commands (erc erc-tls)
+ :bind
+ ;; the global keybinding
+ ("C-c I" . 'cj/erc-start-or-switch)
+ (:map erc-mode-map
+ ;; overrides erc-toggle-interpret-controls
+ ("C-c I" . 'cj/erc-start-or-switch))
+ :hook
+ (erc-mode . emojify-mode)
+ :custom
+ (erc-modules
+ '(autojoin
+ button
+ completion
+ fill
+ irccontrols
+ list
+ log
+ match
+ move-to-prompt
+ noncommands
+ notifications
+ readonly
+ services
+ stamp))
+
+ (erc-autojoin-channels-alist
+ '(("libera"
+ "#erc"
+ "#ledger"
+ "#emacs"
+ "#emacs-social"
+ "#zfsonlinux"
+ "#systemcrafters"
+ "#org-mode")))
+
+ (erc-nick "craigjennings")
+ (erc-user-full-name user-whole-name)
+ (erc-use-auth-source-for-nickserv-password t)
+ (erc-kill-buffer-on-part t)
+ (erc-kill-queries-on-quit t)
+ (erc-kill-server-buffer-on-quit t)
+ (erc-fill-column 120)
+ (erc-fill-function 'erc-fill-static)
+ (erc-fill-static-center 20)
+
+ :config
+ ;; use all text mode abbrevs in ercmode
+ (abbrev-table-put erc-mode-abbrev-table :parents (list text-mode-abbrev-table))
+
+ ;; create log directory if it doesn't exist
+ (setq erc-log-channels-directory (concat user-emacs-directory "erc/logs/"))
+ (if (not (file-exists-p erc-log-channels-directory))
+ (mkdir erc-log-channels-directory t)))
+
+;; --------------------------------- ERC Image ---------------------------------
+;; show inlined images (png/jpg/gif/svg) in erc buffers.
+
+(use-package erc-image
+ :defer 1
+ :after erc
+ :config
+ (setq erc-image-inline-rescale 300)
+ (add-to-list 'erc-modules 'image)
+ (erc-update-modules))
+
+;; -------------------------------- ERC Hl Nicks -------------------------------
+;; uniquely identify names in ERC
+
+(use-package erc-hl-nicks
+ :defer 1
+ :after erc
+ :config
+ (add-to-list 'erc-modules 'hl-nicks))
+
+;; -------------------------------- Connect IRC --------------------------------
+;; convenience function to auto-connect to irc.libera.chat
+
+(defun cj/erc-start-or-switch ()
+ "Start ERC or switch to ERC buffer if it has started already."
+ (interactive)
+ (if (get-buffer "Libera.Chat")
+ (erc-switch-to-buffer)
+ (erc-tls
+ :server "irc.libera.chat"
+ :port 6697
+ :nick "craigjennings"
+ :full-name user-whole-name)))
+
+(provide 'erc-config)
+;;; erc-config.el ends here
diff --git a/modules/eshell-vterm-config.el b/modules/eshell-vterm-config.el
new file mode 100644
index 00000000..cf73205f
--- /dev/null
+++ b/modules/eshell-vterm-config.el
@@ -0,0 +1,152 @@
+;;; eshell-vterm-config --- Settings for the Emacs Shell -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;; ESHELL
+;; - Eshell is useful as a REPL
+;; - Redirect to the kill ring : ls > /dev/kill
+;; - Redirect to the clioboard : ls > /dev/clip
+;; - Redirect to a buffer : ls > #<ls-output>
+;; - Use elisp functions : write your own "detox" command in elisp
+;; : then use it in eshell
+;; - cd to remote directories : cd /sshx:c@cjennings.net:/home/cjennings
+;; : and take all the elisp functionality remotely
+;; : including Dired or Magit on a remote server
+
+;; VTERM
+;; At the moment, vterm behaves like a real terminal. For most keys, vterm will
+;; just send them to the process that is currently running. So, C-a may be
+;; beginning-of-the-line in a shell, or the prefix key in a screen session.
+
+;; If you enter vterm-copy-mode C-c C-t or <pause>, the buffer will become a normal
+;; Emacs buffer. You can then use your navigation keys, select rectangles, etc.
+;; When you press RET, the region will be copied and you'll be back in a working
+;; terminal session.
+
+;; ANSI-TERM & TERM
+;; I haven't yet found a need for term or ansi-term in my workflows, so I leave
+;; them with their default configurations.
+
+;;; Code:
+
+;; ------------------------------ Eshell -----------------------------
+;; the Emacs shell.
+
+(use-package eshell
+ :ensure nil ;; built-in
+ :defer .5
+ :config
+ ;; for plan 9 smart shell functionality
+ (require 'em-smart)
+ (setq eshell-where-to-jump 'begin)
+ (setq eshell-review-quick-commands nil)
+ (setq eshell-smart-space-goes-to-end t)
+
+ (setq eshell-banner-message "")
+ (setq eshell-scroll-to-bottom-on-input 'all)
+ (setq eshell-error-if-no-glob t)
+ (setq eshell-hist-ignoredups t)
+ (setq eshell-save-history-on-exit t)
+ (setq eshell-prefer-lisp-functions nil)
+ (setq eshell-destroy-buffer-when-process-dies t)
+
+ (add-to-list 'eshell-modules-list 'eshell-tramp)
+
+ (add-hook 'eshell-hist-mode-hook
+ (lambda ()
+ (define-key eshell-hist-mode-map (kbd "<up>") 'previous-line)
+ (define-key eshell-hist-mode-map (kbd "<down>") 'next-line)))
+
+ (add-hook 'eshell-mode-hook
+ (lambda ()
+ (add-to-list 'eshell-visual-commands '("lf" "ranger" "tail" "htop" "gotop" "mc" "ncdu" "top"))
+ (add-to-list 'eshell-visual-subcommands '("git" "log" "diff" "show"))
+ (add-to-list 'eshell-visual-options '("git" "--help" "--paginate"))
+
+ ;; aliases
+ (eshell/alias "clear" "clear 1") ;; leaves the prompt at the top of the window
+ (eshell/alias "e" "find-file $*")
+ (eshell/alias "gocj" "cd /sshx:cjennings@cjennings.net:/var/cjennings/")
+ (eshell/alias "gosb" "cd /sshx:cjennings@wolf.usbx.me:/home/cjennings/")
+ (eshell/alias "gowolf" "cd /sshx:cjennings@wolf.usbx.me:/home/cjennings/")
+ (eshell/alias "v" "eshell-exec-visual $*")
+ (eshell/alias "ff" "find-file-other-window $*")
+ (eshell/alias "f" "cj/eshell-find-using-dired $1")
+ (eshell/alias "r" "ranger")
+ (eshell/alias "em" "find-file $*")
+ (eshell/alias "emacs" "find-file $*")
+ (eshell/alias "ll" "ls -l"))))
+
+(defun cj/eshell-find-using-dired (file-pattern)
+ "Find a file FILE-PATTERN' using 'find-name-dired'."
+ (let ((escaped-pattern (regexp-quote file-pattern)))
+ (find-name-dired . escaped-pattern)))
+
+(defun cj/eshell-delete-window-on-exit ()
+ "Close the eshell window when exiting."
+ (when (not (one-window-p))
+ (delete-window)))
+(advice-add 'eshell-life-is-too-much :after 'cj/eshell-delete-window-on-exit)
+
+(use-package eshell-toggle
+ :after eshell
+ :custom
+ (eshell-toggle-size-fraction 3)
+ (eshell-toggle-run-command nil)
+ (eshell-toggle-init-function #'eshell-toggle-init-eshell)
+ :bind
+ ("<f12>" . eshell-toggle))
+
+(use-package xterm-color
+ :defer .5
+ :after eshell
+ :hook (eshell-before-prompt-hook . (lambda ()
+ (setq xterm-color-preserve-properties t)))
+ :config
+ (add-to-list 'eshell-preoutput-filter-functions 'xterm-color-filter)
+ (setq eshell-output-filter-functions (remove 'eshell-handle-ansi-color eshell-output-filter-functions))
+ (setenv "TERM" "xterm-256color"))
+
+;; ------------------------------ Vterm ------------------------------
+;; faster and highly dependable, but not extensible
+
+(use-package vterm
+ :defer .5
+ :commands (vterm vterm-other-window)
+ :init
+ (setq vterm-always-compile-module t)
+
+ (defun cj/turn-off-chrome-for-vterm ()
+ (hl-line-mode -1)
+ (display-line-numbers-mode -1))
+
+ :hook (vterm-mode . cj/turn-off-chrome-for-vterm)
+ :bind
+ (:map vterm-mode-map
+ ("C-y" . vterm-yank)
+ ("C-p" . vterm-copy-mode)
+ ("<pause>" . vterm-copy-mode))
+ :custom
+ (vterm-kill-buffer-on-exit t)
+ (vterm-max-scrollback 100000))
+
+(use-package vterm-toggle
+ :defer .5
+ :bind
+ ("C-<f12>" . vterm-toggle)
+ :config
+ (setq vterm-toggle-fullscreen-p nil)
+ (add-to-list 'display-buffer-alist
+ '((lambda (buffer-or-name _)
+ (let ((buffer (get-buffer buffer-or-name)))
+ (with-current-buffer buffer
+ (or (equal major-mode 'vterm-mode)
+ (string-prefix-p vterm-buffer-name (buffer-name buffer))))))
+ (display-buffer-reuse-window display-buffer-at-bottom)
+ (dedicated . t) ;dedicated is supported in Emacs 27+
+ (reusable-frames . visible)
+ (window-height . 0.3))))
+
+(provide 'eshell-vterm-config)
+;;; eshell-vterm-config.el ends here.
diff --git a/modules/eww-config.el b/modules/eww-config.el
new file mode 100644
index 00000000..f0ee51a6
--- /dev/null
+++ b/modules/eww-config.el
@@ -0,0 +1,28 @@
+;;; eww-config --- EWW Text Browser Settings -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;; ------------------------------------ EWW ------------------------------------
+
+(use-package eww
+ :ensure nil ;; built-in
+ :bind
+ ("M-E" . eww)
+ (:map eww-mode-map
+ ("<" . eww-back-url) ;; in addition to 'l'
+ (">" . eww-forward-url) ;; in addition to 'n'
+ ("i" . eww-toggle-images)
+ ("o" . eww-open-in-new-buffer))
+ :config
+ (setq shr-use-colors nil) ;; respect colors in the html
+ (setq shr-bullet "• ") ;; unordered lists use bullet glyph
+ (setq shr-folding-mode t)
+ (setq eww-search-prefix "https://duckduckgo.com/html?q=") ;; use Duck Duck Go as search engine
+ (setq url-cookie-file "~/.local/share/cookies.txt")
+ (setq url-privacy-level '(email agent lastloc))) ;; don't send any info listed here
+
+(provide 'eww-config)
+;;; eww-config.el ends here
diff --git a/modules/flycheck-config.el b/modules/flycheck-config.el
new file mode 100644
index 00000000..1bad4cbd
--- /dev/null
+++ b/modules/flycheck-config.el
@@ -0,0 +1,47 @@
+;;; flycheck-config --- Syntax/Grammar Check -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+(defun cj/prose-helpers-on ()
+ "Ensure that abbrev, flyspell, and flycheck are on."
+ (interactive)
+ (if (not (abbrev-mode))
+ (abbrev-mode))
+ (flyspell-on-for-buffer-type)
+ (if (not (flycheck-mode))
+ (flycheck-mode)))
+
+;;;; ---------------------------------- Linting --------------------------------
+
+(use-package flycheck
+ :defer .5
+ :hook (sh-mode emacs-lisp-mode)
+ :bind ("C-; ?" . flycheck-list-errors)
+ :config
+ ;; don't warn about double-spaces after period.
+ (setq-default checkdoc-arguments '("sentence-end-double-space" nil "warn-escape" nil))
+
+ ;; proselint must be installed via the OS
+ (flycheck-define-checker proselint
+ "A linter for prose."
+ :command ("proselint" source-inplace)
+ :error-patterns
+ ((warning line-start (file-name) ":" line ":" column ": "
+ (id (one-or-more (not (any " "))))
+ (message) line-end))
+ :modes (text-mode markdown-mode gfm-mode))
+ (add-to-list 'flycheck-checkers 'proselint))
+
+;; ;; https://github.com/emacs-grammarly/flycheck-grammarly
+;; (use-package flycheck-grammarly
+;; :defer 1
+;; :after flycheck
+;; :config
+;; (with-eval-after-load 'flycheck
+;; (flycheck-grammarly-setup)))
+
+(provide 'flycheck-config)
+;;; flycheck-config.el ends here
diff --git a/modules/flyspell-config.el b/modules/flyspell-config.el
new file mode 100644
index 00000000..6109eebc
--- /dev/null
+++ b/modules/flyspell-config.el
@@ -0,0 +1,191 @@
+;;; flyspell-config.el --- Spell Check Configuration -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;; Spell-Checking: Flyspell
+;; C-' is now my main interface for all spell checking. It checks one word, then
+;; you may proceed to your next misspelling by selecting 'C-' again.
+
+;; Flyspell will automatically run in a mode that's appropriate for the buffer type
+;; - if it's a programming mode, it will only check comments
+;; - if in text mode, it will check everything
+;; - otherwise it will turn off.
+;; This check happens on every mode switch.
+
+;; If you want flyspell on in another mode (say fundamental mode), or you'd like it off
+;; to keep it from distracting, you can toggle flyspell's state with 'C-c f'
+
+;; The nicest thing is that each spell correction creates an abbrev. This essentially is a shortcut
+;; that expands that same misspelling to the correct spelling the next time it's typed. That
+;; idea comes courtesy Artur Malabarba, and it's increased my overall typing speed.
+;;
+;; Use M-o to get to 'other options', including saving to your personal dictionary.
+
+;;; Code:
+
+;; ------------------------ Reclaim Flyspell Keybinding ------------------------
+;; remove one of flyspell's many greedy keyboard bindings
+;; this is now used for my personal-keymap
+
+(eval-after-load "flyspell"
+ '(define-key flyspell-mode-map (kbd "C-;") nil))
+
+;; --------------------------------- Flyspell --------------------------------
+;; there's some ispell settings here as well.
+
+(use-package flyspell
+ :defer .5
+ :ensure nil ;; built-in
+ :config
+ (setq ispell-alternate-dictionary (concat user-emacs-directory "assets/english-words.txt"))
+ (setq ispell-dictionary "american") ; better for aspell
+ (setq ispell-extra-args '("--sug-mode=ultra" "-W" "3" "--lang=en_US"))
+ (setq ispell-list-command "--list") ;; in aspell "-l" means --list, not --lang
+ (setq ispell-local-dictionary "en_US")
+ (setq ispell-program-name "aspell") ;; use aspell instead of ispell
+ (setq ispell-local-dictionary-alist '(("en_US" "[[:alpha:]]" "[^[:alpha:]]" "['‘’]"
+ t ; Many other characters
+ ("-d" "en_US") nil utf-8)))
+
+ (setq ispell-personal-dictionary
+ (concat sync-dir "aspell-personal-dictionary")) ;; personal directory goes with sync'd files
+ (setq flyspell-issue-message-flag nil) ;; don't print message for every word when checking
+ (add-to-list 'ispell-skip-region-alist '("^#+BEGIN_SRC" . "^#+END_SRC"))) ;; skip code blocks in org mode
+
+
+(use-package flyspell-correct
+ :defer 1
+ :after flyspell
+ :bind
+ (:map flyspell-mode-map
+ ("C-'" . cj/flyspell-then-abbrev))) ;; disallow other entries to spelling corrections
+
+
+(use-package flyspell-correct-ivy
+ :defer 1
+ :after (ivy flyspell-correct)
+ :init
+ (setq flyspell-correct-interface #'flyspell-correct-ivy)) ;; use ivy as the flyspell interface
+
+
+;; ------------------------ Flyspell On For Buffer Type ------------------------
+;; check strings and comments in prog mode; everything in text mode
+
+(defun flyspell-on-for-buffer-type ()
+ "Enable Flyspell appropriately for the major mode and check the current buffer.
+If flyspell is already enabled, do nothing. If the mode is derived from
+`prog-mode', enable `flyspell-prog-mode' so only strings and comments get
+checked. If the buffer is text based `flyspell-mode' is enabled to check
+all text. Otherwise, flyspell is turned off."
+ (interactive)
+ (if (not (symbol-value flyspell-mode)) ; if not already on
+ (progn
+ (if (derived-mode-p 'prog-mode)
+ (progn
+ (flyspell-prog-mode)
+ (flyspell-buffer)))
+ (if (derived-mode-p 'text-mode)
+ (progn
+ (flyspell-mode 1)
+ (flyspell-buffer))
+ ;; else
+ (progn
+ (flyspell-mode 0))))))
+(add-hook 'after-change-major-mode-hook 'flyspell-on-for-buffer-type)
+(add-hook 'find-file-hook 'flyspell-on-for-buffer-type)
+
+;; ---------------------------- Flyspell Then Abbrev ---------------------------
+;; A stroke of genius from Artur Malabarba.
+;; http://endlessparentheses.com/ispell-and-abbrev-the-perfect-auto-correct.html
+;; Spell checks backwards (after you've noticed a typo). Corrects, then automatically
+;; creates an abbrev to autocorrect the same misspelling.
+;; Bound to C-; above.
+
+(setq save-abbrevs 'silently)
+(setq-default abbrev-mode t)
+
+(defun cj/flyspell-then-abbrev (p)
+ "Call `ispell-word', then create an abbrev for it.
+With prefix P, create local abbrev. Otherwise it will
+be global."
+ (interactive "P")
+ (save-excursion
+ (if (flyspell-goto-previous-word (point))
+ (let ((bef (downcase (or (thing-at-point 'word)
+ "")))
+ aft)
+ (call-interactively 'flyspell-correct-at-point)
+ (setq aft (downcase
+ (or (thing-at-point 'word) "")))
+ (unless (or (string= aft bef)
+ (string= aft "")
+ (string= bef ""))
+ (message "\"%s\" now expands to \"%s\" %sally"
+ bef aft (if p "loc" "glob"))
+ (define-abbrev
+ (if p local-abbrev-table global-abbrev-table)
+ bef aft)))
+ (message "Cannot find a misspelled word"))))
+
+(defun flyspell-goto-previous-word (position)
+ "Go to the first misspelled word that occurs before point.
+But don't look beyond what's visible on the screen."
+ (interactive "d")
+ (let ((top (window-start))
+ (bot (window-end)))
+ (save-restriction
+ (narrow-to-region top bot)
+ (overlay-recenter (point))
+ (add-hook 'pre-command-hook
+ (function flyspell-auto-correct-previous-hook) t t)
+ (unless flyspell-auto-correct-previous-pos
+ ;; only reset if a new overlay exists
+ (setq flyspell-auto-correct-previous-pos nil)
+ (let ((overlay-list (overlays-in (point-min) position))
+ (new-overlay 'dummy-value))
+ ;; search for previous (new) flyspell overlay
+ (while (and new-overlay
+ (or (not (flyspell-overlay-p new-overlay))
+ ;; check if its face has changed
+ (not (eq (get-char-property
+ (overlay-start new-overlay) 'face)
+ 'flyspell-incorrect))))
+ (setq new-overlay (car-safe overlay-list))
+ (setq overlay-list (cdr-safe overlay-list)))
+ ;; if nothing new exits new-overlay should be nil
+ (if new-overlay ;; the length of the word may change so go to the start
+ (setq flyspell-auto-correct-previous-pos
+ (overlay-start new-overlay)))))
+ (if (not flyspell-auto-correct-previous-pos)
+ nil
+ (goto-char flyspell-auto-correct-previous-pos)
+ t))))
+
+;; ------------------------------ Flyspell Toggle ------------------------------
+;; easy toggling flyspell which also leverages the 'for-buffer-type' functionality.
+
+(defun flyspell-toggle ()
+ "Turn Flyspell on if it is off, or off if it is on.
+When turning on,it uses `flyspell-on-for-buffer-type' so code-vs-text is
+handled appropriately."
+ (interactive)
+ (if (symbol-value flyspell-mode)
+ (progn ; flyspell is on, turn it off
+ (message "Flyspell off")
+ (flyspell-mode -1))
+ ;; else - flyspell is off, turn it on
+ (progn
+ (flyspell-on-for-buffer-type)
+ (message "Flyspell on"))))
+(global-set-key (kbd "C-c f") 'flyspell-toggle )
+
+
+;; -------------------------------- Ispell STFU --------------------------------
+;; tell ispell where to send it's spurious error messages
+
+(advice-add 'ispell-lookup-words :around
+ (lambda (orig &rest args)
+ (shut-up (apply orig args))))
+
+(provide 'flyspell-config)
+;;; flyspell-config.el ends here.
diff --git a/modules/font-config.el b/modules/font-config.el
new file mode 100644
index 00000000..a5ed2771
--- /dev/null
+++ b/modules/font-config.el
@@ -0,0 +1,200 @@
+;;; font-config --- Font Defaults and Related Functionality -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;; ----------------------- Font Family And Size Selection ----------------------
+;; preset your fixed and variable fonts, then apply them to text as a set
+
+(use-package fontaine
+ :demand t
+ :bind
+ ("M-F" . fontaine-set-preset)
+ :config
+ (setq fontaine-font-families
+ '((default "Hack Nerd Font Mono" "Fira Code Nerd Font" "MonoLisa" "Codelia Ligatures" "JetBrains Mono Nerd Font")
+ (fixed-pitch "Fira Code Nerd Font" "Hack Nerd Font" "JetBrains Mono Nerd Font" "Liberation Mono")
+ (variable-pitch "Literata" "Novaletra Serif CF" "FiraGO" "Nimbus Roman" "Nimbus Sans")))
+ (setq fontaine-presets
+ '(
+ (Hack-Only
+ :default-family "Hack Nerd Font Mono"
+ :variable-pitch-family "Hack Nerd Font Mono")
+ (Berkeley-Only
+ :default-family "Berkeley Mono"
+ :variable-pitch-family "Berkeley Mono")
+ (Merriweather-Only
+ :default-family "Merriweather"
+ :variable-pitch-family "Merriweather")
+ (MonoLisa-Novaletra
+ :default-family "MonoLisa"
+ :variable-pitch-family "Novaletra Serif CF")
+ (FiraCode-Literata
+ :default-family "Fira Code Nerd Font"
+ :variable-pitch-family "Literata")
+ (JetBrains0Lato
+ :default-family "JetBrains Mono"
+ :variable-pitch-family "Lato")
+ (Codelia-Only
+ :default-family "Codelia Ligatures")
+ (Liberation_Mono-Only
+ :default-family "Liberation Mono")
+ (t ;; shared fallback properties go here
+ :default-family "Berkeley Mono"
+ :default-weight regular
+ :default-height 130
+ :fixed-pitch-family nil ;; falls back to :default-family
+ :fixed-pitch-weight nil ;; falls back to :default-weight
+ :fixed-pitch-height 1.0
+ :fixed-pitch-serif-family nil ;; falls back to :default-family
+ :fixed-pitch-serif-weight nil ;; falls back to :default-weight
+ :fixed-pitch-serif-height 1.0
+ :variable-pitch-family "Merriweather"
+ :variable-pitch-weight light
+ :variable-pitch-height 1.0
+ :bold-family nil ;; use whatever the underlying face has
+ :bold-weight bold
+ :italic-family nil
+ :italic-slant italic
+ :line-spacing nil))))
+
+(with-eval-after-load 'fontaine
+ (defun cj/apply-font-settings-after-ui-creation()
+ "Apply font settings when Emacsclient is run in daemon mode.
+Emacsclient connects to a headless Emacs daemon, when no UI
+exists yet to apply these settings. This solution requests Emacsclient
+to apply the default font settings after the server creates a frame.
+Note that server-after-make-frame-hook is available only in Emacs 27+."
+ (interactive)
+ (fontaine-set-preset 'Berkeley-Only)
+ (if (daemonp)
+ (remove-hook 'server-after-make-frame-hook #'cj/apply-font-settings-after-ui-creation)))
+
+ (if (daemonp)
+ (add-hook 'server-after-make-frame-hook #'cj/apply-font-settings-after-ui-creation)
+ (cj/apply-font-settings-after-ui-creation)))
+
+;; ----------------------------- Font Install Check ----------------------------
+;; convenience function to indicate whether a font is available by name.
+
+(defun cj/font-installed-p (font-name)
+ "Check if font with FONT-NAME is available."
+ (if (find-font (font-spec :name font-name))
+ t
+ nil))
+
+;; ------------------------------- All The Icons -------------------------------
+;; icons made available through fonts
+
+(use-package all-the-icons
+ :demand t
+ :config
+ (when (and (not (cj/font-installed-p "all-the-icons"))
+ (window-system))
+ (all-the-icons-install-fonts t)))
+
+(use-package all-the-icons-nerd-fonts
+ :after all-the-icons
+ :demand t
+ :config
+ (all-the-icons-nerd-fonts-prefer))
+
+;; ----------------------------- Emoji Fonts Per OS ----------------------------
+;; these are in reverse order of priority
+
+(when (member "Segoe UI Emoji" (font-family-list))
+ (set-fontset-font
+ t 'symbol (font-spec :family "Segoe UI Emoji") nil 'prepend))
+(when (member "Apple Color Emoji" (font-family-list))
+ (set-fontset-font
+ t 'symbol (font-spec :family "Apple Color Emoji") nil 'prepend))
+(when (member "Noto Color Emoji" (font-family-list))
+ (set-fontset-font
+ t 'symbol (font-spec :family "Noto Color Emoji") nil 'prepend))
+
+;; ---------------------------------- Emojify ----------------------------------
+;; converts emoji identifiers into emojis; allows for easy emoji entry.
+
+(use-package emojify
+ :hook ((erc-mode . emojify-mode)
+ (org-mode . emojify-mode)
+ (prog-mode . (lambda () (emojify-mode -1)))
+ (gptel-mode . (lambda () (emojify-mode -1))))
+ :custom
+ (emojify-download-emojis-p t) ;; don't ask, just download emojis
+ :bind
+ ("C-c e i" . emojify-insert-emoji) ;; emoji insert
+ ("C-c e l" . emojify-list-emojis) ;; emoji list
+ :config
+ (setq emojify-show-help nil)
+ (setq emojify-point-entered-behaviour 'uncover)
+ (setq emojify-display-style 'image)
+ (setq emojify-emoji-styles '(ascii unicode github)))
+
+;; -------------------------- Display Available Fonts --------------------------
+;; display all available fonts on the system in a side panel
+
+(defun cj/display-available-fonts ()
+ "Display a list of all font faces with sample text in another read-only buffer."
+ (interactive)
+ (pop-to-buffer "*Available Fonts*" '(display-buffer-in-side-window . ((side . right)(window-width . fit-window-to-buffer))))
+ (let ((font-list (font-family-list)))
+ (setq font-list (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 `((:foreground "Light Blue" :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)))
+
+(global-set-key (kbd "C-z F") 'cj/display-available-fonts)
+
+;; ----------------------- Increase / Decrease Font Size -----------------------
+;; make it easy to enlarge or shrink font sizes with keybindings
+
+(setq text-scale-mode-step 1.08)
+(global-set-key (kbd "C-+") 'text-scale-increase)
+(global-set-key (kbd "C-=") 'text-scale-increase)
+(global-set-key (kbd "C-_") 'text-scale-decrease)
+(global-set-key (kbd "C--") 'text-scale-decrease)
+
+;; --------------------------------- Ligatures --------------------------------- '
+;; fancy programming glyphs make code easier to read
+
+(use-package ligature
+ :defer 1
+ :config
+ ;; Enable the www ligature in every possible major mode
+ (ligature-set-ligatures 't '("www"))
+ ;; Enable traditional ligature support in eww, if `variable-pitch' face supports it
+ (ligature-set-ligatures 'eww-mode '("ff" "fi" "ffi"))
+ ;; Enable ligatures in markdown mode
+ (ligature-set-ligatures 'markdown-mode '(("=" (rx (+ "=") (? (| ">" "<"))))
+ ("-" (rx (+ "-")))))
+ ;; Enable ligatures in programming modes
+ (ligature-set-ligatures 'prog-mode '("www" "**" "***" "**/" "*>" "*/" "\\\\" "\\\\\\" "{-" "::"
+ ":::" ":=" "!!" "!=" "!==" "-}" "----" "-->" "->" "->>"
+ "-<" "-<<" "-~" "#{" "#[" "##" "###" "####" "#(" "#?" "#_"
+ "#_(" ".-" ".=" ".." "..<" "..." "?=" "??" ";;" "/*" "/**"
+ "/=" "/==" "/>" "//" "///" "&&" "||" "||=" "|=" "|>" "^=" "$>"
+ "++" "+++" "+>" "=:=" "==" "===" "==>" "=>" "=>>" "<="
+ "=<<" "=/=" ">-" ">=" ">=>" ">>" ">>-" ">>=" ">>>" "<*"
+ "<*>" "<|" "<|>" "<$" "<$>" "<!--" "<-" "<--" "<->" "<+"
+ "<+>" "<=" "<==" "<=>" "<=<" "<>" "<<" "<<-" "<<=" "<<<"
+ "<~" "<~~" "</" "</>" "~@" "~-" "~>" "~~" "~~>" "%%"))
+ (global-ligature-mode t))
+
+(provide 'font-config)
+;;; font-config.el ends here
diff --git a/modules/games-config.el b/modules/games-config.el
new file mode 100644
index 00000000..6b198be3
--- /dev/null
+++ b/modules/games-config.el
@@ -0,0 +1,69 @@
+;;; games-config.el --- emacs games -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; The games menu is the easiest entry. "Shift-Alt-G" will get you there. Enjoy!
+;;
+
+;;; Code:
+
+;; --------------------------------- Games Menu --------------------------------
+
+(defhydra hydra-games (:color blue :hint nil)
+ "\n"
+ ("2" 2048-game "2048 : Combine the Numbered Tiles to get to 2048" :column "Game")
+ ("c" chess "Chess : Play the 64 Squares and Checkmate the King" :column "Game")
+ ("d" dunnet "Dunnet : Emacs' Built-n Text Adventure" :column "Game")
+ ("g" gomoku "Gomoku : Tic Tac Toe, but Five in a Row" :column "Game")
+ ("m" malyon "Malyon : The Text Adventure Player" :column "Game")
+ ("t" tetris "Tetris : Combine falling blocks and scoreq" :column "Game"))
+(global-set-key (kbd "M-G") 'hydra-games/body)
+
+;; ----------------------------------- Malyon ----------------------------------
+;; text based adventure player
+
+(use-package malyon
+ :defer 1
+ :config
+ (setq malyon-stories-directory "~/sync/org/text.games/"))
+
+;; ------------------------------------ 2048 -----------------------------------
+;; combine numbered tiles to create the elusive number 2048.
+(use-package 2048-game
+ :defer 1)
+
+;; ----------------------------------- Chess -----------------------------------
+;; play the 64 squares and checkmate the opponent's king
+(use-package chess
+ :defer 1
+ :config
+ (setq chess-default-display 'chess-images)
+ (setq chess-images-directory (concat user-emacs-directory "assets/chess/pieces/xboard/"))
+ (setq chess-images-dark-color "#779556")
+ (setq chess-images-light-color "#EBECD0")
+ (setq chess-images-default-size 100)
+ (setq chess-full-name user-whole-name)
+ (setq chess-default-engine 'chess-fruit))
+
+
+;; Notes from source code
+;; If you'd like to view or edit Portable Game Notation (PGN) files,
+;; `chess-pgn-mode' provides a text-mode derived mode which can display the
+;; chess position at point.
+
+;; To improve your chess ability, `M-x chess-tutorial' provides a simple knight
+;; movement exercise to get you started, and `M-x chess-puzzle' can be used
+;; to solve puzzle collections in EPD or PGN format.
+;; The variable `chess-default-display' controls which display modules
+;; are tried when a chessboard should be displayed. By default, chess-images
+;; is tried first. If Emacs is not running in a graphical environment,
+;; chess-ics1 is used instead. To enable the chess-plain display module,
+;; customize `chess-default-display' accordingly.
+
+;; Once this is working, the next thing to do is to customize
+;; `chess-default-modules'. This is a list of functionality modules used
+;; by chess.el to provide additional functionality. You can enable or
+;; disable modules so that Emacs Chess better suites your tastes.
+;; Those modules in turn often have configuration variables, and
+
+(provide 'games-config)
+;;; games-config.el ends here.
diff --git a/modules/graphviz-config.el b/modules/graphviz-config.el
new file mode 100644
index 00000000..d5428e5a
--- /dev/null
+++ b/modules/graphviz-config.el
@@ -0,0 +1,17 @@
+;;; graphviz-config --- Graphviz Dot Mode Setup -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+
+;;;; ----------------------- Graphviz-Dot-Mode -----------------------
+
+(use-package graphviz-dot-mode
+ :config
+ (setq graphviz-dot-indent-width 4))
+
+
+(provide 'graphviz-config)
+;;; graphviz-config.el ends here
diff --git a/modules/help-utils.el b/modules/help-utils.el
new file mode 100644
index 00000000..66286704
--- /dev/null
+++ b/modules/help-utils.el
@@ -0,0 +1,89 @@
+;;; help-utils --- Help Additions and Preferences -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+
+(setq help-window-select t) ;; Always select the help buffer in a separate window
+
+(global-set-key (kbd "C-h P") 'list-packages) ;; bring up the package menu
+
+;;;; ---------------------------- Helpful ----------------------------
+
+(use-package helpful
+ :defer .5
+ :bind
+ ("C-h f" . helpful-callable)
+ ("C-h v" . helpful-variable)
+ ("C-h k" . helpful-key)
+ ("C-h F" . helpful-function)
+ ("C-h C" . helpful-command)
+ ("C-h ." . helpful-at-point)
+ ("C-h o" . helpful-symbol) ;; overrides 'describe-symbol' keybinding
+ :config
+ (setq counsel-describe-function-function #'helpful-callable)
+ (setq counsel-describe-variable-function #'helpful-variable))
+
+
+;;;; ------------------------------ Man ------------------------------
+
+(use-package man
+ :defer 1
+ :ensure nil ;; built-in
+ :bind ("C-h M" . man))
+
+
+;;;; ------------------------------ Info -----------------------------
+
+(use-package info
+ :defer 1
+ :ensure nil ;; built-in
+ :bind
+ (:map Info-mode-map
+ ("m" . cj/bookmark-set-and-save) ;; note:overrides menu selection
+ ("M" . Info-menu)) ;; so menu selection goes here
+ :preface
+ (defun open-with-info-mode ()
+ (interactive)
+ (let ((file-name (buffer-file-name)))
+ (kill-buffer (current-buffer))
+ (info file-name)))
+ :hook
+ (info-mode . info-persist-history-mode)
+ :init
+ ;; add personal info files in emacs config assets directory
+ ;; BUG: This causes an error on launch
+ (push (concat user-emacs-directory "assets/info") Info-directory-list)
+ (add-to-list 'auto-mode-alist '("\\.info\\'" . open-with-info-mode)))
+
+;;;; ---------------------------- Devdocs ----------------------------
+
+(use-package devdocs
+ :defer 1
+ :config
+ (global-set-key (kbd "C-h D s") 'devdocs-search)
+ (global-set-key (kbd "C-h D b") 'devdocs-peruse)
+ (global-set-key (kbd "C-h D l") 'devdocs-lookup)
+ (global-set-key (kbd "C-h D i") 'devdocs-install)
+ (global-set-key (kbd "C-h D d") 'devdocs-delete)
+ (global-set-key (kbd "C-h D u") 'devdocs-update-all)
+ (define-key devdocs-mode-map "b" 'devdocs-go-back)
+ (define-key devdocs-mode-map "f" 'devdocs-go-forward))
+
+;;;; ------------------------------- TLDR ------------------------------
+
+(use-package tldr
+ :defer 1
+ :bind ("C-h T" . tldr))
+
+;;;; -------------------------- Wiki-Summary -------------------------
+
+(use-package wiki-summary
+ :defer 1
+ :bind ("C-h W" . wiki-summary))
+
+
+(provide 'help-utils)
+;;; help-utils.el ends here
diff --git a/modules/host-environment.el b/modules/host-environment.el
new file mode 100644
index 00000000..649cabe5
--- /dev/null
+++ b/modules/host-environment.el
@@ -0,0 +1,48 @@
+;;; host-environment.el --- Host Environment Convenience Functions -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Convenience functions to report about the host environment
+
+;;; Code:
+
+(require 'battery)
+
+(defun env-laptop-p ()
+ "Return t if host is a laptop (has a battery), nil if not."
+ (when (and battery-status-function
+ (not (string-match-p "N/A"
+ (battery-format "%B"
+ (funcall battery-status-function)))))
+ t))
+
+(defun env-desktop-p ()
+ "Return t if host is a laptop (has a battery), nil if not."
+ (when (not (env-laptop-p))
+ t))
+
+(defun env-linux-p ()
+ "Return t if host system is GNU/Linux."
+ (string-equal system-type "gnu/linux"))
+
+(defun env-bsd-p ()
+ "Return t if host system is FreeBSD."
+ (string-equal system-type "berkeley-unix"))
+
+(defun env-macos-p ()
+ "Return t if host system is Mac OS (darwin-based)."
+ (string-equal system-type "darwin"))
+
+(defun env-windows-p ()
+ "Return t if host system is Windows."
+ (memq system-type '(cygwin windows-nt ms-dos)))
+
+(defun env-terminal-p ()
+ "Return t if running in a terminal."
+ (not (display-graphic-p)))
+
+(defun env-gui-p ()
+ "Return t if running in graphical environment."
+ (display-graphic-p))
+
+(provide 'host-environment)
+;;; host-environment.el ends here.
diff --git a/modules/httpd-config.el b/modules/httpd-config.el
new file mode 100644
index 00000000..c0ac20ee
--- /dev/null
+++ b/modules/httpd-config.el
@@ -0,0 +1,26 @@
+;;; httpd-config --- Setup for a Simple HTTP Server -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+
+;;;; -------------------------- Simple-Httpd -------------------------
+
+(use-package simple-httpd
+ :defer 1
+ :preface
+ (defconst wwwdir (concat user-emacs-directory "www"))
+ (defun check-or-create-wwwdir ()
+ (unless (file-exists-p wwwdir)
+ (make-directory wwwdir)))
+ :init (check-or-create-wwwdir)
+ :config
+ (setq httpd-root wwwdir)
+ (setq httpd-show-backtrace-when-error t)
+ (setq httpd-serve-files t))
+
+
+(provide 'httpd-config)
+;;; httpd-config.el ends here
diff --git a/modules/keybindings.el b/modules/keybindings.el
new file mode 100644
index 00000000..bf23ebc4
--- /dev/null
+++ b/modules/keybindings.el
@@ -0,0 +1,132 @@
+;;; keybindings --- General Keyboard Shortcuts -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;; TLDR: "C-c ?" Should bring up a helpful menu from wherever you are.
+
+;; I've created a general menu that contains commonly used applications and
+;; utilities.
+
+;; There are also helpful menus that describe common functionality from within
+;; another package, e.g., ledger. You access these with the same "C-c ? keyboard
+;; shortcut"
+
+;; Also, commonly used files should be easy to jump to. The "jump-to" keymap
+;; refers to files defined in user-constants.el.
+
+;; "Hostile Keybindings" are those that are close to keybindings I use commonly
+;; so they're easy to hit by accident, but they have painful results. I'd
+;; rather avoid the pain by unsetting they keybindings and view an error '<key>
+;; is undefined' message. Finally, I'm providing messages to train me to use
+;; faster keybindings and provide feedback when evaluating buffers.
+
+;;; Code:
+
+
+;; ----------------------------------- Hydra -----------------------------------
+;; provides ability to create menus
+
+(use-package hydra
+ :demand t) ;; used in init, so load it now
+
+;; --------------------------------- Main Hydra --------------------------------
+;; convenience menu for commonly used apps
+
+(defhydra hydra-general (:color blue :hint nil)
+ "
+
+ Main Menu
+
+ ^Applications^ ^Communication^ ^Utilities^ ^Entertainment
+ ^^^^^^^^--------------------------------------------------------------------------------
+ _f_: Feed Reader _m_: Mu4e Email _p_: Open Project _r_: Play Radio
+ _b_: Ebook Manager _t_: Telegram _c_: Calculator _g_: Games Menu
+ _F_: File Manager _i_: IRC _W_: World Clock
+ _l_: Ledger Accounting ^^ _z_: Diff Directories
+ _d_: Flashcard Drill ^^ ^^
+ ^^ ^^ ^^ _q_: quit
+\n\n
+"
+ ("q" nil)
+ ;; Applications
+ ("f" elfeed-dashboard)
+ ("b" calibredb)
+ ("F" (dirvish user-home-dir))
+ ("l" (find-file ledger-file))
+ ("d" cj/drill-start)
+
+ ;; Communication
+ ("m" mu4e)
+ ("t" telega)
+ ("i" cj/erc-start-or-switch)
+
+ ;; Utilities
+ ("p" projectile-switch-project)
+ ("c" calc)
+ ("W" world-clock)
+ ("z" ztree-diff)
+
+ ;; Entertainment
+ ("r" eradio-play)
+ ("g" hydra-games/body))
+
+(global-set-key (kbd "C-c ?") 'hydra-general/body)
+
+;; ------------------------------ Jump To Commands -----------------------------
+;; quick access for commonly used files
+
+(defvar jump-to-keymap nil "Jump-to commonly used files/directories/commands.")
+(setq jump-to-keymap (make-sparse-keymap))
+(global-set-key (kbd "C-c j") jump-to-keymap)
+(define-key jump-to-keymap (kbd "E") #'(lambda () (interactive) (find-file emacs-early-init-file)))
+(define-key jump-to-keymap (kbd "I") #'(lambda () (interactive) (find-file emacs-init-file)))
+(define-key jump-to-keymap (kbd "i") #'(lambda () (interactive) (find-file inbox-file)))
+(define-key jump-to-keymap (kbd "A") #'(lambda () (interactive) (find-file article-archive)))
+(define-key jump-to-keymap (kbd "a") #'(lambda () (interactive) (find-file article-file)))
+(define-key jump-to-keymap (kbd "c") #'(lambda () (interactive) (find-file contacts-file)))
+(define-key jump-to-keymap (kbd "s") #'(lambda () (interactive) (find-file schedule-file)))
+(define-key jump-to-keymap (kbd "m") #'(lambda () (interactive) (find-file macros-file)))
+(define-key jump-to-keymap (kbd "$") #'(lambda () (interactive) (find-file ledger-file)))
+
+;; ---------------------------- General Keybindings ----------------------------
+
+;; Avoid hostile bindings
+(global-unset-key (kbd "C-x C-f")) ;; find-file-read-only
+(global-unset-key (kbd "C-z")) ;; suspend-frame is accidentally hit often
+(global-unset-key (kbd "M-o")) ;; facemenu-mode
+
+;; Add commonly-used general keybindings
+(global-set-key (kbd "C-x C-f") 'find-file)
+(global-set-key (kbd "C-c u") 'capitalize-region)
+(global-set-key (kbd "C-c f") 'link-hint-open-link-at-point)
+(global-set-key (kbd "M-*") 'calculator)
+
+;; Normally bound to ESC ESC ESC, hit ESC once to get out of unpleasant situations.
+(global-set-key (kbd "<escape>") 'keyboard-escape-quit)
+
+;; remap C-x \ to sort-lines (from remap activate-transient-input-method)
+(global-unset-key (kbd "C-x \\"))
+(global-set-key (kbd "C-x \\") 'sort-lines)
+
+;; training myself to use C-/ for undo (bound internally) as it's faster.
+(if (display-graphic-p)
+ (progn
+ (global-unset-key (kbd "C-x u"))
+ (define-key global-map (kbd "C-x u")
+ #'(lambda () (interactive)
+ (message (concat "Seriously, " user-name
+ "? Use 'C-/'. It's faster."))))))
+
+;; evaluating a buffer should give confirmation or error.
+(defun cj/eval-buffer-with-confirmation-or-error-message ()
+ "Evaluate the buffer and display a message."
+ (interactive)
+ (let ((result (eval-buffer)))
+ (if (not (eq result 'error))
+ (message "Buffer evaluated.")
+ (message "error occurred during evaluation: %s" result))))
+(global-set-key (kbd "C-c b") 'cj/eval-buffer-with-confirmation-or-error-message)
+
+(provide 'keybindings)
+;;; keybindings.el ends here
diff --git a/modules/latex-config.el b/modules/latex-config.el
new file mode 100644
index 00000000..58454e00
--- /dev/null
+++ b/modules/latex-config.el
@@ -0,0 +1,53 @@
+;;; latex-config --- Setup for LaTeX and Related Software -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;;;; ----------------------------- Auctex ----------------------------
+
+;; (use-package tex
+;; :ensure auctex
+;; :hook
+;; (LaTeX-mode . (lambda () (TeX-fold-mode 1))) ; automatically activate TeX-fold-mode.
+;; (TeX-mode-hook . (lambda () (setq TeX-command-default "latexmk"))) ; use latexmk by default
+;; (LaTeX-mode . flyspell-mode) ; turn on flyspell-mode by default
+;; ;; (LaTeX-mode . TeX-PDF-mode)
+;; ;; (LaTeX-mode . (lambda () (push (list 'output-pdf "Zathura") TeX-view-program-selection)))
+;; :mode
+;; ("\\.tex\\'" . latex-mode)
+;; :config
+;; (setq TeX-auto-save t) ; auto save style info when saving buffer
+;; (setq TeX-parse-self t) ; parse file after loading if it has no style hook
+;; (setq TeX-save-query nil) ; don't ask to save files before starting TeX
+;; (setq TeX-PDF-mode t) ; compile to PDF mode, rather than DVI
+;; (setq-default TeX-master t)) ; Assume the file is the master file itself
+
+;; ;; use pdftools as viewer
+;; ;; https://emacs.stackexchange.com/questions/21755/use-pdfview-as-default-auctex-pdf-viewer#21764
+;; (setq TeX-view-program-selection '((output-pdf "PDF Tools"))
+;; TeX-view-program-list '(("PDF Tools" TeX-pdf-tools-sync-view))
+;; TeX-source-correlate-start-server t) ;; not sure if last line is neccessary
+;; ;; to have the buffer refresh after compilation,
+;; ;; very important so that PDFView refreshes itself after compilation
+;; (add-hook 'TeX-after-compilation-finished-functions
+;; #'TeX-revert-document-buffer)
+
+
+;; https://github.com/tom-tan/auctex-latexmk
+;; You should also add the following line to your .latexmkrc file:
+;; # .latexmkrc starts
+;; $pdf_mode = 1;
+;; # .latexmkrc ends
+
+;; AUCTEX-LATEXMK
+;;
+;; (use-package auctex-latexmk
+;; :config
+;; (auctex-latexmk-setup)
+;; (setq auctex-latexmk-inherit-TeX-PDF-mode t))
+
+
+(provide 'latex-config)
+;;; latex-config.el ends here
diff --git a/modules/ledger-config.el b/modules/ledger-config.el
new file mode 100644
index 00000000..21f7fb81
--- /dev/null
+++ b/modules/ledger-config.el
@@ -0,0 +1,74 @@
+;;; ledger-config.el --- Ledger Configuration -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;; Use C-c ? in ledger mode for discoverability.
+
+;; Unlike Mu4e and Elfeed (with Elfeed dashboard), there's no landing page for
+;; Ledger. You're thrown immediately into your financial data file in edit mode.
+;; Ok, but might be frightening for someone learning how to use Ledger. The
+;; added hydra is intended to show what actions are available, as well as their
+;; key bindings.
+
+
+;; -------------------------------- Ledger Mode --------------------------------
+;; edit files in ledger format
+
+(use-package ledger-mode
+ :mode ("\\.dat\\'"
+ "\\.ledger\\'"
+ "\\.journal\\'")
+ :preface
+ (defun cj/ledger-save ()
+ "Automatically clean the ledger buffer at each save."
+ (interactive)
+ (save-excursion
+ (when (buffer-modified-p)
+ (with-demoted-errors (ledger-mode-clean-buffer))
+ (save-buffer))))
+ :bind
+ (:map ledger-mode-map
+ ("C-x C-s" . cj/ledger-save))
+ :custom
+ (ledger-clear-whole-transactions t)
+ (ledger-reconcile-default-commodity "$")
+ (ledger-report-use-header-line nil)
+ (ledger-reports
+ '(("bal" "%(binary) --strict -f %(ledger-file) bal")
+ ("bal this month" "%(binary) --strict -f %(ledger-file) bal -p %(month) -S amount")
+ ("bal this year" "%(binary) --strict -f %(ledger-file) bal -p 'this year'")
+ ("net worth" "%(binary) --strict -f %(ledger-file) bal Assets Liabilities")
+ ("account" "%(binary) --strict -f %(ledger-file) reg %(account)"))))
+
+;; === Ledger Hydra ===
+;; menu available while in ledger-mode for common ledger commands
+
+(with-eval-after-load 'ledger-mode
+ (defhydra hydra-ledger (:color teal :timeout 10 :hint nil)
+ "Ledger common commands menu"
+ ("r" ledger-report "report" :column "report")
+ ("a" ledger-add-transaction "add transaction" :column "edit")
+ ("R" ledger-sort-region "sort region" :column "edit")
+ ("A" ledger-sort-buffer "sort all" :column "edit"))
+
+ (defun ledger-mode-hook-hydra-setup ()
+ "Create ledger hydra/menu keybinding when entering ledger mode."
+ (local-set-key (kbd "C-c ?") 'hydra-ledger/body))
+ (add-hook 'ledger-mode-hook 'ledger-mode-hook-hydra-setup))
+
+;; ------------------------------ Flycheck Ledger ------------------------------
+;; syntax and unbalanced transaction linting
+
+(use-package flycheck-ledger
+ :after ledger-mode)
+
+;; ------------------------------- Company Ledger ------------------------------
+;; autocompletion for ledger
+
+(use-package company-ledger
+ :after (company ledger-mode)
+ :config
+ (add-to-list 'company-backends 'company-ledger))
+
+(provide 'ledger-config)
+;;; ledger-config.el ends here.
diff --git a/modules/local-repository.el b/modules/local-repository.el
new file mode 100644
index 00000000..bb8c2770
--- /dev/null
+++ b/modules/local-repository.el
@@ -0,0 +1,50 @@
+;;; local-repository.el --- local repository functionality -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'elpa-mirror)
+
+;; ------------------------------ Utility Function -----------------------------
+
+
+(defun car-member (value list)
+ "Check if VALUE exists as the car of any cons cell in LIST."
+ (member value (mapcar #'car list)))
+
+;; ------------------------------- Customizations ------------------------------
+
+(defcustom localrepo-repository-id "localrepo"
+ "The name used to identify the local repository internally.
+Used for the package-archive and package-archive-priorities lists.")
+
+(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.")
+
+(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.")
+
+(defun localrepo-update-repository ()
+ "Update the local repository with currently installed packages."
+ (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 (car-member localrepo-repository-id package-archives)
+ (add-to-list 'package-archives
+ (localrepo-repository-id . localrepo-repository-location)))
+
+ (unless (car-member localrepo-repository-id package-archive-priorities)
+ (add-to-list 'package-archive-priorities
+ (localrepo-repository-id . localrepo-repository-priority))))
+
+
+
+(provide 'local-repository)
+;;; local-repository.el ends here.
diff --git a/modules/mail-config.el b/modules/mail-config.el
new file mode 100644
index 00000000..ac982814
--- /dev/null
+++ b/modules/mail-config.el
@@ -0,0 +1,210 @@
+;;; mail-config --- Settings for Mu4e Email -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+;; I found Aime Bertrand's blog post to be an excellent walkthrough of how to
+;; setup a Mu4e config.
+
+;; https://macowners.club/posts/email-emacs-mu4e-macos/
+
+;;; Code:
+
+(use-package mu4e
+ :ensure nil ;; mu4e gets installed by installing 'mu' via the system package manager
+ :load-path "/usr/share/emacs/site-lisp/mu4e/"
+ :defer .5
+ :bind
+ ("C-c m". mu4e)
+ (:map mu4e-headers-mode-map
+ ("M" . cj/mu4e-mark-all-headers)
+ ("D" . mu4e-headers-mark-for-trash)
+ ("d" . mu4e-headers-mark-for-delete))
+ (:map mu4e-view-mode-map
+ ("r" . mu4e-compose-wide-reply)
+ ("R" . mu4e-compose-reply))
+ :hook
+ (mu4e-view-mode . turn-on-visual-line-mode)
+ :config
+ (setq gnus-blocked-images "http") ;; block external images
+ (setq mail-user-agent 'mu4e-user-agent) ;; default to mu4e for email
+ (setq message-citation-line-format "On %a %d %b %Y at %R, %f wrote:\n") ;; helps show up properly in Outlook/Gmail threads
+ (setq message-citation-line-function 'message-insert-formatted-citation-line)
+ (setq message-kill-buffer-on-exit t) ;; don't keep message buffers around
+ (setq message-signature-file (concat user-emacs-directory "signature")) ;; look for the signature here
+ (setq mu4e-change-filenames-when-moving t) ;; avoid gmail dup UID issues: https://goo.gl/RTCgVa
+ (setq mu4e-completing-read-function 'completing-read) ;; use generic completing read, rather than ido
+ (setq mu4e-compose-context-policy 'ask) ;; ask for context if no context matches
+ (setq mu4e-compose-format-flowed t) ;; plain text mails must flow correctly for recipients
+ (setq mu4e-compose-keep-self-cc t) ;; keep me in the cc list
+ (setq mu4e-compose-signature-auto-include nil) ;; don't include signature by default
+ (setq mu4e-confirm-quit nil) ;; don't ask when quitting
+ (setq mu4e-context-policy 'pick-first) ;; start with the first (default) context
+ (setq mu4e-headers-auto-update nil) ;; updating headers buffer on email is too jarring
+ (setq mu4e-root-maildir "~/.mail") ;; root directory for all email accounts
+ (setq mu4e-sent-messages-behavior 'delete) ;; don't save to "Sent", IMAP does this already
+ (setq mu4e-show-images t) ;; show embedded images
+ (setq mu4e-update-interval nil) ;; don't update automatically
+
+ (setq mu4e-mu-binary (executable-find "mu"))
+ (setq mu4e-get-mail-command (concat (executable-find "mbsync") " -a")) ;; command to sync mail
+ (setq mu4e-user-mail-address-list '("c@cjennings.net" "craigmartinjennings@gmail.com"))
+ (setq mu4e-index-update-error-warning nil) ;; don't warn me about spurious sync issues
+
+ (setq mu4e-contexts
+ (list
+
+ (make-mu4e-context
+ :name "gmail.com"
+ :match-func
+ (lambda (msg)
+ (when msg
+ (string-prefix-p "/gmail" (mu4e-message-field msg :maildir))))
+ :vars '((user-mail-address . "craigmartinjennings@gmail.com")
+ (user-full-name . "Craig Jennings")
+ (mu4e-drafts-folder . "/gmail/Drafts")
+ (mu4e-sent-folder . "/gmail/Sent")
+ (mu4e-starred-folder . "/gmail/Starred")
+ (mu4e-trash-folder . "/gmail/Trash")))
+
+ (make-mu4e-context
+ :name "cjennings.net"
+ :match-func
+ (lambda (msg)
+ (when msg
+ (string-prefix-p "/cmail" (mu4e-message-field msg :maildir))))
+ :vars '((user-mail-address . "c@cjennings.net")
+ (user-full-name . "Craig Jennings")
+ (mu4e-drafts-folder . "/cmail/Drafts")
+ (mu4e-sent-folder . "/cmail/Sent")))))
+
+ (setq mu4e-maildir-shortcuts
+ '(("/cmail/Inbox" . ?i)
+ ("/cmail/Sent" . ?s)
+ ("/gmail/Inbox" . ?I)
+ ("/gmail/Sent" . ?S)))
+
+ ;; bookmarks and their shortcuts
+ (setq mu4e-bookmarks
+ `((:name "cjennings inbox"
+ :query "maildir:/cmail/INBOX"
+ :key ?i)
+ (:name "cjennings unread"
+ :query "maildir:/cmail/INBOX AND flag:unread AND NOT flag:trashed"
+ :key ?u)
+ (:name "cjennings starred"
+ :query "maildir:/cmail/INBOX AND flag:flagged"
+ :key ?s)
+ (:name "cjennings large"
+ :query "maildir:/cmail/INBOX AND size:5M..999M"
+ :key ?l)
+ (:name "gmail.com inbox"
+ :query "maildir:/gmail/INBOX"
+ :key ?I)
+ (:name "gmail.com unread"
+ :query "maildir:/gmail/INBOX AND flag:unread AND NOT flag:trashed"
+ :key ?U)
+ (:name "gmail.com starred"
+ :query "maildir:/gmail/INBOX AND flag:flagged"
+ :key ?S)
+ (:name "gmail.com large"
+ :query "maildir:/gmail/INBOX AND size:5M..999M"
+ :key ?L)))
+
+ (add-hook 'mu4e-compose-mode-hook
+ (defun cj/set-mu4e-compose ()
+ "My settings for message composition."
+ (set-fill-column 72)))
+
+ ;; Always BCC myself
+ ;; http://www.djcbsoftware.nl/code/mu/mu4e/Compose-hooks.html
+ (defun cj/add-header ()
+ "Add CC: and Bcc: to myself header."
+ (save-excursion (message-add-header
+ (concat "CC: " "\n")
+ ;; pre hook above changes user-mail-address.
+ (concat "Bcc: " user-mail-address "\n"))))
+ (add-hook 'mu4e-compose-mode-hook 'cj/add-header)
+
+ ;; remap the awkward mml-attach-file to the quicker mail-add-attachment
+ (define-key mu4e-compose-mode-map [remap mml-attach-file] 'mail-add-attachment)
+
+ ;; don't allow openwith to mess with your attachments
+ (add-to-list 'mm-inhibit-file-name-handlers 'openwith-file-handler)
+
+ ;; use imagemagick to render images, if available
+ (when (fboundp 'imagemagick-register-types)
+ (imagemagick-register-types)))
+
+;; xwidgets not able to be built into emacs on linux
+;; ;; view in xwidget html rendererer
+;; (add-to-list 'mu4e-headers-actions
+;; '("xWidget" . mu4e-action-view-with-xwidget) t)
+;; (add-to-list 'mu4e-view-actions
+;; '("xWidget" . mu4e-action-view-with-xwidget) t))
+
+(defun no-auto-fill ()
+ "Turn off \'auto-fill-mode\'."
+ (auto-fill-mode -1))
+(add-hook 'mu4e-compose-mode-hook #'no-auto-fill)
+
+
+;; ----------------------------- Compose Mode Hydra ----------------------------
+;; menu available in compose mode
+
+(defhydra hydra-mu4e-compose (:color blue :timeout 10 :hint nil)
+ "Compose Mode Menu\n\n"
+ ("q" quit-window "Quit" :column "")
+ ("a" mail-add-attachment "Add Attachment" :column "")
+ ("r" message-new-line-and-reformat "Newline and Reformat" :column "")
+ ("d" message-delete-not-region "Delete Outside Region" :column "")
+ )
+
+(defun mu4e-compose-mode-hook-hydra-setup ()
+ "Create hydra/menu keybinding when entering compose mode."
+ (local-set-key (kbd "C-c ?") 'hydra-mu4e-compose/body))
+(add-hook 'mu4e-compose-mode-hook 'mu4e-compose-mode-hook-hydra-setup)
+
+;; ------------------------- Mark All Headers ------------------------
+;; convenience function to mark all headers for an action
+
+(defun cj/mu4e-mark-all-headers ()
+ "Mark all headers for a later action.
+Prompts user for the action when executing."
+ (interactive)
+ (mu4e-headers-mark-for-each-if
+ (cons 'something nil)
+ (lambda (_msg _param) t)))
+
+;;; ------------------ Smtpmail & Easy PG Assistant -----------------
+
+;; Send Mail to smtp host from smtpmail temp buffer.
+(use-package smtpmail
+ :ensure nil ;; built-in
+ :defer .5
+ :config
+ ;; (require 'epa-file)
+ ;; (epa-file-enable)
+ ;; (setq epa-pinentry-mode 'loopback)
+ ;; (auth-source-forget-all-cached)
+
+ (setq message-kill-buffer-on-exit t) ;; don't keep compose buffers after sending
+ (setq sendmail-program (executable-find "msmtp"))
+ (setq send-mail-function 'message-send-mail-with-sendmail
+ message-send-mail-function 'message-send-mail-with-sendmail)
+ (setq message-sendmail-envelope-from 'header)
+ ;; (setq smtpmail-auth-credentials (expand-file-name "~/.authinfo.gpg"))
+ ;; (setq starttls-use-gnutls t)
+ ;; (setq smtpmail-starttls-credentials '(("smtp.gmail.com" 587 nil nil)))
+ ;; (setq smtpmail-default-smtp-server "smtp.gmail.com")
+ ;; (setq smtpmail-smtp-server "smtp.gmail.com")
+ ;; (setq smtpmail-smtp-service 587)
+ (setq smtpmail-debug-info t))
+
+;; BUG: queuing mu4e email doesn't currently work.
+;; if you need offline mode, set these -- and create the queue dir
+;; with 'mu mkdir', i.e.. mu mkdir ~/.mail/queued-mail/"
+;; (setq smtpmail-queue-mail nil)
+;; (setq smtpmail-queue-dir "~/.mail/queued-mail/"))
+
+(provide 'mail-config)
+;;; mail-config.el ends here
diff --git a/modules/markdown-config.el b/modules/markdown-config.el
new file mode 100644
index 00000000..e25dfbf3
--- /dev/null
+++ b/modules/markdown-config.el
@@ -0,0 +1,47 @@
+;;; markdown-config --- Settings for Editing Markdown -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;;;; ------------------------- Markdown-Mode -------------------------
+
+(use-package markdown-mode
+ :mode (("README\\.md\\'" . gfm-mode)
+ ("\\.md\\'" . markdown-mode)
+ ("\\.markdown\\'" . markdown-mode))
+ :bind (:map markdown-mode-map
+ ("<f2>" . markdown-preview)) ;; use same key as compile for consistency
+ :init (setq markdown-command "multimarkdown"))
+
+;;;; ------------------------- Impatient-Mode ------------------------
+
+;; allows for live previews of your html
+;; see: https://github.com/skeeto/impatient-mode
+(use-package impatient-mode
+ :defer t
+ :config
+ (setq imp-set-user-filter 'markdown-html))
+
+;;;; --------------------- WIP: Markdown-Preview ---------------------
+
+;; the filter to apply to markdown before impatient-mode pushes it to the server
+(defun markdown-preview ()
+ (interactive)
+ (httpd-start)
+ (impatient-mode 1)
+ (setq imp-user-filter #'cj/markdown-html)
+ (cl-incf imp-last-state)
+ (imp--notify-clients)
+ ;; (browse-url-generic-function 'browse-url-xdg-open)
+ (browse-url-generic "https://localhost:8080/imp" 1))
+
+(defun cj/markdown-html (buffer)
+ (princ (with-current-buffer buffer
+ (format "<!DOCTYPE html><html><title>Impatient Markdown</title><xmp theme=\"united\" style=\"display:none;\"> %s </xmp><script src=\"http://ndossougbe.github.io/strapdown/dist/strapdown.js\"></script></html>"
+ (buffer-substring-no-properties (point-min) (point-max))))
+ (current-buffer)))
+
+(provide 'markdown-config)
+;;; markdown-config.el ends here
diff --git a/modules/modeline-config.el b/modules/modeline-config.el
new file mode 100644
index 00000000..cea31660
--- /dev/null
+++ b/modules/modeline-config.el
@@ -0,0 +1,36 @@
+;;; modeline-config --- Modeline Settings -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+;; I'm currently working on refactoring and bug fixing for
+;; tautologyclub/feebleline. In transition, I use moodline. Both are good, but I
+;; prefer the slimmer modeline which just uses the echo area.
+
+;; This code allows me to load and test feebleline when it's working directory
+;; exists. Otherwise, it loads and configures moodline.
+
+;;; Code:
+
+(defvar feebleline-working-directory "~/code/feebleline"
+ "The working directory for the feebleline mode line.")
+
+;; --------------------------------- Feebleline --------------------------------
+;; ultrathin simple modeline. adds only useful info to echo area.
+
+(use-package feebleline
+ :if (file-readable-p feebleline-working-directory)
+ :load-path feebleline-working-directory
+ :config
+ (feebleline-mode))
+
+;; ---------------------------------- Moodline ---------------------------------
+;; a sleek and minimalistic modeline.
+
+(use-package mood-line
+ :unless (file-readable-p feebleline-working-directory)
+ :config
+ (setq mood-line-glyph-alist mood-line-glyphs-ascii)
+ (mood-line-mode))
+
+(provide 'modeline-config)
+;;; modeline-config.el ends here
diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el
new file mode 100644
index 00000000..c034aa05
--- /dev/null
+++ b/modules/org-agenda-config.el
@@ -0,0 +1,254 @@
+;;; org-agenda-config --- Org-Agenda/Todo Config -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;; Agenda views are tied to the F8 (fate) key.
+;; f8 - A daily schedule with task items with a scheduled date or deadline of
+;; the current day. This is followed by a task list containing tasks from
+;; all agenda sources.
+;; C-f8 - A task list containing all tasks from all agenda sources
+;; M-f8 - A task list containing all tasks from the current org-mode buffer.
+
+;; NOTE:
+;; Files that contain information relevant to the agenda will be found in the
+;; following places: the schedule-file, org-roam notes tagged as 'projects' and
+;; project todo.org files found in project-dir and code-dir. The function
+;; that rebuilds the agenda list.
+
+;; How the agenda is created:
+;; In order to stay current, the files containing agenda information are queried
+;; before calling the functions in the section org-agenda functions to display
+;; the data.
+
+;; This way, we can maximize flexibility and limit the agenda-files to a smaller
+;; set of files for a scoped agenda.
+
+;;; Code:
+
+(with-eval-after-load 'org-roam
+
+ ;; ----------------------------- Org TODO Settings ---------------------------
+
+ (setq org-todo-keywords '((sequence "TODO(t)" "PROJECT(p)" "DOING(i)"
+ "WAITING(w)" "VERIFY(v)" "STALLED(s)"
+ "DELEGATED(x)" "|"
+ "FAILED(f)" "DONE(d)" "CANCELLED(c)")))
+
+ (setq org-todo-keyword-faces
+ '(("TODO" . "green")
+ ("PROJECT" . "blue")
+ ("DOING" . "yellow")
+ ("WAITING" . "white")
+ ("VERIFY" . "orange")
+ ("STALLED" . "light blue")
+ ("DELEGATED" . "green")
+ ("FAILED" . "red")
+ ("DONE" . "dark grey")
+ ("CANCELLED" . "dark grey")))
+
+ (setq org-highest-priority ?A)
+ (setq org-lowest-priority ?D)
+ (setq org-default-priority ?C)
+ (setq org-priority-faces '((?A . (:foreground "Cyan" :weight bold))
+ (?B . (:foreground "Yellow"))
+ (?C . (:foreground "Green"))
+ (?D . (:foreground "Grey"))))
+
+ (setq org-enforce-todo-dependencies t)
+ (setq org-enforce-todo-checkbox-dependencies t)
+ (setq org-deadline-warning-days 7) ;; warn me w/in a week of deadlines
+ (setq org-log-done nil) ;; don't log when tasks was done
+
+ ;; inherit parents properties (no schedules or deadlines)
+ (setq org-use-property-inheritance t)
+
+ ;; ------------------ Org TODO Next/Previous Set Keybindings -----------------
+
+ (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)))
+
+ ;; ------------------------------ Org Super Agenda -----------------------------
+
+ (use-package org-super-agenda
+ :config (org-super-agenda-mode))
+
+;;;; ORG AGENDA VIEW DEFINITIONS
+ (defun cj/agenda-today-view()
+ "This agenda from all tasks that are scheduled or have a deadline."
+ (setq org-super-agenda-groups
+ '((:log t) ; Automatically named "Log"
+ (:name "SCHEDULED AND DUE"
+ :time-grid t
+ :deadline past
+ :deadline today
+ :scheduled past
+ :scheduled today)
+ (:habit t)
+ (:name "DUE SOON"
+ :deadline future)
+ (:name "LESS IMPORTANT"
+ :scheduled future
+ :order 100)
+ (:discard (:anything t)))))
+
+ (defun cj/agenda-all-view()
+ "This agenda is built from all tasks."
+ (setq org-super-agenda-groups
+ '(
+ (:name "Ready To Go"
+ :todo "STAGED"
+ :todo "READY"
+ :order 5)
+ (:name "Due Today"
+ :deadline past
+ :deadline today
+ :order 2)
+ (:name "Empty Projects"
+ :todo "PROJECT"
+ :order 85)
+ (:name "Delegated"
+ :todo "DELEGATED"
+ :order 50)
+ (:name "Scheduled Later"
+ :scheduled future
+ :order 75)
+ (:name "Scheduled Today"
+ :scheduled today
+ :scheduled past
+ :order 4)
+ (:name "In Progress"
+ :todo "DOING"
+ :order 7)
+ (:name "High Priority"
+ :priority "A"
+ :order 10)
+ (:name "Waiting"
+ :todo "WAITING"
+ :order 60)
+ (:name "Upcoming"
+ :deadline future
+ :order 30)
+ (:name "Next Priority"
+ :priority "B"
+ :order 70)
+ (:name "Everything Else"
+ :anything t
+ :order 90))))
+
+ ;; ------------------------------ Add Agenda Time ------------------------------
+
+ (defun cj/add-agenda-time (s)
+ "Add an event with time S to appear underneath the line-at-point.
+This allows a line to show in an agenda without being scheduled or a deadline."
+ (interactive "sTime: ")
+ (defvar cj/timeformat "%Y-%m-%d %a")
+ (org-end-of-line)
+ (save-excursion
+ (open-line 1)
+ (forward-line 1)
+ (insert (concat "<" (format-time-string cj/timeformat (current-time)) " " s ">" ))))
+
+ (global-set-key (kbd "M-t") #'cj/add-agenda-time)
+
+ ;; ---------------------------- Org Agenda Settings ----------------------------
+
+ (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-dim-blocked-tasks 'invisible)
+ (setq org-agenda-skip-scheduled-if-done nil)
+ (setq org-agenda-skip-include-deadlines t)
+ (setq org-agenda-remove-tags t)
+ (setq org-agenda-compact-blocks t)
+
+ ;; ------------------------ Add Files To Org Agenda List -----------------------
+ ;; finds files named 'todo.org' (case insensitive) and adds them to
+ ;; org-agenda-files list.
+
+ (defun cj/add-files-to-org-agenda-files (directory)
+ "Recursively searches for files named 'todo.org'.
+Searches in DIRECTORY and adds them to org-project-files."
+ (interactive "D")
+ (setq org-agenda-files
+ (append org-agenda-files
+ (directory-files-recursively directory "^[Tt][Oo][Dd][Oo]\\.[Oo][Rr][Gg]$" t))))
+
+ ;; NOTE: the following functions require org-roam functionality
+ (with-eval-after-load 'org-roam-config
+
+ ;; ---------------------------- Rebuild Org Agenda ---------------------------
+
+ (defun cj/build-org-agenda-list ()
+ "Rebuilds the org agenda list."
+ (interactive)
+ ;; reset org-agenda-files to inbox-file
+ (setq org-agenda-files (list inbox-file))
+ (let ((new-files
+ (append
+ (cj/org-roam-list-notes-by-tag "Project"))))
+ (dolist (file new-files)
+ (unless (member file org-agenda-files)
+ (setq org-agenda-files (cons file org-agenda-files)))))
+
+ (cj/add-files-to-org-agenda-files projects-dir)
+ (cj/add-files-to-org-agenda-files code-dir))
+
+ ;; build org-agenda-list for the first time once emacs init is complete.
+ (add-hook 'emacs-startup-hook 'cj/build-org-agenda-list)
+
+ ;; ------------------------ Org Agenda Display Functions -----------------------
+
+ (defun cj/agenda-all-agenda-files-day ()
+ "Display an \'org-agenda\' schedule with tasks covering today.
+The contents of the agenda will be built from org-project-files and org-roam
+files that have project in their filetag."
+ (interactive)
+ (cj/build-org-agenda-list)
+ (setq org-agenda-span 'day)
+ (cj/agenda-today-view)
+ (org-agenda "a" "a"))
+ (global-set-key (kbd "<f8>") #'cj/agenda-all-agenda-files-day)
+
+
+ (defun cj/agenda-all-agenda-files-week ()
+ "Display an 'org-agenda' schedule with tasks covering this week.
+The contents of the agenda will be built from org-project-files and org-roam
+files that have project in their filetag."
+ (interactive)
+ (cj/build-org-agenda-list)
+ (setq org-agenda-span 'week)
+ (cj/agenda-today-view)
+ (org-agenda "a" "a"))
+ (global-set-key (kbd "s-<f8>") #'cj/agenda-all-agenda-files-week)
+
+ (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)
+ (cj/build-org-agenda-list)
+ (cj/agenda-all-view)
+ (org-agenda "a" "t"))
+ (global-set-key (kbd "C-<f8>") #'cj/todo-list-all-agenda-files)
+
+ (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)
+ (let ((org-agenda-files (list buffer-file-name)))
+ (cj/agenda-all-view)
+ (org-agenda "a" "t"))
+ (message (concat "Your org agenda request based on '" (buffer-name (current-buffer))
+ "' failed because it's not an org buffer."))))
+ (global-set-key (kbd "M-<f8>") #'cj/todo-list-from-this-buffer)
+
+ ) ;; end with-eval-after-load 'org-roam-config
+ ) ;; end with-eval-after-load 'org-roam
+
+(provide 'org-agenda-config)
+;;; org-agenda-config.el ends here
diff --git a/modules/org-appearance-config.el b/modules/org-appearance-config.el
new file mode 100644
index 00000000..7522b5c5
--- /dev/null
+++ b/modules/org-appearance-config.el
@@ -0,0 +1,64 @@
+;;; org-appearance-config.el --- Org-Mode UI Appearance Settings -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(with-eval-after-load 'org
+
+ ;; face settings need to be called every time org mode is loaded.
+ (defun cj/set-org-face-settings()
+ "Sets foreground, background, and font styles for org mode.
+To be called every time org is loaded."
+ (interactive)
+ ;; org-hide should use fix-pitch to align indents for proportional fonts
+ (set-face-attribute 'org-hide nil :inherit 'fixed-pitch)
+ (set-face-attribute 'org-meta-line nil :inherit 'shadow)
+
+ ;; Remove foreground and background from block faces
+ (set-face-attribute 'org-block nil :foreground 'unspecified :background 'unspecified)
+ (set-face-attribute 'org-block-begin-line nil :foreground 'unspecified :background 'unspecified)
+ (set-face-attribute 'org-block-end-line nil :foreground 'unspecified :background 'unspecified)
+
+ ;; Get rid of the background on column views
+ (set-face-attribute 'org-column nil :background 'unspecified)
+ (set-face-attribute 'org-column-title nil :background 'unspecified)
+
+ ;; make sure org-links are underlined
+ (set-face-attribute 'org-link nil :underline t)
+
+ ;; remove hook after first run to avoid calling function everytime a frame is made
+ (if (daemonp)
+ (remove-hook 'server-after-make-frame-hook #'cj/set-org-face-settings)))
+
+ ;; if emacsclient, setup hook to run font settings function, otherwise, run it now
+ (if (daemonp)
+ (add-hook 'server-after-make-frame-hook #'cj/set-org-face-settings)
+ (cj/set-org-face-settings))
+
+
+ ;; settings need to be called only once
+ (setq org-ellipsis " ▾") ;; change ellipses to down arrow
+ (setq org-hide-emphasis-markers t) ;; remove emphasis markers to keep the screen clean
+ (setq org-hide-leading-stars t) ;; hide leading stars, just show one per line
+ (setq org-pretty-entities t) ;; render special symbols
+ (setq org-pretty-entities-include-sub-superscripts nil) ;; ...except superscripts and subscripts
+ (setq org-fontify-emphasized-text nil) ;; ...and don't render bold and italic markup
+ (setq org-fontify-whole-heading-line t) ;; fontify the whole line for headings (for face-backgrounds)
+ (add-hook 'org-mode-hook 'prettify-symbols-mode)
+
+
+
+ ;; nicer bullets than simple asterisks.
+ (use-package org-superstar
+ :after org
+ :config
+ (org-superstar-configure-like-org-bullets)
+ (setq org-superstar-leading-bullet ?\s)
+ (add-hook 'org-mode-hook (lambda () (org-superstar-mode 1))))
+
+ ) ;; end with-eval-after-load
+
+(provide 'org-appearance-config)
+;;; org-appearance-config.el ends here.
diff --git a/modules/org-babel-config.el b/modules/org-babel-config.el
new file mode 100644
index 00000000..dfb9ca14
--- /dev/null
+++ b/modules/org-babel-config.el
@@ -0,0 +1,88 @@
+;;; org-babel-config.el --- Org Babel/Tempo Config -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; All Org-Babel and Org-Tempo Packages, Settings, and Languages.
+
+;;; Code:
+
+(with-eval-after-load 'org
+ (require 'ob-core)
+
+ ;; ------------------- Babel Execution Confirmation Toggle -------------------
+ ;; org-babel verifies before each execution
+
+ (defun babel-confirm (flag)
+ "Report the setting of org-confirm-babel-evaluate.
+ If invoked with C-u, toggle the setting."
+ (interactive "P")
+ (if (equal flag '(4))
+ (setq org-confirm-babel-evaluate (not org-confirm-babel-evaluate)))
+ (message "Babel evaluation confirmation is %s"
+ (if org-confirm-babel-evaluate "on" "off")))
+
+ ;; ---------------------------- Org Babel Languages ----------------------------
+ ;; create executable code blocks in a language within org-mode
+
+ ;; (org-babel-do-load-languages
+ ;; 'org-babel-load-languages
+ ;; '((shell . t)))
+
+ (use-package ob-python
+ :ensure nil ;; built-in
+ :defer .5
+ :after org-contrib
+ :commands (org-babel-execute:python))
+
+ (use-package ob-emacs-lisp
+ :ensure nil ;; built-in
+ :defer .5
+ :commands
+ (org-babel-execute:emacs-lisp
+ org-babel-expand-body:emacs-lisp))
+
+ (use-package ob-dot
+ :ensure nil ;; built-in
+ :defer .5
+ :commands
+ (org-babel-execute:dot
+ org-babel-expand-body:dot)
+ :config
+ ;; https://stackoverflow.com/questions/16770868/org-babel-doesnt-load-graphviz-editing-mode-for-dot-sources
+ (add-to-list 'org-src-lang-modes (quote ("dot" . graphviz-dot))))
+
+ ;; --------------------------------- Org-Tempo ---------------------------------
+ ;; expands snippets to babel code blocks using templates
+
+ (use-package org-tempo
+ :defer .5
+ :ensure nil ;; built-in
+ :after org
+ :config
+ (add-to-list 'org-structure-template-alist '("awk" . "src awk"))
+ (add-to-list 'org-structure-template-alist '("bash" . "src bash"))
+ (add-to-list 'org-structure-template-alist '("dot" . "src dot :file temp.png :cmdline -Kdot -Tpng"))
+ (add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp"))
+ (add-to-list 'org-structure-template-alist '("json" . "src json"))
+ (add-to-list 'org-structure-template-alist '("latex" . "src latex"))
+ (add-to-list 'org-structure-template-alist '("sicp" . "src racket :lang sicp"))
+ (add-to-list 'org-structure-template-alist '("py" . "src python"))
+ (add-to-list 'org-structure-template-alist '("sh" . "src sh"))
+ (add-to-list 'org-structure-template-alist '("yaml" . "src yaml")))
+
+ ;; ---------------------------- Org Babel Settings ---------------------------
+ ;; general org babel settings
+
+ (setq org-src-fontify-natively t) ;; fontify the code in blocks
+ (setq org-src-tab-acts-natively t) ;; tabs act like in language major mode buffer
+ (setq org-src-window-setup 'current-window) ;; don't split window when source editing wih C-c '
+ (setq org-confirm-babel-evaluate nil) ;; just evaluate the source code
+ (setq org-babel-default-header-args
+ (cons '(:tangle . "yes")
+ (assq-delete-all :tangle org-babel-default-header-args))) ;; default header args for babel
+ (add-hook 'org-babel-after-execute-hook 'org-redisplay-inline-images) ;; for seeing inline dotgraph images
+
+ ) ;; end with-eval-after-load
+
+
+(provide 'org-babel-config)
+;;; org-babel-config.el ends here.
diff --git a/modules/org-capture-config.el b/modules/org-capture-config.el
new file mode 100644
index 00000000..2ce8494b
--- /dev/null
+++ b/modules/org-capture-config.el
@@ -0,0 +1,107 @@
+;;; org-capture-config.el --- Org Capture/Refile -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Customizations related to org-capture and org-refile is here.
+;; This includes 'web-clipper' functionality.
+
+;; To ensure the code below is only loaded after org-mode, all code is wrapped in an
+;; eval-after-load function.
+
+
+;;; Code:
+
+
+(with-eval-after-load 'org
+
+ ;; ---------------------- Org-Website-Clipper ----------------------
+ ;; Saves a copy of the page eww is visiting in an org file for offline reading
+ ;; In other words, it's a "Poor Man's Pocket/Instapaper"
+
+
+ (defun org-website-clipper ()
+ "Capture a web page for later viewing in an org-file.
+Encodes all links and marks that may interfere with org mode
+display, then inserts the content into a file for later offline use.
+This is meant to be used in coordination with an org-capture-template.
+
+Example Template:
+ (\"w\" \"Website\" plain (function org-website-clipper)
+ \"* %a\\nCaptured On: %U\\n\" :immediate-finish t)))
+
+Requires Emacs 15 and the 2017-02-12 or later version of org-eww.el."
+ (interactive)
+
+ ;; Ensure valid major mode before encoding
+ (cond
+ ((eq major-mode 'w3m-mode)
+ (org-w3m-copy-for-org-mode))
+ ((eq major-mode 'eww-mode)
+ (org-eww-copy-for-org-mode))
+ (t
+ (error "Not valid -- must be in w3m or eww mode")))
+
+ ;; Check for full path to the archive file. Create missing directories.
+ (unless (file-exists-p article-file)
+ (let ((dir (file-name-directory article-file)))
+ (unless (file-exists-p dir)
+ (make-directory dir))))
+
+ ;; Move to end of file and insert blank line for org-capture to add timestamp, etc.
+ (find-file article-file)
+ (goto-char (point-max))
+ (insert "\n\n\n\n\n")
+
+ ;; Insert the web content keeping our place.
+ (save-excursion (yank))
+
+ ;; Remove page info from kill ring. Also, fix the yank pointer.
+ (setq kill-ring (cdr kill-ring))
+ (setq kill-ring-yank-pointer kill-ring)
+
+ ;; Final repositioning.
+ (forward-line -1))
+
+;;;; --------------------------------- Functions -------------------------------
+
+ (defun org-capture-pdf-active-region ()
+ "Capture the active region of the pdf-view buffer.
+Intended to be called within an org capture template."
+ (let* ((pdf-buf-name (plist-get org-capture-plist :original-buffer))
+ (pdf-buf (get-buffer pdf-buf-name)))
+ (if (buffer-live-p pdf-buf)
+ (with-current-buffer pdf-buf
+ (car (pdf-view-active-region-text)))
+ (user-error "Buffer %S not alive." pdf-buf-name))))
+
+;;;; --------------------------- Org-Capture Templates -------------------------
+
+ ;; ORG-CAPTURE TEMPLATES
+ (setq org-protocol-default-template-key "L")
+ (setq org-capture-templates
+ '(
+ ("e" "Event" entry (file+headline schedule-file "Scheduled Events")
+ "* %?\nWHEN: %^t" :prepend t)
+
+ ("E" "Epub Text" entry (file+headline inbox-file "Inbox")
+ "* %?\n#+BEGIN_QUOTE\n %i\n#+END_QUOTE\nSource: [[%:link][%(buffer-name (org-capture-get :original-buffer))]]\nCaptured On: %U" :prepend t)
+
+ ("P" "PDF Text" entry (file+headline inbox-file "Inbox")
+ "* %?\n#+BEGIN_QUOTE\n %(org-capture-pdf-active-region)\n#+END_QUOTE\nSource:[[%L][%(buffer-name (org-capture-get :original-buffer))]]\nCaptured On: %U" :prepend t)
+
+ ("p" "Link with Selection" entry (file+headline inbox-file "Inbox")
+ "* TODO %?\n#+BEGIN_QUOTE\n%i\n#+END_QUOTE\n[[%:link][%:description]]\nCaptured On: %U" :prepend t)
+
+ ("L" "Link" entry (file+headline inbox-file "Inbox")
+ "* TODO %?\n[[%:link][%:description]]\nCaptured On: %U" :prepend t)
+
+ ("m" "Mu4e Email" entry (file+headline inbox-file "Inbox")
+ "* TODO %?\n[[%:link][%:description]]\nCaptured On: %U" :prepend t)
+
+ ("w" "Website" plain
+ (function org-website-clipper)
+ "* %a\nArticle Link: %L\nCaptured On: %U\n\n" :immediate-finish t)))
+
+ ) ;; end with-eval-after-load 'org
+
+(provide 'org-capture-config)
+;;; org-capture-config.el ends here.
diff --git a/modules/org-config.el b/modules/org-config.el
new file mode 100644
index 00000000..1bfcf704
--- /dev/null
+++ b/modules/org-config.el
@@ -0,0 +1,154 @@
+;;; org-config --- Settings and Enhancements to Org Mode -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+;;; Commentary:
+
+;; Note:
+;; Setting org-modules to org-protocol, ol-eww, ol-w3m, and ol-info removes
+;; several modules that org would otherwise load automatically.
+
+;; For clarity and reference sake, this is what's removed in Emacs 29.1:
+;; ol-doi - links to Digital Object Identifiers. See: https://www.doi.org/
+;; ol-bbdb - implements links to BBDB database entries
+;; ol-bibtex - implements links to database entries in BibTeX files
+;; ol-docview - implements links to open files in doc-view-mode
+;; ol-gnus - implements links to Gnus groups and messages
+;; ol-irc - implements links to an IRC session
+;; ol-mhe - implements links to MH-E (Rand Mail Handler) messages
+;; ol-rmail - implements links to Rmail messages
+
+;;; Code:
+
+;;;; --------------------------- Constants ---------------------------
+
+;; note: some constants used here are defined in init.el
+(defvar org-archive-location (concat sync-dir "/archives/archive.org::datetree/")) ;; location of archive file
+(defvar org-project-files (list schedule-file))
+
+;; ---------------------------- APT Sorting Function ---------------------------
+
+(defun cj/org-reorder-list-apt ()
+ "Sort the org header by three criteria: alpha, pri, then todo."
+ (interactive)
+ (save-excursion
+ (ignore-errors
+ (progn
+ (org-sort-entries t ?a)
+ (org-sort-entries t ?p)
+ (org-sort-entries t ?t)
+ (org-cycle)
+ (org-cycle)))))
+
+;; ---------------------------------- Org Mode ---------------------------------
+
+(use-package org
+ :defer .5
+ :ensure nil ;; use the built-in package
+ :pin manual ;; never upgrade from the version built-into Emacs
+ :preface
+ ;; create an org-table-map so you can use C-c t as prefix
+ (define-prefix-command 'org-table-map)
+ (global-set-key (kbd "C-c T") 'org-table-map)
+ :bind
+ ("C-c c" . org-capture)
+ ("C-c a" . org-agenda)
+ (:map org-mode-map
+ ("C-c ?" . hydra-general/body) ;; was org-table-field-info
+ ("C-c I" . org-table-field-info) ;; was C-c ?
+ ("C-c w" . cj/org-refile-in-file)
+ ("C-\\" . org-match-sparse-tree)
+ ("C-c t" . org-set-tags-command)
+ ("C-c l" . org-store-link)
+ ("C-c r" . cj/org-reorder-list-apt)
+ ("C-c C-l" . org-insert-link)
+ ("s-<up>" . org-priority-up)
+ ("s-<down>" . org-priority-down)
+ ("C-c N" . org-narrow-to-subtree)
+ ("C-c >" . cj/org-narrow-forward)
+ ("C-c <" . cj/org-narrow-backwards)
+ ("<f5>" . org-reveal)
+ ("C-c <ESC>" . widen))
+ (:map org-table-map
+ ("r i" . org-table-insert-row)
+ ("r d" . org-table-kill-row)
+ ("c i" . org-table-insert-column)
+ ("c d" . org-table-delete-column))
+
+ ;; backward and forward day are ',' and '.'
+ ;; shift & meta moves by week or year
+ ;; C-. jumps to today
+ ;; original keybindings blocked by windmove keys
+ ;; these are consistent with plain-old calendar mode
+ (:map org-read-date-minibuffer-local-map
+ ("," . (lambda () (interactive)
+ (org-eval-in-calendar '(calendar-backward-day 1))))
+ ("." . (lambda () (interactive)
+ (org-eval-in-calendar '(calendar-forward-day 1))))
+ ("<" . (lambda () (interactive)
+ (org-eval-in-calendar '(calendar-backward-month 1))))
+ (">" . (lambda () (interactive)
+ (org-eval-in-calendar '(calendar-forward-month 1))))
+ ("M-," . (lambda () (interactive)
+ (org-eval-in-calendar '(calendar-backward-year 1))))
+ ("M-." . (lambda () (interactive)
+ (org-eval-in-calendar '(calendar-forward-year 1)))))
+
+ :init
+ ;; windmove's keybindings conflict with org-agenda-todo-nextset/previousset keybindings
+ ;; solution: map the super key so that
+ ;; - super up/down increases and decreases the priority
+ ;; - super left/right changes the todo state
+ (setq org-replace-disputed-keys t)
+ (custom-set-variables
+ '(org-disputed-keys
+ '(([(shift left)] . [(super left)])
+ ([(shift right)] . [(super right)])
+ ([(shift up)] . [(super up)])
+ ([(shift down)] . [(super down)])
+ ([(control shift right)] . [(meta shift +)])
+ ([(control shift left)] . [(meta shift -)]))))
+ :hook
+ (org-mode . flyspell-mode)
+ (org-mode . turn-on-visual-line-mode)
+ (org-mode . org-indent-mode)
+ (org-mode . (lambda () (interactive) (company-mode -1))) ;; no company-mode in org
+
+ :config
+ ;; Unbind org-cycle-agenda-files keys for use elsewhere
+ (unbind-key "C-'" org-mode-map)
+ (unbind-key "C-," org-mode-map)
+
+
+ ;; ORG-PROTOCOL
+ ;; enable recognition of org-protocol:// as a parameter
+ (require 'org-protocol)
+ (setq org-modules '(org-protocol ol-eww ol-w3m ol-info))
+
+ ;; GENERAL
+ (setq org-startup-folded t) ;; all org files should start in the folded state
+ (setq org-cycle-open-archived-trees t) ;; re-enable opening headings with archive tags with TAB
+ (setq org-outline-path-complete-in-steps nil)
+ (setq org-return-follows-link t) ;; hit return to follow an org-link
+ (setq org-list-allow-alphabetical t) ;; allow alpha ordered lists (i.e., a), A), a., etc.)
+
+ ;; INDENTATION
+ (setq org-startup-indented t) ;; load org files indented
+ (setq org-adapt-indentation t) ;; adapt indentation to outline node level
+ (setq org-indent-indentation-per-level 2) ;; indent two character-widths per level
+
+ ;; INLINE IMAGES
+ (setq org-startup-with-inline-images t) ;; preview images by default
+ (setq org-image-actual-width '(500)) ;; keep image
+ ;; sizes in check
+
+ (setq org-bookmark-names-plist nil) ;; don't set org-capture bookmarks
+
+ ;; force pdfs exported from org to open in emacs
+ (add-to-list 'org-file-apps '("\\.pdf\\'" . emacs)))
+
+
+;; https://www.reddit.com/r/orgmode/comments/n56fcv/important_the_contrib_directory_now_lives_outside/
+;; (use-package org-contrib
+;; :after org)
+
+(provide 'org-config)
+;;; org-config.el ends here
diff --git a/modules/org-contacts-config.el b/modules/org-contacts-config.el
new file mode 100644
index 00000000..489d7a33
--- /dev/null
+++ b/modules/org-contacts-config.el
@@ -0,0 +1,30 @@
+;;; org-contacts-config.el --- Org Contacts Customizations -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(use-package org-contacts
+ :after (org org-contrib mu4e)
+ :defer 1
+ :bind ("C-z C" . org-contacts) ; starts contacts search
+ :config
+ (setq org-contacts-files (cons contacts-file '()))
+
+ (add-to-list 'org-capture-templates
+ '("c" "Contact" entry (file+headline contacts-file "Contacts")
+ "*%?\n:PROPERTIES:\n:ADDRESS: \n:PHONE: \n:EMAIL: \n:BIRTHDAY: \n:NOTES: \n:END:" :prepend t))
+
+
+ (setq mu4e-contacts-file contacts-file)
+
+ (add-to-list 'mu4e-headers-actions
+ '("add contact" . mu4e-action-add-org-contact))
+ (add-to-list 'mu4e-view-actions
+ '("add contact" . mu4e-action-add-org-contact))
+ (add-to-list 'mu4e-view-actions
+ '("download attachments" . cj/mu4e-view-save-attachments)))
+
+(provide 'org-contacts-config)
+;;; org-contacts-config.el ends here.
diff --git a/modules/org-drill-config.el b/modules/org-drill-config.el
new file mode 100644
index 00000000..f2120c33
--- /dev/null
+++ b/modules/org-drill-config.el
@@ -0,0 +1,88 @@
+;;; org-drill-config.el --- Org Drill Settings -*- lexical-binding: t; -*-
+;;; Commentary:
+;;
+;; Notes: Org-Drill
+;; Start out your org-drill with C-d s, then select your file.
+
+;; the javascript bookmark I use to capture information from the web is below:
+;; javascript:location.href='org-protocol://capture?template=d&url=%27+encodeURIComponent(location.href)+%27&title=%27+encodeURIComponent(document.title)+%27&body=%27+encodeURIComponent(window.getSelection())
+
+;; create a new bookmark and add "Drill Entry" to the name field and the above
+;; snippet to the URL field.
+
+;; ----------------------------------- Tasks -----------------------------------
+
+
+;;; Code:
+
+(defvar org-drill-files (directory-files (concat sync-dir "drill/")
+ t directory-files-no-dot-files-regexp))
+
+(with-eval-after-load 'org
+ (cj/merge-list-to-list
+ 'org-capture-templates
+ '(("d" "Drill Question - Web"
+ entry
+ (file (lambda () (completing-read "Choose file: " org-drill-files)))
+ "* Item :drill:\n%?\n** Answer\n%i\nSource: [[%:link][%:description]]\nCaptured On: %U" :prepend t)
+ ("b" "Drill Question - EPUB"
+ entry
+ (file (lambda () (completing-read "Choose file: " org-drill-files)))
+ "* Item :drill:\n%?\n** Answer\n%i\nSource: [[%:link][%(buffer-name (org-capture-get :original-buffer))]]\nCaptured On: %U" :prepend t)
+ ("f" "Drill Question - PDF"
+ entry
+ (file (lambda () (completing-read "Choose file: " org-drill-files)))
+ "* Item :drill:\n%?\n** Answer\n%(org-capture-pdf-active-region)\nSource:[[%L][%(buffer-name (org-capture-get :original-buffer))]]\nCaptured On: %U" :prepend t))))
+
+
+(use-package org-drill
+ :after org
+ :preface
+ (defun cj/drill-start ()
+ "Prompt user to pick a drill org file, then starts an org-drill session."
+ (interactive)
+ (let ((choices org-drill-files))
+ (setq chosen-drill-file (completing-read "Choose Flashcard File:" choices ))
+ (find-file chosen-drill-file)
+ (org-drill)))
+
+ (defun cj/drill-edit ()
+ "Prompts the user to pick a drill org file, then opens it for editing."
+ (interactive)
+ (let* ((choices org-drill-files)
+ (chosen-drill-file (completing-read "Choose Flashcards to Edit:" choices)))
+ (find-file chosen-drill-file)))
+
+ (defun cj/drill-capture ()
+ "Quickly capture a drill question."
+ (interactive)
+ (org-capture nil "d"))
+
+ (defun cj/drill-refile ()
+ "Refile to a drill file."
+ (interactive)
+ (setq org-refile-targets '((nil :maxlevel . 1)
+ (org-drill-files :maxlevel . 1)))
+ (call-interactively 'org-refile))
+
+ ;; create an org-drill-map so you can use C-d as prefix
+ (define-prefix-command 'org-drill-map)
+ (global-set-key (kbd "C-d") 'org-drill-map)
+ :bind
+ (:map org-drill-map
+ ("s" . cj/drill-start)
+ ("e" . cj/drill-edit)
+ ("c" . cj/drill-capture)
+ ("r" . cj/drill-refile)
+ ("R" . org-drill-resume))
+ :config
+ (setq org-drill-leech-failure-threshold 50) ;; leech cards = 50 wrong anwers
+ (setq org-drill-leech-method 'warn) ;; leech cards show warnings
+ (setq org-drill-use-visible-cloze-face-p t) ;; cloze text show up in a different font
+ (setq org-drill-hide-item-headings-p t) ;; don't show heading text
+ (setq org-drill-maximum-items-per-session 1000) ;; drill sessions end after 1000 cards
+ (setq org-drill-maximum-duration 60) ;; each drill session can last up to a an hour
+ (setq org-drill-add-random-noise-to-intervals-p t)) ;; slightly vary number of days to repetition
+
+(provide 'org-drill-config)
+;;; org-drill-config.el ends here.
diff --git a/modules/org-export-config.el b/modules/org-export-config.el
new file mode 100644
index 00000000..befb4361
--- /dev/null
+++ b/modules/org-export-config.el
@@ -0,0 +1,39 @@
+;;; org-export-config.el --- Org Export Configuration -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;;; Code:
+
+;;;; ------------------------- Org-Exporting -------------------------
+
+(with-eval-after-load 'org
+
+ ;; markdown
+ (use-package ox-md
+ :ensure nil ;; built-in
+ :after ox)
+
+ ;; hugo markdown
+ (use-package ox-hugo
+ :after ox)
+
+ ;; beamer presentations
+ (use-package ox-beamer
+ :ensure nil ;; built-in
+ :after ox)
+
+ ;; ORG EXPORT
+ (setq org-export-coding-system 'utf-8) ;; force utf-8 in org
+ (setq org-export-headline-levels 6) ;; export headlines 6 levels deep
+ (setq org-export-initial-scope 'subtree) ;; export the current subtree by default
+ (setq org-export-with-author nil) ;; export without author by default
+ (setq org-export-with-section-numbers nil) ;; export without section numbers by default
+ (setq org-export-with-tags nil) ;; export without tags by default
+ (setq org-export-with-tasks nil) ;; export without tasks by default
+ (setq org-export-with-toc nil) ;; export without table of contents by default
+
+ ) ;; end with-eval-after-load
+
+
+(provide 'org-export-config)
+;;; org-export-config.el ends here.
diff --git a/modules/org-refile-config.el b/modules/org-refile-config.el
new file mode 100644
index 00000000..8c058d66
--- /dev/null
+++ b/modules/org-refile-config.el
@@ -0,0 +1,62 @@
+;;; org-refile-config.el --- Org Refile Customizations -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(with-eval-after-load 'org-roam-config
+ (require 'org-refile)
+
+ ;; ----------------------------- Org Refile Targets ----------------------------
+ ;; sets refile targets
+ ;; - adds project files in org-roam to the refile targets
+ ;; - adds todo.org files in subdirectories of the code and project directories
+
+ (defun cj/add-files-to-org-refile-targets (directory)
+ "Recursively searches for all files named 'todo.org' in DIRECTORY and adds them to org-project-files."
+ (interactive "D")
+ (let ((files (directory-files-recursively directory "^[Tt][Oo][Dd][Oo]\\.[Oo][Rr][Gg]$" t)))
+ (dolist (file files)
+ (add-to-list 'org-refile-targets `(,file . (:level . 1))))))
+
+ (defun cj/build-org-refile-targets()
+ "Build org-refile-targets.
+Starts with the schedule file, then adds the Emacs task list,
+and any task list in the code or projects directories."
+ (interactive)
+ (let ((new-files
+ (append
+ (cj/org-roam-list-notes-by-tag "Project")
+ (cj/org-roam-list-notes-by-tag "Topic"))))
+ (dolist (file new-files)
+ (unless (member file org-agenda-files)
+ (setq org-agenda-files (cons file org-agenda-files))))
+ )
+ (setq org-refile-targets '((org-agenda-files :maxlevel . 1)))
+ (cj/add-files-to-org-refile-targets user-emacs-directory)
+ (cj/add-files-to-org-refile-targets code-dir)
+ (cj/add-files-to-org-refile-targets projects-dir))
+
+ ;; --------------------------------- Org-Refile -------------------------------
+ ;; convenience function for scoping the refile candidates to the current buffer.
+
+ (defun cj/org-refile-in-file ()
+ "Refile to a target within the current file and save the buffer."
+ (interactive)
+ (let ((org-refile-targets `(((,(buffer-file-name)) :maxlevel . 6))))
+ (call-interactively 'org-refile)
+ (save-buffer)))
+
+ ;; ----------------------- Save Org Files After Refile -----------------------
+ ;; advice that saves all open org buffers after a refile is complete
+
+ (advice-add 'org-refile :after
+ (lambda (&rest _)
+ (org-save-all-org-buffers)))
+
+
+ ) ;; end with eval-after-load 'org-roam
+
+(provide 'org-refile-config)
+;;; org-refile-config.el ends here.
diff --git a/modules/org-roam-config.el b/modules/org-roam-config.el
new file mode 100644
index 00000000..1852cde5
--- /dev/null
+++ b/modules/org-roam-config.el
@@ -0,0 +1,192 @@
+;;; org-roam-config.el --- Org-Roam Config -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Currently a work in progress. The initial version of this was taken from David Wilson:
+;; https://systemcrafters.net/build-a-second-brain-in-emacs/5-org-roam-hacks/
+
+;;; Code:
+
+
+;; ---------------------------------- Org Roam ---------------------------------
+
+(use-package org-roam
+ :after org
+ :defer .5
+ :custom
+ (org-roam-directory "~/sync/org/roam/")
+ (org-roam-dailies-directory "journal/")
+ (org-roam-completion-everywhere t)
+ (org-roam-dailies-capture-templates
+ '(("d" "default" entry "* %<%I:%M:%S %p %Z> %?"
+ :if-new (file+head "%<%Y-%m-%d>.org" "#+FILETAGS: Journal\n#+TITLE: %<%Y-%m-%d>"))))
+ (org-roam-capture-templates
+ '(("d" "default" plain
+ "%?"
+ :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+TITLE: ${title}\n")
+ :unnarrowed t)
+ ("v" "v2mom" plain (file "~/sync/org/roam/templates/v2mom.org")
+ :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+TITLE: ${title}\n")
+ :unnarrowed t)
+ ("r" "recipe" plain (file "~/sync/org/roam/templates/recipe.org")
+ :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+TITLE: ${title}\n#+CATEGORY: ${title}\n#+FILETAGS: Recipe")
+ :unnarrowed t)
+ ("p" "project" plain (file "~/sync/org/roam/templates/project.org")
+ :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+TITLE: ${title}\n#+CATEGORY: ${title}\n#+FILETAGS: Project")
+ :unnarrowed t)
+ ("t" "topic" plain (file "~/sync/org/roam/templates/topic.org")
+ :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+TITLE: ${title}\n#+CATEGORY: ${title}\n#+FILETAGS: Topic")
+ :unnarrowed t)))
+ :bind (("C-c n ?" . org-roam-hydra/body)
+ ("C-c n l" . org-roam-buffer-toggle)
+ ("C-c n f" . org-roam-node-find)
+ ("C-c n p" . cj/org-roam-find-project)
+ ("C-c n r" . cj/org-roam-find-recipe)
+ ("C-c n t" . cj/org-roam-find-topic)
+ ("C-c n i" . org-roam-node-insert)
+ :map org-mode-map
+ ("C-M-i" . completion-at-point)
+ :map org-roam-dailies-map
+ ("Y" . org-roam-dailies-capture-yesterday)
+ ("T" . org-roam-dailies-capture-tomorrow))
+ :bind-keymap
+ ("C-c n d" . org-roam-dailies-map)
+ :config
+ (setq org-log-done 'time)
+ (setq org-agenda-timegrid-use-ampm t)
+
+ ;; remove/disable if performance slows
+ ;; (setq org-element-use-cache nil) ;; disables caching org files
+
+ ;; move closed tasks to today's journal when marked done
+ (add-to-list 'org-after-todo-state-change-hook
+ (lambda ()
+ (when (equal org-state "DONE")
+ (cj/org-roam-copy-todo-to-today))))
+
+ (require 'org-roam-dailies) ;; Ensures the keymap is available
+ (org-roam-db-autosync-mode))
+
+;; ------------------------- Org Roam Insert Immediate -------------------------
+
+(defun cj/org-roam-node-insert-immediate (arg &rest args)
+ "Create new node and insert its link immediately.
+The prefix ARG .
+ARGS represents the node name to link."
+ (interactive "P")
+ (let ((args (cons arg args))
+ (org-roam-capture-templates (list (append (car org-roam-capture-templates)
+ '(:immediate-finish t)))))
+ (apply #'org-roam-node-insert args)))
+(global-set-key (kbd "C-c n I") 'cj/org-roam-node-insert-immediate)
+
+;; ------------------------- Tag Listing And Filtering -------------------------
+
+(defun cj/org-roam-filter-by-tag (tag-name)
+ (lambda (node)
+ (member tag-name (org-roam-node-tags node))))
+
+(defun cj/org-roam-list-notes-by-tag (tag-name)
+ (mapcar #'org-roam-node-file
+ (seq-filter
+ (cj/org-roam-filter-by-tag tag-name)
+ (org-roam-node-list))))
+
+;; -------------------------- Org Roam Find Functions --------------------------
+
+(defun cj/org-roam-find-node (tag template-key template-file)
+ "List all node of type \\=`TAG\\=` in completing read for selection or creation."
+ (interactive)
+ ;; Add the project file to the agenda after capture is finished
+ (add-hook 'org-capture-after-finalize-hook #'cj/org-roam-add-node-to-agenda-files-finalize-hook)
+
+ ;; Select a project file to open, creating it if necessary
+ (org-roam-node-find
+ nil
+ nil
+ (cj/org-roam-filter-by-tag tag)
+ nil
+ :templates
+ `((,template-key ,tag plain (file ,template-file)
+ :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" ,(concat "#+TITLE: ${title}\n#+CATEGORY: ${title}\n#+FILETAGS: " tag))
+ :unnarrowed t))))
+
+(defun cj/org-roam-find-topic ()
+ "List all node of type \\=`topic\\=` in completing read for selection or creation."
+ (interactive)
+ (cj/org-roam-find-node "Topic" "t" "~/sync/org/roam/templates/topic.org"))
+
+(defun cj/org-roam-find-recipe ()
+ "List all node of type \\=`recipe\\=` in completing read for selection or creation."
+ (interactive)
+ (cj/org-roam-find-node "Recipe" "r" "~/sync/org/roam/templates/recipe.org"))
+
+(defun cj/org-roam-find-project ()
+ "List all node of type \\='project\\=' in completing read for selection or creation."
+ (interactive)
+ (cj/org-roam-find-node "Project" "p" "~/sync/org/roam/templates/project.org"))
+
+;; ---------------------- Org Capture After Finalize Hook ----------------------
+
+(defun cj/org-roam-add-node-to-agenda-files-finalize-hook ()
+ "Add the captured project file to \\='org-agenda-files\\='."
+ ;; Remove the hook since it was added temporarily
+ (remove-hook 'org-capture-after-finalize-hook #'cj/org-roam-add-node-to-agenda-files-finalize-hook)
+
+ ;; Add project file to the agenda list if the capture was confirmed
+ (unless org-note-abort
+ (with-current-buffer (org-capture-get :buffer)
+ (add-to-list 'org-agenda-files (buffer-file-name)))))
+
+;; ------------------------------- Capture Inbox -------------------------------
+
+(defun cj/org-roam-capture-inbox ()
+ (interactive)
+ (org-roam-capture- :node (org-roam-node-create)
+ :templates '(("i" "inbox" plain "** %?"
+ :if-new (file+head+olp "~/sync/org/roam/inbox.org"
+ "#+TITLE: Inbox\n#+CATEGORY: Inbox\n#+FILETAGS: Project"
+ ("Inbox"))))))
+(global-set-key (kbd "C-t") #'cj/org-roam-capture-inbox)
+
+;; -------------------------------- Capture Task -------------------------------
+
+(defun cj/org-roam-capture-task ()
+ (interactive)
+ ;; Add the project file to the agenda after capture is finished
+ (add-hook 'org-capture-after-finalize-hook #'cj/org-roam-add-node-to-agenda-files-finalize-hook)
+
+ ;; Capture the new task, creating the project file if necessary
+ (org-roam-capture- :node (org-roam-node-read
+ nil
+ (cj/org-roam-filter-by-tag "Project"))
+ :templates '(("p" "project" plain "** TODO %?"
+ :if-new (file+head+olp "%<%Y%m%d%H%M%S>-${slug}.org"
+ "#+TITLE: ${title}\n#+CATEGORY: ${title}\n#+FILETAGS: Project"
+ ("${title}"))))))
+(global-set-key (kbd "C-c n t") #'cj/org-roam-capture-task)
+
+;; ------------------------ Org Roam Copy Done To Daily ------------------------
+
+(defun cj/org-roam-copy-todo-to-today ()
+ (interactive)
+ (let ((org-refile-keep t) ;; Set this to nil to delete the original!
+ (org-roam-dailies-capture-templates
+ '(("t" "tasks" entry "%?"
+ :if-new (file+head+olp "%<%Y-%m-%d>.org" "#+FILETAGS: Journal\n#+TITLE: %<%Y-%m-%d>\n" ("Completed Tasks")))))
+ (org-after-refile-insert-hook #'save-buffer)
+ today-file
+ pos)
+ (save-window-excursion
+ (org-roam-dailies--capture (current-time) t)
+ (setq today-file (buffer-file-name))
+ (setq pos (point)))
+
+ ;; Only refile if the target file is different than the current file
+ (unless (equal (file-truename today-file)
+ (file-truename (buffer-file-name)))
+ (org-refile nil nil (list "Completed Tasks" today-file nil pos)))))
+
+
+
+(provide 'org-roam-config)
+;;; org-roam-config.el ends here.
diff --git a/modules/pdf-config.el b/modules/pdf-config.el
new file mode 100644
index 00000000..518f3883
--- /dev/null
+++ b/modules/pdf-config.el
@@ -0,0 +1,51 @@
+;;; pdf-config --- PDF Viewer Setup -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;; --------------------------------- PDF Tools ---------------------------------
+
+(use-package pdf-tools
+ :defer 1
+ :mode (("\\.pdf\\'" . pdf-view-mode))
+ :init
+ (setq pdf-view-display-size 'fit-page)
+ (setq pdf-view-midnight-colors '( "#F1D5AC" . "#0F0E06")) ;; fg . bg
+ :config
+ (pdf-tools-install :no-query) ;; automatically compile on first launch
+ :bind
+ (:map pdf-view-mode-map
+ ("M" . pdf-view-midnight-minor-mode)
+ ("m" . cj/bookmark-set-and-save)
+ ("z" . (lambda () (interactive) (cj/open-file-with-command "zathura")))
+ ("e" . (lambda () (interactive) (cj/open-file-with-command "evince")))
+ ("j" . pdf-view-next-line-or-next-page)
+ ("k" . pdf-view-previous-line-or-previous-page)))
+
+(use-package pdf-view
+ :ensure nil ;; built-in
+ :after pdf-tools
+ :custom
+ (pdf-view-display-size 'fit-page)
+ (pdf-view-resize-factor 1.1)
+ ;; Avoid searching for unicodes to speed up pdf-tools.
+ ;; ... and yes, 'ligther' is not a typo
+ (pdf-view-use-unicode-ligther nil)
+ ;; Enable HiDPI support, at the cost of memory.
+ (pdf-view-use-scaling t))
+
+;; ------------------------------ PDF View Restore -----------------------------
+
+;; restores the last known position on opening a pdf file.
+(use-package pdf-view-restore
+ :after pdf-tools
+ :defer 1
+ :hook
+ (pdf-view-mode . pdf-view-restore-mode)
+ :config
+ (setq pdf-view-restore-filename (concat user-emacs-directory "/.pdf-view-restore")))
+
+(provide 'pdf-config)
+;;; pdf-config.el ends here
diff --git a/modules/prog-c.el b/modules/prog-c.el
new file mode 100644
index 00000000..d3e29d44
--- /dev/null
+++ b/modules/prog-c.el
@@ -0,0 +1,32 @@
+;;; prog-c --- C Programming Settings and Functionality -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;;;; ------------------------------ C-Mode Settings ------------------------------
+
+(defun cj/c-mode-settings ()
+ "Settings for \\='c-mode\\='."
+ (setq-default indent-tabs-mode nil) ;; spaces, not tabs
+ (setq-default c-basic-offset 4) ;; 4 spaces offset
+ (setq c-default-style "stroustrup") ;; k&r c, 2nd edition
+ (setq c-basic-indent 4) ;; indent 4 spaces
+ (setq compile-command "CFLAGS=\"-Wall -g \" make ") ;; default make command
+ (setq display-line-numbers-type t) ;; disable relative line numbers in C
+ (setq comment-auto-fill-only-comments t) ;; only auto-fill inside comments
+ (auto-fill-mode) ;; auto-fill multiline comments
+ (electric-pair-mode)) ;; automatic parenthesis pairing
+(add-hook 'c-mode-common-hook 'cj/c-mode-settings)
+
+;;;; -------------------------- Keybindings --------------------------
+
+(add-hook 'c-mode-common-hook (lambda ()
+ (local-set-key (kbd "S-<f2>") #'compile)
+ (local-set-key (kbd "S-<f3>") #'gdb)))
+
+
+
+(provide 'prog-c)
+;;; prog-c.el ends here
diff --git a/modules/prog-comments.el b/modules/prog-comments.el
new file mode 100644
index 00000000..61d5e328
--- /dev/null
+++ b/modules/prog-comments.el
@@ -0,0 +1,136 @@
+;;; prog-comments.el --- Comments and Underscores -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Simple utility functions for creating and managing comments.
+
+;;; Code:
+
+;; ------------------------------ Comment Reformat -----------------------------
+;; uncomments the selected text,joins into one paragraph,reapplies comments
+
+(defun cj/comment-reformat ()
+ "Reformats commented text into a single paragraph."
+ (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 beg end)
+ (comment-region beg end)
+ (setq fill-column orig-fill-column )))
+ ;; if no region
+ (message "No region was selected. Select the comment lines to reformat."))
+(global-set-key (kbd "C-z c r") 'cj/comment-reformat)
+
+;; ------------------------------ Comment Centered -----------------------------
+;; Horizontal comment char with centered text. Defaults to appropriate comments
+;; per major mode.
+
+(defun cj/comment-centered (&optional comment-char)
+ "Insert comment text centered around the 'COMMENT-CHAR' character.
+Will default to the '#' character if called with no arguments. Uses
+\\="fill-column"\\= or 80 (whichever is less) to calculate the comment length.
+Will begin and end the line with the appropriate comment symbols based on
+programming mode."
+ (interactive)
+ (if (not (char-or-string-p comment-char))
+ (setq comment-char "#"))
+ (let* ((comment (capitalize (string-trim (read-from-minibuffer "Comment: "))))
+ (fill-column (min fill-column 80))
+ (comment-length (length comment))
+ (comment-start-length (length comment-start))
+ (comment-end-length (length comment-end))
+ (current-column-pos (current-column))
+ (space-on-each-side (/ (- fill-column
+ current-column-pos
+ comment-length
+ (length comment-start)
+ (length comment-end)
+ ;; Single space on each side of comment
+ (if (> comment-length 0) 2 0)
+ ;; Single space after comment syntax sting
+ 1)
+ 2)))
+ (if (< space-on-each-side 2)
+ (message "Comment string is too big to fit in one line")
+ (progn
+ (insert comment-start)
+ (when (equal comment-start ";") ; emacs-lisp line comments are ;;
+ (insert comment-start)) ; so insert comment-char again
+ (insert " ")
+ (dotimes (_ space-on-each-side) (insert comment-char))
+ (when (> comment-length 0) (insert " "))
+ (insert comment)
+ (when (> comment-length 0) (insert " "))
+ (dotimes (_ (if (= (% comment-length 2) 0)
+ (- space-on-each-side 1)
+ space-on-each-side))
+ (insert comment-char))
+ (insert " ")
+ (insert comment-end)))))
+(global-set-key (kbd "C-z c l") 'cj/comment-line)
+
+;; ------------------------------- Comment Hyphen ------------------------------
+;; Horizontal dashes with centered text, typically used to indicating sections
+
+(defun cj/comment-hyphen()
+ "Insert a centered comment with '-' (hyphens) on each side."
+ (interactive)
+ (cj/comment-centered "-"))
+(global-set-key (kbd "C-z c -") 'cj/comment-hyphen)
+
+;; -------------------------------- Comment Box --------------------------------
+;; Traditional comment boxes
+
+(defun cj/comment-box ()
+ "Insert a comment with '#' drawn around a string the user inputs.
+The box extends to the fill column. Places the point on the line after the
+comment box."
+ (interactive)
+ (let* ((comment-char "#")
+ (comment-pad 4) ; 4 = 2 comment chars & 2 spaces
+ (comment (capitalize (string-trim (read-from-minibuffer "Comment: "))))
+ (comment-length (length comment)))
+
+ ;; message if the comment doesn't fit on a single line
+ (if (> comment-length (- fill-column comment-pad))
+ (message "Comment string is too big to fit in one line")
+ (progn
+ (dotimes (_ (- fill-column 1)) (insert comment-char))
+ (newline)
+ (insert comment-char)
+ (insert " ")
+ (insert comment)
+ (dotimes(_ (- fill-column comment-length comment-pad)) (insert " ")))
+ (insert comment-char)
+ (newline)
+ (dotimes (_ (- fill-column 1)) (insert comment-char)))))
+
+;; ------------------------------ Underscore Line ------------------------------
+;; Underlines the current line with the character of your choosing
+
+(defun cj/underscore-line (char)
+ "Insert the number of 'CHAR' underneath the current line to mimic an underscore."
+ (interactive "cEnter the character for underlining: ")
+ (save-excursion
+ (let ((length (- (point-at-eol) (point-at-bol))))
+ (end-of-line)
+ (insert "\n")
+ (insert (make-string length char)))))
+
+;; --------------------------- Remove Buffer Comments --------------------------
+;; another nice suggestion from malabarba.
+;; https://emacs.stackexchange.com/questions/5441/function-to-delete-all-comments-from-a-buffer-without-moving-them-to-kill-ring
+
+(defun cj/remove-buffer-comments ()
+ (interactive)
+ (goto-char (point-min))
+ (let (kill-ring)
+ (comment-kill (count-lines (point-min) (point-max)))))
+
+
+(provide 'prog-comments)
+;;; prog-comments.el ends here.
diff --git a/modules/prog-general.el b/modules/prog-general.el
new file mode 100644
index 00000000..b374daf3
--- /dev/null
+++ b/modules/prog-general.el
@@ -0,0 +1,203 @@
+;;; prog-general --- General Programming Functionality and Settings -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+;; This module provides general programming functionality not related to a
+;; specific programming language, such as code-folding, project management,
+;; highlighting symbols, snippets, and whitespace management.
+
+;;; Code:
+
+;; ---------------------- General Prog Settings ----------------------
+;; keybindings, minor-modes, and prog-mode settings
+
+(defun cj/general-prog-settings ()
+ "Keybindings, minor modes, and settings for programming mode."
+ (interactive)
+ (display-line-numbers-mode) ;; show line numbers
+ (setq display-line-numbers-type 'relative) ;; display numbers relative to 'the point'
+ (setq-default display-line-numbers-width 3) ;; 3 characters reserved for line numbers
+ (turn-on-visual-line-mode) ;; word-wrapping
+ (auto-fill-mode) ;; auto wrap at the fill column set
+ (local-set-key (kbd "M-;") 'comment-dwim)) ;; comment/uncomment region as appropriate
+
+(add-hook 'prog-mode-hook #'cj/general-prog-settings)
+(add-hook 'html-mode-hook #'cj/general-prog-settings)
+(add-hook 'yaml-mode-hook #'cj/general-prog-settings)
+(add-hook 'toml-mode-hook #'cj/general-prog-settings)
+
+;; --------------------------- Header Folding --------------------------
+
+;; dependencies for bicycle
+(use-package outline-minor-mode
+ :ensure nil ;; built-in part of org mode
+ :hook prog-mode)
+
+(use-package hs-minor-mode
+ :ensure nil ;; built-in
+ :hook prog-mode)
+
+;; BICYCLE
+;; cycle visibility of outline sections and code blocks.
+;; additionally it can make use of the hideshow package.
+(use-package bicycle
+ :after outline
+ :defer 1
+ :bind (:map outline-minor-mode-map
+ ("C-<tab>" . bicycle-cycle)
+ ;; backtab is shift-tab
+ ("<backtab>" . bicycle-cycle-global)))
+
+;; --------------------------- Project Switch Actions --------------------------
+;; if there's a todo or readme file in the project, display it when switching
+
+(defun cj/project-switch-actions ()
+ "Opens TODO or README file on projectile switch project.
+If none exists, it opens magit-status."
+ ;; (dired-sidebar-hide-sidebar)
+ ;; (dired-sidebar-show-sidebar)
+ (let ((todo-file
+ (cond
+ ((file-exists-p (concat (projectile-project-root) "TODO.org")) "TODO.org")
+ ((file-exists-p (concat (projectile-project-root) "todo.org")) "todo.org")
+ ((file-exists-p (concat (projectile-project-root) "TODO.md")) "TODO.md")
+ ((file-exists-p (concat (projectile-project-root) "TODO.txt")) "TODO.txt"))))
+ (if todo-file
+ (find-file (concat (projectile-project-root) todo-file))
+ (magit-status))))
+
+;; --------------------------------- Projectile --------------------------------
+;; project support
+
+;; notify user when scanning for projects
+(defun cj/projectile-identify-projects ()
+ (message "No project cache found. Identifying projects....")
+ (projectile-discover-projects-in-search-path))
+
+;; only run discover projects when there's no bookmarks file
+(defun cj/projectile-schedule-project-discovery ()
+ (let ((bookmark-file (concat user-emacs-directory "/projectile-bookmarks.eld")))
+ (unless (file-exists-p bookmark-file)
+ (run-at-time "3" nil 'cj/projectile-identify-projects))))
+
+(use-package projectile
+ :defer .5
+ :bind-keymap
+ ("C-c p" . projectile-command-map)
+ :bind
+ (:map projectile-command-map
+ ("r" . projectile-replace-regexp))
+ :custom
+ (projectile-auto-discover nil)
+ (projectile-project-search-path '("~/code"
+ "~/projects"))
+ :config
+ (cj/projectile-schedule-project-discovery)
+ ;; don't reuse comp buffers between projects
+ (setq projectile-per-project-compilation-buffer t)
+ (projectile-mode)
+ (setq projectile-switch-project-action #'cj/project-switch-actions))
+
+;; groups ibuffer by projects
+(use-package ibuffer-projectile
+ :defer .5
+ :after projectile
+ :hook (ibuffer-mode . ibuffer-projectile-set-filter-groups))
+
+;; list all errors project-wide
+(use-package flycheck-projectile
+ :defer .5
+ :after projectile
+ :bind
+ (:map projectile-command-map
+ ("X" . flycheck-projectile-list-errors)))
+
+;; ---------------------------------- Ripgrep ----------------------------------
+;; allows fast searching for text anywhere in the project with C-c p G (grep)
+
+(use-package ripgrep
+ :defer .5
+ :after projectile
+ :bind
+ (:map projectile-command-map
+ ("G" . projectile-ripgrep)))
+
+;; ---------------------------------- Snippets ---------------------------------
+;; reusable code and text.
+
+(use-package yasnippet
+ :defer 1
+ :bind
+ ("C-c s n" . yas-new-snippet)
+ ("C-c s e" . yas-visit-snippet-file)
+ :config
+ (setq yas-snippet-dirs '(snippets-dir))
+ (yas-global-mode 1))
+
+(use-package ivy-yasnippet
+ :after yasnippet
+ :bind
+ ("C-c s i" . ivy-yasnippet))
+
+;; --------------------- Display Color On Color Declaration --------------------
+;; display the actual color as highlight to color hex code
+
+(use-package rainbow-mode
+ :defer .5
+ :hook (prog-mode . rainbow-mode))
+
+;; ---------------------------- Symbol Overlay Mode ----------------------------
+;; Highlight symbols with keymap-enabled overlays
+;; replaces highlight-symbol-mode
+
+(use-package symbol-overlay
+ :defer .5
+ :bind-keymap
+ ("C-c C-s" . symbol-overlay-map)
+ :hook
+ (prog-mode . symbol-overlay-mode))
+
+;; ------------------------------ Highlight TODOs ------------------------------
+;; Highlights todo keywords in code for easy spotting
+(use-package hl-todo
+ :defer 1
+ :hook
+ (prog-mode . hl-todo-mode)
+ :config
+ (setq hl-todo-keyword-faces
+ '(("FIXME" . "#FF0000")
+ ("BUG" . "#FF0000")
+ ("HACK" . "#FF0000")
+ ("ISSUE" . "#DAA520")
+ ("TASK" . "#DAA520")
+ ("NOTE" . "#2C780E")
+ ("WIP" . "#1E90FF"))))
+
+;; --------------------------- Whitespace Management ---------------------------
+;; when saving your file, trims unneeded whitespace only from lines you've modified
+
+(use-package ws-butler
+ :defer .5
+ :commands (ws-butler-mode)
+ :init
+ (add-hook 'prog-mode-hook #'ws-butler-mode)
+ (add-hook 'org-mode-hook #'ws-butler-mode)
+ (add-hook 'text-mode-hook #'ws-butler-mode)
+ :config
+ (setq ws-butler-convert-leading-tabs-or-spaces t))
+
+;; ----------------- Auto-Close Successful Compilation Windows -----------------
+;; from 'enberg' on #emacs
+
+(add-hook 'compilation-finish-functions
+ (lambda (buf str)
+ (if (null (string-match ".*exited abnormally.*" str))
+ ;;no errors, make the compilation window go away in a few seconds
+ (progn
+ (run-at-time
+ "1.5 sec" nil 'delete-windows-on
+ (get-buffer-create "*compilation*"))))))
+
+
+(provide 'prog-general)
+;;; prog-general.el ends here
diff --git a/modules/prog-go.el b/modules/prog-go.el
new file mode 100644
index 00000000..617fceaa
--- /dev/null
+++ b/modules/prog-go.el
@@ -0,0 +1,85 @@
+;;; prog-go --- Golang Specific Settings and Functionality -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+
+(defun cj/go-setup ()
+ "My default code preferences for Golang."
+ (require 'tree-sitter)
+ (require 'tree-sitter-langs)
+ (require 'tree-sitter-hl)
+ (tree-sitter-hl-mode)
+ (hs-minor-mode)
+ (company-mode)
+ (setq-default tab-width 4) ;; set the tab width to 4 spaces
+ (setq-default standard-indent 4) ;; indent 4 spaces
+ (setq-default indent-tabs-mode nil) ;; disable tab characters
+ (electric-pair-mode t)) ;; match delimiters automatically
+(add-hook 'go-mode-hook 'cj/go-setup)
+
+;;;; ---------------------------- Go Mode ----------------------------
+
+(use-package go-mode
+ :bind (:map go-mode-map
+ ("<f6>" . gofmt)
+ ("C-c 6" . gofmt)
+ ("<f4>" . golint)
+ ("C-c 4" . golint))
+ :config
+ (add-to-list 'exec-path "~/go/bin")
+ ;; allow adding/removing fmt lines; install with:
+ ;; go install golang.org/x/tools/cmd/goimports@latest
+ (setq gofmt-command "goimports"))
+
+;; (use-package go-mode
+;; :config
+;; (general-define-key
+;; :keymaps 'go-mode-map
+;; :states '(normal)
+;; "K" #'godoc-at-point
+;; "C-]" #'godef-jump)
+
+;; (general-define-key
+;; :keymaps 'go-mode-map
+;; :states '(normal)
+;; :prefix mpereira/leader
+;; "tt" #'go-test-current-test
+;; "tT" #'go-test-current-file
+;; "pt" #'go-test-current-project))
+
+;; ------------- Configure Emacs To Find Go Project Root -------------
+
+;; Note: This appears to interfere with tramp. Before re-enabling, this
+;; should have a toggle and turned off when working in tramp.
+
+;; (require 'project)
+
+;; (defun project-find-go-module (dir)
+;; (when-let ((root (locate-dominating-file dir "go.mod")))
+;; (cons 'go-module root)))
+
+;; (cl-defmethod project-root ((project (head go-module)))
+;; (cdr project))
+
+;; (add-hook 'project-find-functions #'project-find-go-module)
+
+;; -------------------- Enable Eglot Integrations --------------------
+
+;; The depth of -10 places this before eglot's willSave notification,
+;; so that that notification reports the actual contents that will be saved.
+;; (defun eglot-format-buffer-on-save ()
+;; (add-hook 'before-save-hook #'eglot-format-buffer -10 t))
+;; (add-hook 'go-mode-hook #'eglot-format-buffer-on-save)
+
+;; -------------------- Configure Gopls Via Eglot --------------------
+
+;; (setq-default eglot-workspace-configuration
+;; '((:gopls .
+;; ((staticcheck . t)
+;; (matcher . "CaseSensitive")))))
+
+(provide 'prog-go)
+;;; prog-go.el ends here
diff --git a/modules/prog-lisp.el b/modules/prog-lisp.el
new file mode 100644
index 00000000..618108bd
--- /dev/null
+++ b/modules/prog-lisp.el
@@ -0,0 +1,128 @@
+;;; prog-lisp --- Lisp Specific Settings and Functionality -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;; ==== Setting up Quicklisp ====
+;; Quicklisp is a library manager for Common Lisp. It works with your existing Common Lisp
+;; implementation to download, install, and load any of over 1,500 libraries with a few
+;; simple commands.
+;; https://www.quicklisp.org/beta/
+
+;; mostly from: https://gist.github.com/jteneycke/7947353
+;; * Install SBCL
+;; sudo pacman -S sbcl # arch
+;; doas pkg install sbcl # bsd
+;; sudo apt-get install sbcl # debian
+
+;; * Install QuickLisp
+;; curl -O http://beta.quicklisp.org/quicklisp.lisp
+;; sbcl --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --quit
+;; sbcl --load ~/quicklisp/setup.lisp --eval "(ql:add-to-init-file)" --quit
+
+;; * Emacs Config
+;; (load (expand-file-name "~/quicklisp/slime-helper.el"))
+;; ;; Replace "sbcl" with the path to your implementation
+;; (setq inferior-lisp-program "/usr/bin/sbcl")
+
+;; to get readline support in SBCL's REPL, install rlwrap and run it
+;; before sbcl like so:
+
+;; $ rlwrap sbcl
+
+;;; Code:
+
+;; -------------------------------- Elisp Setup --------------------------------
+;; run this on editing an elisp file
+
+(defun cj/elisp-setup ()
+ "My default code preferences for emacs-lisp."
+ (setq-default tab-width 4) ;; set the tab width to 4 spaces
+ (setq-default indent-tabs-mode -1) ;; disable tab characters
+ (setq-default fill-column 80))
+(add-hook 'emacs-lisp-mode-hook 'cj/elisp-setup)
+
+;; ------------------------------ Emacs Lisp REPL ------------------------------
+
+(use-package ielm
+ :ensure nil ;; built-in
+ :hook (ielm-mode . eldoc-mode)
+ :config (setq ielm-prompt "elisp> "))
+
+;; ----------------------------------- Eldoc -----------------------------------
+
+(use-package eldoc
+ :ensure nil ;; built-in
+ :hook ((c-mode-common emacs-lisp-mode) . eldoc-mode)
+ :custom
+ (eldoc-echo-area-use-multiline-p 3)
+ (eldoc-echo-area-display-truncation-message nil))
+
+;; -------------------------- ERT + Testing Libraries --------------------------
+;; unit/regression testing framework
+;; basic introduction: https://nullprogram.com/blog/2012/08/15/
+;; https://www.gnu.org/software/emacs/manual/html_node/ert/
+;; or: [[info:ert#User Input]]
+
+(use-package ert
+ :ensure nil ;; built-into emacs
+ :defer 1)
+
+;; mocking/stub framework
+;; note: Find the documentation at M-x describe-function RET with-mock RET
+;; and M-x describe-function RET mocklet RET.testing
+(with-eval-after-load 'ert
+ (defun ert-all-tests ()
+ "Run all ert tests and display the results in a buffer."
+ (interactive)
+ (ert t))
+
+ (keymap-global-unset "C-r" t)
+ (define-key emacs-lisp-mode-map (kbd "C-r") 'ert-all-tests)
+ (define-key lisp-interaction-mode-map (kbd "C-r") 'ert-all-tests))
+
+(use-package el-mock) ;; mock/stub framework
+
+
+(defun cj/eval-and-run-all-tests-in-buffer ()
+ "Delete loaded tests, evaluate current buffer. and run all loaded ERT tests."
+ (interactive)
+ (ert-delete-all-tests)
+ (eval-buffer)
+ (ert 't))
+
+;; ------------------------------ Package Tooling ------------------------------
+
+(use-package package-lint
+ :defer 1)
+
+(use-package flycheck-package
+ :defer 1
+ :after (flycheck package-lint)
+ :config
+ (flycheck-package-setup))
+
+(use-package package-build
+ :defer 1)
+
+;; ----------------------------- Rainbow Delimiters ----------------------------
+
+(use-package rainbow-delimiters
+ :defer .5
+ :hook
+ (emacs-lisp-mode . rainbow-delimiters-mode)
+ (lisp-mode . rainbow-delimiters-mode)
+ :config
+ (set-face-foreground 'rainbow-delimiters-depth-1-face "#c66") ;; red
+ (set-face-foreground 'rainbow-delimiters-depth-2-face "#6c6") ;; green
+ (set-face-foreground 'rainbow-delimiters-depth-3-face "#69f") ;; blue
+ (set-face-foreground 'rainbow-delimiters-depth-4-face "#cc6") ;; yellow
+ (set-face-foreground 'rainbow-delimiters-depth-5-face "#6cc") ;; cyan
+ (set-face-foreground 'rainbow-delimiters-depth-6-face "#c6c") ;; magenta
+ (set-face-foreground 'rainbow-delimiters-depth-7-face "#ccc") ;; light gray
+ (set-face-foreground 'rainbow-delimiters-depth-8-face "#999") ;; medium gray
+ (set-face-foreground 'rainbow-delimiters-depth-9-face "#666")) ;; dark gray
+
+
+(provide 'prog-lisp)
+;;; prog-lisp.el ends here
diff --git a/modules/prog-lsp.el b/modules/prog-lsp.el
new file mode 100644
index 00000000..e5dc4852
--- /dev/null
+++ b/modules/prog-lsp.el
@@ -0,0 +1,55 @@
+;;; prog-lsp --- Setup for LSP Mode -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;; good reference as to what to enable/disable in lsp-mode
+;; https://emacs-lsp.github.io/lsp-mode/tutorials/how-to-turn-off/
+
+;;; Code:
+
+
+;;;;; ---------------------------- LSP Mode ---------------------------
+
+(use-package lsp-mode
+ :hook
+ ((c-mode c++-mode go-mode js-mode js-jsx-mode typescript-mode python-mode web-mode) . lsp-deferred)
+ :commands (lsp)
+ :bind (:map lsp-mode-map
+ ("C-c d" . lsp-describe-thing-at-point)
+ ("C-c a" . lsp-execute-code-action))
+ :bind-keymap ("C-c L" . lsp-command-map)
+ :config
+ (setq lsp-auto-guess-root t)
+ (setq lsp-log-io nil)
+ (setq lsp-restart 'auto-restart)
+ (setq lsp-enable-symbol-highlighting nil)
+ (setq lsp-enable-on-type-formatting nil)
+ (setq lsp-signature-auto-activate nil)
+ (setq lsp-signature-render-documentation nil)
+ (setq lsp-eldoc-hook nil)
+ (setq lsp-modeline-code-actions-enable nil)
+ (setq lsp-modeline-diagnostics-enable nil)
+ (setq lsp-headerline-breadcrumb-enable nil)
+ (setq lsp-semantic-tokens-enable nil)
+ (setq lsp-enable-folding nil)
+ (setq lsp-enable-imenu nil)
+ (setq lsp-enable-snippet nil)
+ (setq read-process-output-max (* 1024 1024)) ;; 1MB
+ (setq lsp-idle-delay 0.5))
+
+;;;;; ----------------------------- LSP UI ----------------------------
+
+(use-package lsp-ui
+ :after lsp-mode
+ :commands lsp-ui-mode
+ :config
+ (setq lsp-ui-doc-enable nil)
+ (setq lsp-ui-doc-header t)
+ (setq lsp-ui-doc-include-signature t)
+ (setq lsp-ui-doc-border (face-foreground 'default))
+ (setq lsp-ui-sideline-show-code-actions nil) ;; turn off code actions in sidebar
+ (setq lsp-ui-sideline-delay 0.05))
+
+(provide 'prog-lsp)
+;;; prog-lsp.el ends here
diff --git a/modules/prog-python.el b/modules/prog-python.el
new file mode 100644
index 00000000..5dc06bf6
--- /dev/null
+++ b/modules/prog-python.el
@@ -0,0 +1,74 @@
+;;; prog-python --- Python Specific Setup and Functionality -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;; ------------------------- General Settings ------------------------
+
+(add-hook 'python-mode-hook (lambda () (setq indent-tabs-mode nil))) ;; use spaces, not tabs
+
+;; ----------------------------------- Python ----------------------------------
+;; remove the guess indent python message
+
+(use-package python
+ :config
+ (setq python-indent-guess-indent-offset-verbose nil))
+
+;; --------------------------- Python Mode ---------------------------
+
+(use-package python-mode
+ :ensure nil ;; built-in
+ :hook
+ ((python-mode . flyspell-prog-mode)
+ (python-mode . superword-mode)
+ (python-mode . company-mode)
+ (python-mode . electric-pair-mode)) ;; auto-complete braces and pairs
+ :custom
+ (python-shell-interpreter "python3")
+ (setq python-indent-offset 4)) ;; 4 spaces default indent
+
+;; ----------------------------------- Poetry ----------------------------------
+;; virtual environments and dependencies
+
+;; (use-package poetry
+;; :defer t
+;; :config
+;; ;; Checks for the correct virtualenv. Better strategy IMO because the default
+;; ;; one is quite slow.
+;; (setq poetry-tracking-strategy 'switch-buffer)
+;; :hook (python-mode . poetry-tracking-mode))
+
+;; ---------------------------------- Blacken ----------------------------------
+;; formatting on save
+
+(use-package blacken
+ :defer t
+ :custom
+ (blacken-allow-py36 t)
+ (blacken-skip-string-normalization t)
+ :hook (python-mode . blacken-mode))
+
+;; ---------------------------------- Numpydoc ---------------------------------
+;; automatically insert NumPy style docstrings in Python function definitions
+
+(use-package numpydoc
+ :defer t
+ :custom
+ (numpydoc-insert-examples-block nil)
+ (numpydoc-template-long nil)
+ :bind (:map python-mode-map
+ ("C-c C-n" . numpydoc-generate)))
+
+;; ------------------------------------ Toml -----------------------------------
+
+(use-package toml-mode
+ :defer .5)
+
+(use-package eldoc-toml
+ :defer .5)
+
+
+(provide 'prog-python)
+;;; prog-python.el ends here
diff --git a/modules/prog-shell.el b/modules/prog-shell.el
new file mode 100644
index 00000000..6f35b73d
--- /dev/null
+++ b/modules/prog-shell.el
@@ -0,0 +1,14 @@
+;;; prog-shell --- Shell Programming Settings and Functionality -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(use-package sh-script
+ :defer .5
+ :hook (sh-mode . flycheck-mode))
+
+(provide 'prog-shell)
+;;; prog-shell.el ends here
diff --git a/modules/prog-training.el b/modules/prog-training.el
new file mode 100644
index 00000000..801b1982
--- /dev/null
+++ b/modules/prog-training.el
@@ -0,0 +1,30 @@
+;;; prog-training.el --- Training -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+
+;; ----------------------------- Exercism ----------------------------
+
+(use-package exercism
+ :commands (exercism)
+ :bind
+ ("C-h E" . exercism))
+
+
+;;; ----------------------------- Leetcode ----------------------------
+
+(use-package leetcode
+ :commands (leetcode)
+ :custom
+ (url-debug t)
+ :config
+ (setq leetcode-prefer-language "golang")
+ (setq leetcode-directory "~/code/leetcode")
+ (setq leetcode-save-solutions t))
+
+
+(provide 'prog-training)
+;;; prog-training.el ends here.
diff --git a/modules/prog-webdev.el b/modules/prog-webdev.el
new file mode 100644
index 00000000..2d5fc5da
--- /dev/null
+++ b/modules/prog-webdev.el
@@ -0,0 +1,79 @@
+;;; prog-webdev.el --- Web Development Packages and Settings -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+;; --------------------------------- JSON Mode ---------------------------------
+;; mode for editing JavaScript Object Notation (JSON) data files
+
+(use-package json-mode
+ :mode ("\\.json\\'" . json-mode)
+ :defer .5)
+
+;; ---------------------------------- JS2 Mode ---------------------------------
+;; javascript editing mode
+
+(use-package js2-mode
+ :mode ("\\.js\\'" . js2-mode)
+ :defer .5)
+
+;; --------------------------------- CSS Eldoc ---------------------------------
+;; CSS info in the echo area
+
+(use-package css-eldoc
+ :defer .5)
+
+;; ------------------------------------ Tide -----------------------------------
+;; typescript interactive development environment
+
+(use-package tide
+ :defer .5)
+
+(defun cj/activate-tide ()
+ (interactive)
+ (tide-setup)
+ (eldoc-mode 1)
+ (tide-hl-identifier-mode 1))
+
+;; ---------------------------------- Web Mode ---------------------------------
+;; major mode for editing web templates
+
+(use-package web-mode
+ :defer .5
+ :after (tide css-eldoc)
+ :custom
+ (web-mode-enable-current-element-highlight t)
+ :bind
+ ([(control return)] . cj/complete-web-mode)
+ :mode
+ (("\\.html?$" . cj/setup-web-mode-mixed)))
+
+(defun cj/complete-web-mode ()
+ (interactive)
+ (let ((current-scope (web-mode-language-at-pos (point))))
+ (cond ((string-equal "javascript" current-scope)
+ (company-tide 'interactive))
+ ((string-equal "css" current-scope)
+ (company-css 'interactive))
+ (t
+ (company-dabbrev-code 'interactive)))))
+
+(defun cj/eldoc-web-mode ()
+ (let ((current-scope (web-mode-language-at-pos (point))))
+ (cond ((string-equal "javascript" current-scope)
+ (tide-eldoc-function))
+ ((string-equal "css" current-scope)
+ (css-eldoc-function))
+ (t
+ nil))))
+
+(defun cj/setup-web-mode-mixed ()
+ (web-mode)
+ (cj/activate-tide)
+ (setq-local eldoc-documentation-function #'cj/eldoc-web-mode))
+
+
+(provide 'prog-webdev)
+;;; prog-webdev.el ends here.
diff --git a/modules/prog-yaml.el b/modules/prog-yaml.el
new file mode 100644
index 00000000..48251155
--- /dev/null
+++ b/modules/prog-yaml.el
@@ -0,0 +1,18 @@
+;;; prog-yaml --- YAML Settings -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+(use-package yaml-mode
+ :defer .5
+ :commands (yaml-mode)
+ :config
+ (add-to-list 'auto-mode-alist '("\\.yml\\'" . yaml-mode))
+ (add-to-list 'auto-mode-alist '("\\.yaml\\'" . yaml-mode)))
+
+(add-hook 'yaml-mode-hook ' flycheck-mode-hook)
+
+(provide 'prog-yaml)
+;;; prog-yaml.el ends here
diff --git a/modules/reconcile-open-repos.el b/modules/reconcile-open-repos.el
new file mode 100644
index 00000000..ed7c08a6
--- /dev/null
+++ b/modules/reconcile-open-repos.el
@@ -0,0 +1,72 @@
+;;; reconcile-open-repos.el --- reconcile open repos -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; I have tried to keep this Emacs config as general as possible, and move all
+;; config related to my personal workflows here.
+
+;; I typically work with multiple git repositories the day, and can forget to
+;; commit uncommitted work. Also, I often forget to start a session by pulling
+;; changes. So, making it a habit to run cj/check-for-open-work when starting
+;; and ending the work session allows me to know that I've iterated through all
+;; git repositories in my projects and code directories and have reconciled all
+;; changes.
+
+;;; Code:
+
+;; -------------------------- Reconcile Git Directory --------------------------
+
+(defun cj/reconcile-git-directory (directory)
+ "Reconcile unopened work in a git project directory leveraging magit."
+ (message "checking: %s" directory)
+ (let ((default-directory directory))
+ ;; Check for the presence of the .git directory
+ (if (file-directory-p (expand-file-name ".git" directory))
+ (progn
+ (let ((remote-url (shell-command-to-string "git config --get remote.origin.url")))
+ (setq remote-url (string-trim remote-url))
+
+ ;; skip local git repos, or remote URLs that are http or https,
+ ;; these are typically cloned for reference only
+ (unless (or (string-empty-p remote-url)
+ (string-match-p "^\\(http\\|https\\)://" remote-url))
+
+ ;; if git directory is clean, pulling generates no errors
+ (if (string-empty-p (shell-command-to-string "git status --porcelain"))
+ (progn
+ ;; (message "%s is a clean git repository" directory)
+ (shell-command "git pull --quiet"))
+
+ ;; if directory not clean, pull latest changes and display Magit for manual intervention
+ (progn
+ (message "%s contains uncommitted work" directory)
+ (shell-command "git stash --quiet")
+ (shell-command "git pull --quiet")
+ (shell-command "git stash pop --quiet")
+ (call-interactively #'magit-status)
+ ;; pause until magit buffer is closed
+ (while (buffer-live-p (get-buffer (format "*magit: %s*" (file-name-nondirectory directory))))
+ (sit-for 0.5))))))))))
+
+;; ---------------------------- Check For Open Work ----------------------------
+
+(defun cj/check-for-open-work ()
+ "Check all project directories for open work."
+ (interactive)
+ ;; these are constants defined in init.el
+ ;; children of these directories will be checked
+ (dolist (base-dir (list projects-dir code-dir))
+ (when (file-directory-p base-dir)
+ (dolist (child-dir (directory-files base-dir t "^[^.]+$" 'nosort))
+ (when (file-directory-p child-dir)
+ (cj/reconcile-git-directory child-dir)))))
+
+ ;; check these directories individually
+ (cj/reconcile-git-directory sync-dir)
+ (cj/reconcile-git-directory user-emacs-directory)
+
+ ;; communicate when finished.
+ (message "Complete. All project repositories checked for uncommitted work and code updated from remote repository"))
+(global-set-key (kbd "M-P") 'cj/check-for-open-work)
+
+(provide 'reconcile-open-repos)
+;;; reconcile-open-repos.el ends here.
diff --git a/modules/selection-framework.el b/modules/selection-framework.el
new file mode 100644
index 00000000..6428a977
--- /dev/null
+++ b/modules/selection-framework.el
@@ -0,0 +1,155 @@
+;;; selection-framework --- Search/Narrowing and Related Functionality -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+
+;; ---------------------------------- Company ----------------------------------
+;; Company is a modular in-buffer tool-tip-style completion front-end framework.
+
+(use-package company
+ :defer .5
+ :hook
+ ((text-mode . company-mode)
+ (prog-mode . company-mode)
+ (lisp-interaction-mode . company-mode))
+ :custom
+ ;; search other buffers =with the same mode= for completion
+ (company-dabbrev-other-buffers t)
+ (company-dabbrev-code-other-buffers t)
+ ;; M-<num> to select an option according to its number.
+ (company-show-numbers t)
+ ;; 2 letters required for completion to activate.
+ (company-minimum-prefix-length 3)
+ ;; don't downcase completions by default.
+ (company-dabbrev-downcase nil)
+ ;; provide proper casing even if I don't.
+ (company-dabbrev-ignore-case t)
+ ;; company completion wait
+ (company-idle-delay 0.2)
+ ;; use vscode icons in the margin
+ (setq company-format-margin-function #'company-vscode-light-icons-margin)
+ ;; no company-mode in shell & eshell
+ (company-global-modes '(not eshell-mode shell-mode)))
+
+(with-eval-after-load 'company
+ (define-key company-active-map
+ (kbd "TAB")
+ #'company-complete-common-or-cycle)
+ (define-key company-active-map
+ (kbd "<backtab>")
+ (lambda ()
+ (interactive)
+ (company-complete-common-or-cycle -1))))
+
+;; ---------------------------------- Counsel ----------------------------------
+;; part of the counsel/ivy/swiper trio. ivy-enhanced versions of Emacs commands.
+
+(use-package counsel
+ :defer .5
+ :bind
+ ("C-c U" . counsel-unicode-char))
+
+;; ------------------------------------ Ivy ------------------------------------
+;; A generic completion mechanism for Emacs. https://github.com/abo-abo/swiper#ivy
+
+(use-package ivy
+ :defer .5
+ :bind (("C-c u" . ivy-resume))
+ (:map ivy-occur-grep-mode-map
+ ("n" . ivy-occur-next-line)
+ ("p" . ivy-occur-previous-line)
+ ("b" . backward-char)
+ ("f" . forward-char)
+ ("v" . ivy-occur-press)
+ ("RET" . ivy-occur-press))
+ :config
+ (setq ivy-action-wrap t) ;; wrap next and previous actions
+ (setq ivy-count-format "%d/%d ") ;; show index as well as count
+ (setq ivy-extra-directories nil) ;; don't show ./ and ../ in lists
+ (setq ivy-height 13) ;; 13 lines high
+ (setq ivy--regex-ignore-order t) ;; ignore word order
+ (setq ivy-use-selectable-prompt t) ;; prompt becomes selectable
+ (setq ivy-use-virtual-buffers t) ;; ivy-switch-buffer shows recently killed buffers
+ (setq ivy-virtual-abbreviate 'full) ;; show the full virtual file path
+ (setq ivy-wrap t) ;; wrap list when finished to start and vice-versa
+ (ivy-mode)
+
+ ;; modify default search behaviour of ivy
+ (setq ivy-re-builders-alist
+ '((t . ivy--regex-plus))))
+
+;; ALL THE ICONS IVY
+;; Shows icons while using Ivy and Counsel
+;; https://github.com/asok/all-the-icons-ivy
+(use-package all-the-icons-ivy
+ :defer .5
+ :after ivy
+ :init (add-hook 'after-init-hook 'all-the-icons-ivy-setup))
+
+;; IVY-RICH
+;; comes with rich transformers for commands from ivy and counsel.
+;; https://github.com/Yevgnen/ivy-rich
+(use-package ivy-rich
+ :after ivy
+ :defer .5
+ :hook (counsel-mode . ivy-rich-mode)
+ :config
+ ;; For better performance
+ ;; Better experience with icons
+ (setq ivy-rich-parse-remote-buffer nil))
+
+
+;; ALL THE ICONS IVY RICH
+;; extracted from Centaur Emacs and leverages ivy-rich and all-the-icons.
+;; https://github.com/seagle0128/all-the-icons-ivy-rich
+(use-package all-the-icons-ivy-rich
+ :defer .5
+ :after (all-the-icons ivy-rich)
+ :config
+ (all-the-icons-ivy-rich-mode 1)
+ (setq all-the-icons-ivy-rich-icon-size 0.8))
+
+;; ----------------------------------- Swiper ----------------------------------
+;; Swiper displays an overview of all matches, leveraging Ivy.
+
+(use-package swiper
+ :defer .5
+ :bind
+ (("C-s" . swiper)
+ ("M-s" . swiper-isearch-thing-at-point))
+ :config
+ (setq swiper-action-recenter t) ;; recenter after selection
+ (setq swiper-goto-start-of-match t)) ;; jump to the beginning of match after selection
+
+;; --------------------------------- Marginalia --------------------------------
+;; Enables richer annotations in the selection framework
+
+(use-package marginalia
+ :defer .5
+ ;; Either bind `marginalia-cycle` globally or only in the minibuffer
+ :bind (("M-A" . marginalia-cycle)
+ :map minibuffer-local-map
+ ("M-A" . marginalia-cycle))
+ :config
+ (marginalia-mode))
+
+;; --------------------------------- Prescient ---------------------------------
+;; Sorts and filters candidates that appear when you use a package like Ivy or Company.
+
+(use-package prescient
+ :defer .5
+ :config
+ (setq prescient-sort-full-matches-first t))
+
+;; IVY PRESCIENT: Prescient integration with Ivy
+(use-package ivy-prescient
+ :defer .5
+ :after (ivy prescient)
+ :config
+ (ivy-prescient-mode 1))
+
+(provide 'selection-framework)
+;;; selection-framework.el ends here
diff --git a/modules/show-kill-ring.el b/modules/show-kill-ring.el
new file mode 100644
index 00000000..fdc400ee
--- /dev/null
+++ b/modules/show-kill-ring.el
@@ -0,0 +1,106 @@
+;;; show-kill-ring --- Displays Previous Kill Ring Entries -*- lexical-binding: t; -*-;; Show Kill Ring
+;; Stolen from Steve Yegge when he wasn't looking
+
+
+;;; Commentary:
+;; Browse items you've previously killed.
+
+
+;;; 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 ()
+ "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)
+
+ ;; show it
+ (goto-char (point-min))
+ (set-buffer-modified-p nil)
+ (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)) ?=))
+ (insert (concat "\n\nItem " (int-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))
+
+(global-set-key (kbd "M-K") 'show-kill-ring)
+
+(provide 'show-kill-ring)
+;;; show-kill-ring.el ends here
+
diff --git a/modules/system-defaults.el b/modules/system-defaults.el
new file mode 100644
index 00000000..48ac3d50
--- /dev/null
+++ b/modules/system-defaults.el
@@ -0,0 +1,195 @@
+;;; system-defaults --- Emacs Non-UI Preferences -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;; ---------------------------------- Unicode ----------------------------------
+
+(set-locale-environment "en_US.UTF-8")
+(prefer-coding-system 'utf-8)
+(set-default-coding-systems 'utf-8)
+(set-terminal-coding-system 'utf-8)
+(set-keyboard-coding-system 'utf-8)
+(set-selection-coding-system 'utf-8)
+(setq locale-coding-system 'utf-8)
+(set-charset-priority 'unicode)
+(setq x-select-request-type
+ '(UTF8_STRING COMPOUND_TEXT TEXT STRING))
+
+;; -------------------------- Disabling Functionality --------------------------
+
+(defun cj/disabled ()
+ "Do nothing. Functionality disabled."
+ (interactive))
+
+;; VIEW EMACS NEWS
+;; no news is good news
+(defalias 'view-emacs-news 'cj/disabled)
+(global-unset-key (kbd "C-h n"))
+
+;; DESCRIBE GNU PROJECT
+;; no gnus is good gnus
+(defalias 'describe-gnu-project 'cj/disabled)
+(global-unset-key (kbd "C-h g"))
+
+;; CUSTOMIZATIONS
+;; All customizations should be declared in Emacs init files.
+;; any customizations go into a temp file that's never read.
+(setq custom-file (make-temp-file
+ "emacs-customizations-trashbin"))
+
+;; ------------------------- Re-Enabling Functionality -------------------------
+
+(put 'upcase-region 'disabled nil) ;; upcase region is useful
+(put 'erase-buffer 'disabled nil) ;; and so is erase-buffer
+
+;; ------------------------------ Non UI Settings ------------------------------
+
+(setq ring-bell-function 'ignore) ;; disable the bell ring.
+(setq default-directory user-home-dir) ;; consider user home the default directory
+
+(global-auto-revert-mode) ;; update the buffer when the associated file has changed
+(setq global-auto-revert-non-file-buffers t) ;; do so for all buffer types (e.g., ibuffer)
+(setq bidi-display-reordering nil) ;; don't reorder bidirectional text for display
+(setq bidi-paragraph-direction t) ;; forces directionality of text for performance.
+
+(setq system-time-locale "C") ;; use en_US locale to format time.
+
+;; --------------------------------- Clipboard ---------------------------------
+
+(setq select-enable-clipboard t) ;; cut and paste using clipboard
+(setq yank-pop-change-selection t) ;; update system clipboard when yanking in emacs
+(setq save-interprogram-paste-before-kill t) ;; saves existing clipboard to kill ring before replacing
+
+;; -------------------------------- Tab Settings -------------------------------
+;; use spaces, not tabs
+
+(setq-default tab-width 4) ;; if tab, make them 4 spaces default
+(setq-default indent-tabs-mode nil) ;; but turn off tabs by default
+
+;; ------------------------------ Scroll Settings ------------------------------
+
+(setq mouse-wheel-scroll-amount '(1 ((shift) . 1))) ;; one line at a time
+(setq mouse-wheel-progressive-speed nil) ;; don't accelerate scrolling
+
+;; ----------------------------- Case Insensitivity ----------------------------
+;; make user interfaces case insensitive
+
+(setq case-fold-search t) ;; case-insensitive searches
+(setq completion-ignore-case t) ;; case-insensitive completion
+(setq read-file-name-completion-ignore-case t) ;; case-insensitive file completion
+
+;; ------------------------------- Async Commands ------------------------------
+;; always create new async command buffers silently
+
+(setq async-shell-command-buffer 'new-buffer)
+
+;; never automatically display async command output buffers
+;; but keep them in the buffer list for later inspection
+(add-to-list 'display-buffer-alist
+ '("*Async Shell Command*" display-buffer-no-window (nil)))
+
+;; ------------------------ Mouse And Trackpad Settings ------------------------
+;; provide smoothest scrolling and avoid accidental gestures
+
+(setq mouse-wheel-follow-mouse 't) ;; scroll window under mouse
+(setq scroll-margin 5) ;; scroll w/in 10 lines of top/bottom
+(setq scroll-step 1) ;; keyboard scroll one line at a time
+
+;; disable pasting with mouse-wheel click
+(global-unset-key (kbd "<mouse-2>"))
+
+;; disable pinching gesture or mouse-wheel changing font size
+(global-unset-key (kbd "<pinch>"))
+(global-set-key [remap mouse-wheel-text-scale] 'cj/disabled)
+
+;; ------------------------------- Be Quiet(er)! -------------------------------
+;; reduces "helpful" instructions that distract Emacs power users.
+
+(setq-default vc-follow-symlinks) ;; don't ask to follow symlinks if target is version controlled
+(setq kill-buffer-query-functions ;; don't ask about killing buffers with processes, just kill them
+ (remq 'process-kill-buffer-query-function
+ kill-buffer-query-functions))
+(setq confirm-kill-processes nil) ;; automatically kill running processes on exit
+(setq confirm-nonexistent-file-or-buffer nil) ;; don't ask if a file I visit with C-x C-f or C-x b doesn't exist
+(setq ad-redefinition-action 'accept) ;; silence warnings about advised functions getting redefined.
+(setq large-file-warning-threshold nil) ;; open files regardless of size
+(fset 'yes-or-no-p 'y-or-n-p) ;; require a single letter for binary answers
+(setq use-short-answers t) ;; same as above with Emacs 28+
+(setq auto-revert-verbose nil) ;; turn off auto revert messages
+(setq custom-safe-themes t) ;; treat all themes as safe (stop asking)
+(setq server-client-instructions nil) ;; I already know what to do when done with the frame
+
+;; ------------------ Reduce Garbage Collections In Minibuffer -----------------
+;; triggers garbage collection when it won't impact user minibuffer entries
+
+(defun cj/minibuffer-setup-hook ()
+ "Hook to prevent garbage collection while user's in minibuffer."
+ (setq gc-cons-threshold most-positive-fixnum))
+
+(defun cj/minibuffer-exit-hook ()
+ "Hook to trigger garbage collection when exiting minibuffer."
+ (setq gc-cons-threshold 800000))
+
+(add-hook 'minibuffer-setup-hook #'cj/minibuffer-setup-hook)
+(add-hook 'minibuffer-exit-hook #'cj/minibuffer-exit-hook)
+
+;; ----------------------------- Bookmark Settings -----------------------------
+;; keep bookmarks in sync location, and save the file whenever a mark is added
+
+;; place bookmark file sync'd org files
+(setq bookmark-default-file (concat sync-dir "emacs_bookmarks"))
+
+;; save bookmarks each (1) time it's modified.
+(setq bookmark-save-flag 1)
+
+;; -------------------------------- Recent Files -------------------------------
+;; don't suggest bookmarks, packages, indexes, or recentf in recent files.
+
+(use-package recentf
+ :defer .5
+ :ensure nil ;;built-in
+ :config
+ (setq recentf-max-saved-items 1000)
+ (setq recentf-max-menu-items 50)
+ (add-to-list 'recentf-exclude "emacs_bookmarks")
+ (add-to-list 'recentf-exclude "\\.emacs\\.d/elpa")
+ (add-to-list 'recentf-exclude "\\.emacs\\.d/recentf")
+ (add-to-list 'recentf-exclude "\\ElfeedDB/index"))
+
+;; -------------------------- Autosave And Lock Files --------------------------
+;; don't create lockfiles or autosave (i.e., filename~) files.
+
+(setq auto-save-default nil)
+(setq create-lockfiles nil)
+
+;; ------------------------------ Backup Settings ------------------------------
+;; per-save backups can be invaluable, so create them in ~/.emacs.d/backups
+
+;; BACKUP DIRECTORY CREATION
+(defvar cj/backup-directory (concat user-emacs-directory "backups"))
+(if (not (file-exists-p cj/backup-directory))
+ (make-directory cj/backup-directory t))
+
+;; BACKUP SETTINGS
+(setq make-backup-files t) ;; do make backup files
+(setq backup-directory-alist `(("." . ,cj/backup-directory))) ;; put all originals in backup directory
+(setq backup-by-copying t) ;; don't clobber symlinks
+(setq version-control t) ;; make numeric backup versions
+(setq delete-old-versions t) ;; delete excess backup files w/o asking
+(setq kept-new-versions 25) ;; keep 25 of the newest backups made (default: 2)
+(setq vc-make-backup-files t) ;; also backup any files in version control
+
+;; ---------------------------- Exec Path From Shell ---------------------------
+;; ensure $PATH is the same between your normal shell and your Emacs shells.
+
+(use-package exec-path-from-shell
+ :defer .5
+ :config
+ (when (daemonp)
+ (exec-path-from-shell-initialize)))
+
+(provide 'system-defaults)
+;;; system-defaults.el ends here
diff --git a/modules/system-utils.el b/modules/system-utils.el
new file mode 100644
index 00000000..bab4f01a
--- /dev/null
+++ b/modules/system-utils.el
@@ -0,0 +1,311 @@
+;;; system-utils --- System-Wide Utilities -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;; ---------------------------------- Ibuffer ----------------------------------
+
+(global-set-key [remap list-buffers] 'ibuffer) ;; use ibuffer, not list-buffers
+
+(use-package nerd-icons-ibuffer
+ :defer .5
+ :after nerd-icons
+ :hook (ibuffer-mode . nerd-icons-ibuffer-mode)
+ :config
+ (setq nerd-icons-ibuffer-icon t)
+ (setq nerd-icons-ibuffer-color-icon t)
+ (setq nerd-icons-ibuffer-human-readable-size t))
+
+;; ------------------------------ Bury Alive List ------------------------------
+;; buries buffers on the 'cj/bury-alive-list' list rather than killing them. The
+;; keybinding for kill-buffer is remapped to this function.
+
+(defvar cj/bury-alive-list '("*dashboard*" "*scratch*" "*Messages*")
+ "Buffers that shouldn't be killed, but buried instead.")
+
+(defun cj/kill-or-bury-alive (target-buffer)
+ "Bury buffers on the bury-instead-list rather than killing them.
+With a prefix argument, add the TARGET-BUFFER to \='cj/bury-alive-list\='."
+ (interactive "bKill or Add to bury (don't kill) buffer list: ")
+ (with-current-buffer target-buffer
+ (if current-prefix-arg
+ (progn
+ (add-to-list 'cj/bury-alive-list (buffer-name (current-buffer)))
+ (message "Added %s to bury-alive-list" (buffer-name (current-buffer))))
+ (if (member (buffer-name (current-buffer)) cj/bury-alive-list)
+ (bury-buffer)
+ (kill-buffer (current-buffer))))))
+
+(global-set-key [remap kill-buffer] #'cj/kill-or-bury-alive)
+
+;; --------------------------- Emacs Server Shutdown ---------------------------
+;; shuts down the Emacs server. useful with emacsclient.
+
+(defun server-shutdown ()
+ "Save buffers, quit, and shutdown (kill) server."
+ (interactive)
+ (save-some-buffers)
+ (kill-emacs))
+(global-set-key (kbd "C-<f10>") 'server-shutdown)
+(global-set-key (kbd "<f10>") 'save-buffers-kill-terminal)
+
+;; --------------------------------- Free Keys ---------------------------------
+;; Displays free keybindings. Allows indicating a specific key prefix.
+
+(use-package free-keys
+ :defer 1
+ :bind ("C-h C-k" . free-keys))
+
+;; --------------------------------- Sudo Edit ---------------------------------
+;; Edit a file the current buffer is visiting as sudo user.
+
+(use-package sudo-edit
+ :defer 1
+ :bind ("C-x M-f" . sudo-edit))
+
+;; --------------------------- Open File With Command --------------------------
+;; opens the current buffer's file with a command. Prompts if interactive.
+
+(defun cj/open-file-with-command (command)
+ "Asynchronously open the file for the current buffer with a specified COMMAND.
+Don't automatically display output buffers, but keep them in buffer list."
+ (interactive "MOpen with program: ")
+ (let ((display-buffer-keywords '(("*Async Shell Command*" display-buffer-no-window (nil)))))
+ (add-to-list 'display-buffer-alist display-buffer-keywords))
+ (async-shell-command (format "%s \"%s\"" command buffer-file-name)))
+
+;; --------------------------------- Open With ---------------------------------
+;; automatically opens files with specific programs using file extensions.
+
+(use-package openwith
+ :defer 1
+ :config
+ (setq openwith-associations
+ (list
+ (list (openwith-make-extension-regexp
+ '("mpg" "mpeg" "mp3" "mp4" "webp"
+ "avi" "wmv" "wav" "mov" "flv"
+ "ogm" "ogg" "mkv"))
+ "mpv"
+ '(file))
+ ;; removed jpg from list below as dashboard was opening nxiv
+ (list (openwith-make-extension-regexp
+ '("xbm" "pbm" "pgm" "ppm" "pnm"
+ "png" "gif" "bmp" "tif"))
+ "nsxiv"
+ '(file))
+ (list (openwith-make-extension-regexp
+ '("odt" "odf"))
+ "libreoffice"
+ '(file))
+ (list (openwith-make-extension-regexp
+ '("cbr" "cbz"))
+ "zathura"
+ '(file)))))
+
+;; --------------------------------- Which Key ---------------------------------
+;; displays key bindings following your currently entered incomplete command
+
+(use-package which-key
+ :defer 1
+ :config
+ (setq which-key-idle-delay 3.0)
+ (setq which-key-popup-type 'side-window)
+ (add-to-list 'which-key-replacement-alist '((nil . "digit-argument") . t))
+ (which-key-setup-side-window-right-bottom)
+ (which-key-mode 1))
+
+;; ------------------------------- Scratch Buffer ------------------------------
+;; make the scratch buffer joyful, org-mode by default, and persistent.
+;; coding tip: org babel + code blocks allows for more flexibility and comments.
+
+(defvar scratch-emacs-version-and-system (concat ";; Welcome to Emacs "
+ emacs-version " running on "
+ system-configuration ".\n"))
+(defvar scratch-greet (concat ";; Emacs ♥ you, " user-login-name ". "
+ "Happy Hacking!\n\n"))
+
+
+(setq initial-scratch-message (concat scratch-emacs-version-and-system
+ scratch-greet))
+
+;; make scratch buffer an org-mode buffer
+(setq initial-major-mode 'org-mode)
+
+;; persists scratch contents between Emacs sessions
+(use-package persistent-scratch
+ :defer .5
+ :custom
+ (persistent-scratch-save-file (expand-file-name ".scratch" user-emacs-directory))
+ :config
+ (persistent-scratch-setup-default))
+
+;; -------------------------------- World Clock --------------------------------
+;; displays current time in various timezones
+
+(use-package time
+ :ensure nil ;; built-in
+ :defer .5
+ :bind ("C-x c" . world-clock)
+ :config
+ (setq world-clock-list
+ '(("Pacific/Honolulu" " Honolulu")
+ ("America/Los_Angeles" " San Francisco")
+ ("America/Chicago" " New Orleans, Chicago")
+ ("America/New_York" " New York")
+ ("Etc/UTC" " UTC ====================================")
+ ("Europe/London" " London")
+ ("Europe/Paris" " Paris, Berlin, Rome, Barcelona")
+ ("Europe/Athens" " Athens, Istanbul, Kyiv, Moscow, Tel Aviv")
+ ("Asia/Yerevan" " Yerevan")
+ ("Asia/Kolkata" " India")
+ ("Asia/Shanghai" " Shanghai, Singapore")
+ ("Asia/Tokyo" " Tokyo, Seoul")))
+ (setq world-clock-time-format " %a, %d %b @ %I:%M %p %Z"))
+
+;; ---------------------------------- Calendar ---------------------------------
+;; providing simple shortcuts
+;; backward and forward day are ',' and '.'
+;; shift & meta moves by week or year
+;; C-. jumps to today
+;; consistent with scheduling in org-mode
+
+(use-package calendar
+ :defer .5
+ :ensure nil ;; built-in
+ :bind
+ ("M-#" . calendar)
+ (:map calendar-mode-map
+ ("," . calendar-backward-day)
+ ("." . calendar-forward-day)
+ ("<" . calendar-backward-month)
+ (">" . calendar-forward-month)
+ ("M-," . calendar-backward-year)
+ ("M-." . calendar-forward-year)))
+
+;; --------------------------------- Dictionary --------------------------------
+;; install Webster's dictionary in StarDict format
+;; http://jsomers.net/blog/dictionary
+
+(use-package sdcv-mode
+ :defer 1
+ :ensure nil; custom-file
+ :load-path "custom/sdcv-mode.el"
+ :bind ("C-h d" . 'sdcv-search))
+
+;; ------------------------------ -Keyboard Macros -----------------------------
+;; note that this leverages simple, easy to remember shortcuts
+;;
+;; start a macro with C-f3, perform your actions, then finish with C-f3 (same key)
+;; run your macro by pressing f3
+;; if you wish to save and edit it (provide a keybinding), use C-u M-f3
+;; otherwise, jsut save it with M-f3 and call it with M-x (name you provided)
+
+(defun kbd-macro-start-or-end ()
+ "Begins a keyboard macro definition, or if one's in progress, finish it."
+ (interactive)
+ (if defining-kbd-macro
+ (end-kbd-macro)
+ (start-kbd-macro nil)))
+(global-set-key (kbd "C-<f3>") 'kbd-macro-start-or-end)
+(global-set-key (kbd"<f3>") 'call-last-kbd-macro)
+(global-set-key (kbd"M-<f3>") 'cj/save-maybe-edit-macro)
+
+(defun cj/save-maybe-edit-macro (name)
+ "Save a macro in `macros-file'.
+Save the last defined macro as NAME at the end of your `macros-file'
+The `macros-file' is defined in the constants section of the `init.el').
+The function offers the option to open the `macros-file' for editing when called with a prefix argument."
+ (interactive "SName of the macro (w/o spaces): ")
+ (kmacro-name-last-macro name)
+ (find-file macros-file)
+ (goto-char (point-max))
+ (newline)
+ (insert-kbd-macro name)
+ (newline)
+ ;; Save changes and switch back to previous buffer
+ (save-buffer)
+ (switch-to-buffer (other-buffer (current-buffer) 1))
+ ;; Check for presence of a prefix argument and open the macros-file for editing if exists
+ (if current-prefix-arg
+ (progn
+ (find-file macros-file)
+ (goto-char (point-max))))
+ ;; Return name for convenience
+ name)
+
+;; now load all saved macros, creating an empty macro-file if it doesn't exist.
+(if (file-exists-p macros-file)
+ (load macros-file)
+ (progn
+ (write-region ";;; -*- lexical-binding: t -*-\n" nil macros-file)
+ (message "Saved macros file not found, so created: %s" macros-file)))
+
+;; ----------------------------- Merge List To List ----------------------------
+;; Convenience method for merging two lists together
+;; https://emacs.stackexchange.com/questions/38008/adding-many-items-to-a-list/68048#68048
+
+(defun cj/merge-list-to-list (dst src)
+ "Merge content of the 2nd list SRC with the 1st one DST."
+ (set dst
+ (append (eval dst) src)))
+
+;; -------------------------------- Log Silently -------------------------------
+;; utility function to log silently to the Messages buffer (for debugging/warning)
+
+(defun cj/log-silently (text)
+ "Send TEXT to the Messages buffer bypassing the echo area."
+ (let ((inhibit-read-only t)
+ (messages-buffer (get-buffer "*Messages*")))
+ (with-current-buffer messages-buffer
+ (goto-char (point-max))
+ (unless (bolp)
+ (insert "\n"))
+ (insert text)
+ (unless (bolp)
+ (insert "\n")))))
+
+;; ----------------------------------- Proced ----------------------------------
+;; yes, a process monitor in Emacs
+
+(use-package proced
+ :defer .5
+ :ensure nil ;;built-in
+ :commands proced
+ :bind ("C-M-p" . proced)
+ :custom
+ (proced-auto-update-flag t)
+ (proced-goal-attribute nil)
+ (proced-show-remote-processes t)
+ (proced-enable-color-flag t)
+ (proced-format 'custom)
+ :config
+ (add-to-list
+ 'proced-format-alist
+ '(custom user pid ppid sess tree pcpu pmem rss start time state (args comm))))
+
+;; ------------------------------- Who Called Me? ------------------------------
+;; convenience function to display which function called a message
+
+(defun who-called-me? (old-fun format &rest args)
+ "Display the function that called a message.
+OLD-FUN: The original function that is being overridden.
+FORMAT : The format string used in the original function's `message` call.
+ARGS : The variables to populate the placeholders in the format string.
+
+This function works by overriding an existing function, usually `message`,
+with a new version that prints a trace of the function call stack before the
+original message."
+ (let ((trace nil) (n 1) (frame nil))
+ (while (setf frame (backtrace-frame n))
+ (setf n (1+ n)
+ trace (cons (cadr frame) trace)) )
+ (apply old-fun (concat "<<%S>>\n" format) (cons trace args))))
+
+;; uncomment this line for the underlying function
+;; (advice-add 'message :around #'who-called-me?)
+
+(provide 'system-utils)
+;;; system-utils.el ends here
diff --git a/modules/telegram-config.el b/modules/telegram-config.el
new file mode 100644
index 00000000..6e1da03b
--- /dev/null
+++ b/modules/telegram-config.el
@@ -0,0 +1,48 @@
+;;; telegram-config.el --- Configuration for the Telegram Client -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;; Telegram Client Setup Notes
+
+;; 1. Pull latest telega-server image:
+;; $ docker pull zevlg/telega-server:latest
+
+;; 2. Set telega-use-docker to non-nil to connect to docker telega-server
+;; (setq telega-use-docker t)
+
+;; 3. Set telega-use-images to non-nil (for emacsclient)
+;; (setq telega-use-images t)
+
+;; 4. keybindings for telega only display if you bind the prefix-map for a list
+;; of default keys: https://zevlg.github.io/telega.el/#telega-prefix-map, so put
+;; this in your use-package declaration:
+;; :bind
+;; ("C-c T" . telega)
+
+;; 5. Set up docker image and tell Telega to use it.
+;; docker pull zevlg/telega-server:latest
+;; (setq telega-use-docker t)
+
+
+;;; Code:
+
+;; ----------------------------------- Telega ----------------------------------
+;; telegram client
+
+(use-package telega
+ :defer 1
+ :commands (telega)
+ :init
+ (define-key global-map (kbd "C-c t") telega-prefix-map)
+ :bind
+ ("C-c T" . telega)
+ (:map telega-chat-button-map
+ ("DEL" . telega-chat-delete))
+ :custom
+ (telega-use-images t)
+ (telega-emoji-use-images t)
+ :config
+ (setq telega-use-docker t))
+
+(provide 'telegram-config)
+;;; telegram-config.el ends here.
diff --git a/modules/test-code.el b/modules/test-code.el
new file mode 100644
index 00000000..6c586f03
--- /dev/null
+++ b/modules/test-code.el
@@ -0,0 +1,170 @@
+;;; test-code.el --- test code -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; This is where you should put config code you want to test.
+;; I recommend calling this file from the end of your init.el
+;; if something breaks, you have most of your Emacs config loaded.
+
+;;; Code:
+
+
+;; --------------------------------- Recording ---------------------------------
+
+(defvar cj/ffmpeg-process nil
+ "Variable to store the process of the ffmpeg recording.")
+
+(defvar cj/recording-location "~/videos/recordings"
+ "The location to save the ffmpeg recordings.")
+
+(defun cj/start-recording (arg)
+ "Starts the ffmpeg recording.
+If called with a prefix arg C-u, choose the location on where to save the recording,
+otherwise use the default location in `cj/recording-location'."
+ (interactive "P")
+ (let* ((location (if arg
+ (read-directory-name "Enter recording location: ")
+ cj/recording-location))
+ (directory (file-name-directory location)))
+ (unless (file-directory-p directory)
+ (make-directory directory t))
+ (cj/ffmpeg-record location)))
+
+(defun cj/ffmpeg-record (directory)
+ "Start an ffmpeg recording. Save output to DIRECTORY."
+ (unless cj/ffmpeg-process
+ (let* ((location (expand-file-name directory))
+ (name (format-time-string "%Y-%m-%d-%H-%M-%S"))
+ (filename (concat location "/" name ".mkv"))
+ (ffmpeg-command
+ (concat "ffmpeg -framerate 30 -f x11grab -i :0.0+ "
+ "-f pulse -i alsa_input.pci-0000_00_1b.0.analog-stereo "
+ "-ac 1 -f pulse -i alsa_output.pci-0000_00_1b.0.analog-stereo.monitor "
+ "-ac 2 " filename)))
+ ;; start the recording
+ (setq cj/ffmpeg-process
+ (start-process-shell-command "ffmpeg-recording"
+ "*ffmpeg-recording*"
+ ffmpeg-command))
+ (set-process-query-on-exit-flag cj/ffmpeg-process nil)
+ (message "Started recording process."))))
+
+(defun cj/stop-recording ()
+ "Stop the ffmpeg recording process."
+ (interactive)
+ (when cj/ffmpeg-process
+ (delete-process cj/ffmpeg-process)
+ (setq cj/ffmpeg-process nil)
+ (message "Stopped recording process.")))
+
+;; ------------------------ Insert Around Word Or Region -----------------------
+
+(defun cj/insert-around-word-or-region ()
+ "Prompt for a string, insert it before and after the word at point or selected region."
+ (interactive)
+ (let ((str (read-string "Enter a string: "))
+ (regionp (use-region-p)))
+ (save-excursion
+ (if regionp
+ (let ((beg (region-beginning))
+ (end (region-end)))
+ (goto-char end)
+ (insert str)
+ (goto-char beg)
+ (insert str))
+ (if (thing-at-point 'word)
+ (let ((bounds (bounds-of-thing-at-point 'word)))
+ (goto-char (cdr bounds))
+ (insert str)
+ (goto-char (car bounds))
+ (insert str))
+ (message "Can't insert around. No word at point and no region selected."))))))
+
+(global-set-key (kbd "C-; i a") 'cj/insert-around-word-or-region)
+
+;; --------------------------------- Easy Hugo ---------------------------------
+
+(use-package easy-hugo
+ :defer .5
+ :init
+ (setq easy-hugo-basedir "~/code/cjennings.net/")
+ (setq easy-hugo-url "https://cjennings.net")
+ (setq easy-hugo-sshdomain "cjennings.net")
+ (setq easy-hugo-root "/var/www/cjennings/")
+ (setq easy-hugo-previewtime "300")
+ (setq easy-hugo-postdir "content")
+ (setq easy-hugo-server-flags "-D")
+ (setq easy-hugo-default-ext ".md")
+ :bind ("C-c H" . easy-hugo)
+ :config
+ (easy-hugo-enable-menu))
+
+;; -------------------------------- Google This --------------------------------
+
+;; BUG: Fix warnings and errors thrown
+(use-package google-this
+ :load-path "~/code/emacs-google-this/"
+ :defer 1
+ :bind
+ ("C-h g" . 'google-this-search)
+ :config
+ (google-this-mode 1)
+ (setq google-this-browse-url-function 'eww-browse-url))
+
+;; ----------------------------------- Wttrin ----------------------------------
+;; show the weather forecast in an Emacs buffer
+
+(use-package wttrin
+ :defer .5
+ :after xterm-color
+ :load-path ("~/code/emacs-wttrin")
+ :bind
+ ("C-c w" . wttrin)
+ :custom
+ (wttrin-unit-system "u")
+ (wttrin-default-cities '(
+ "Albuquerque, New Mexico"
+ "Berkeley, CA"
+ "Boston, Massachussetts"
+ "Chicago, Illinois"
+ "Huntington Beach, CA"
+ "Littlestown, PA"
+ "London, UK"
+ "New Orleans, LA"
+ "New York, New York"
+ "Oakland, California"
+ "Paris, FR"
+ "San Francisco, California"
+ "Santa Fe, New Mexico"
+ "Yerevan, AM"
+ )))
+
+;; dependency for wttrin
+(use-package xterm-color
+ :defer .5)
+
+;; ------------------------------ ERC Yank To Gist -----------------------------
+;; automatically create a Gist if pasting more than 5 lines
+;; this module requires https://github.com/defunkt/gist
+;; via ruby: 'gem install gist' via the aur: yay -S gist
+
+(use-package erc-yank
+ :defer 1
+ :after erc
+ :bind
+ (:map erc-mode-map
+ ("C-y" . erc-yank)))
+
+;; --------------------------------- Ob-Racket ---------------------------------
+
+;; (use-package ob-racket
+;; :load-path "~/code/ob-racket"
+;; :defer .5
+;; :after racket-mode
+;; :commands (org-babel-execute:racket)
+;; :quelpa (ob-racket
+;; :fetcher github
+;; :repo "hasu/emacs-ob-racket"
+;; :files ("*.el" "*.rkt")))
+
+(provide 'test-code)
+;;; test-code.el ends here.
diff --git a/modules/text-config.el b/modules/text-config.el
new file mode 100644
index 00000000..2924521d
--- /dev/null
+++ b/modules/text-config.el
@@ -0,0 +1,102 @@
+;;; text-config --- Text Settings and Functionality -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;; ------------------------------- Text Settings -------------------------------
+
+(defun cj/text-mode-settings ()
+ "Personal settings for \\='text-mode\\='."
+ (turn-on-visual-line-mode) ;; wrap text in text modes (additional modes set elsewhere)
+ (setq-default indent-tabs-mode nil) ;; indentation should not insert tabs
+ (setq sentence-end-double-space nil)) ;; in the 21st century, sentences may end w/ a single space
+(add-hook 'text-mode-hook 'cj/text-mode-settings)
+
+;; don't require newlines at EOF
+(setq require-final-newline nil) ;; don't require newlines at the end of files
+(custom-set-variables '(require-final-newline nil)) ;; some major modes read newline setting from custom
+
+;; --------------------------------- Move Text ---------------------------------
+;; move the current line or selected region up or down in the buffer
+
+(use-package move-text
+ :defer .5
+ :bind
+ ("C-<up>" . move-text-up)
+ ("C-<down>" . move-text-down)
+ :config
+ (move-text-default-bindings))
+
+;; ------------------------------- Expand Region -------------------------------
+;; increase the selected region by semantic units
+
+(use-package expand-region
+ :defer .5
+ :bind
+ ("M-=" . er/expand-region)
+ ("C->" . er/expand-region)
+ ("M--" . er/contract-region)
+ ("C-<" . er/contract-region))
+
+;; ---------------------------- Change Inner / Outer ---------------------------
+;; change inner and outer, just like in vim.
+
+(use-package change-inner
+ :defer .5
+ :bind (("C-c i" . change-inner)
+ ("C-c o" . change-outer)))
+
+;; ------------------------------ Delete Selection -----------------------------
+;; delete the region on character insertion
+
+(use-package delsel
+ :ensure nil ;; built-in
+ :defer .5
+ :custom (delete-selection-mode t))
+
+;; ------------------------------- Edit Indirect -------------------------------
+;; edit selection in new buffer, C-c to finish; replaces with modifications
+
+(use-package edit-indirect
+ :defer 1
+ :bind ("M-I" . edit-indirect-region))
+
+;; ------------------------------ Prettify Symbols -----------------------------
+;; replacing the word l-a-m-b-d-a with a symbol, just because
+
+(setq-default prettify-symbols-alist
+ '(("#+begin_src" . "λ")
+ ("#+BEGIN_SRC" . "λ")
+ ("#+end_src" . "λ")
+ ("#+END_SRC" . "λ")
+ ("lambda" . "λ")))
+
+(add-hook 'prog-mode-hook 'turn-on-prettify-symbols-mode)
+(add-hook 'org-mode-hook 'turn-on-prettify-symbols-mode)
+
+;; ---------------------------------- Olivetti ---------------------------------
+;; center text in the middle of the screen.
+
+(use-package olivetti
+ :defer 1
+ :config
+ (setq-default olivetti-body-width 100))
+
+;; --------------------------- Acccent (Diacriticals) --------------------------
+;; an easy way to enter diacritical marks
+
+(use-package accent
+ :defer 1
+ :bind ("C-c C-a" . accent-company))
+
+;; ----------------------------- Visual Fill Column ----------------------------
+;; text wrapping
+
+(use-package visual-fill-column
+ :defer .5
+ :demand t)
+
+(provide 'text-config)
+;;; text-config.el ends heref
diff --git a/modules/tramp-config.el b/modules/tramp-config.el
new file mode 100644
index 00000000..0ae52db3
--- /dev/null
+++ b/modules/tramp-config.el
@@ -0,0 +1,52 @@
+;;; tramp-config.el --- Tramp Configuration -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+;; TRAMP NOTES:
+;; note that you should disable your fancy prompt if connecting to the
+;; remote server from tramp. Here's what to add to the top of the file
+
+;; [[ $TERM == "dumb" ]] && PS1='$ ' && return
+;; [[ $TERM == "tramp" ]] && PS1='$ ' && return
+
+;;; Code:
+
+(use-package tramp
+ :defer .5
+ :ensure nil ;; built-in
+ :config
+ ;; uncomment for better debugging
+ ;; (setq tramp-debug-buffer t)
+ ;; (setq tramp-verbose 10)
+
+ ;; terminal type reported by tramp to host
+ (setq tramp-terminal-type "dumb")
+
+ ;; use the path assigned to the remote user by the remote host
+ (add-to-list 'tramp-remote-path 'tramp-own-remote-path)
+
+ ;; store auto-save files locally.
+ (setq tramp-auto-save-directory
+ (expand-file-name "tramp-auto-save" user-emacs-directory))
+
+ ;; turn off the backup "$filename~" feature for remote files
+ (setq remote-file-name-inhibit-auto-save-visited t)
+ (add-to-list 'backup-directory-alist
+ (cons tramp-file-name-regexp nil))
+
+ ;; set a more representative name for the persistency file.
+ (setq tramp-persistency-file-name
+ (expand-file-name "tramp-connection-history" user-emacs-directory))
+
+ (setq tramp-copy-size-limit nil) ;; always use external program to copy
+ (setq remote-file-name-inhibit-cache nil) ;; avoid cache invalidation
+
+ ;; cache and don't expire passwords
+ (setq password-cache t)
+ (setq password-cache-expiry nil)
+
+ ;; don't determine remote files vc status (for a performance gain)
+ (setq vc-ignore-dir-regexp tramp-file-name-regexp))
+
+(provide 'tramp-config)
+;;; tramp-config.el ends here
diff --git a/modules/treesitter-config.el b/modules/treesitter-config.el
new file mode 100644
index 00000000..c19564e6
--- /dev/null
+++ b/modules/treesitter-config.el
@@ -0,0 +1,38 @@
+;;; treesitter-config.el --- Treesitter Code Highlighting Configuration -*- lexical-binding: t; -*-
+;;; Commentary:
+
+;; Tree-sitter's now built into Emacs 29+
+
+;;; Code:
+
+
+;; ------------------ Installation And Configuration -----------------
+
+(use-package tree-sitter
+ :defer .5)
+ ;; :hook ((ruby-mode . tree-sitter-hl-mode)
+ ;; (js-mode . tree-sitter-hl-mode)
+ ;; (rust-mode . tree-sitter-hl-mode)
+ ;; (sh-mode . tree-sitter-hl-mode)
+ ;; (c-mode . tree-sitter-hl-mode)
+ ;; (typescript-mode . tree-sitter-hl-mode)
+ ;; (go-mode . tree-sitter-hl-mode)))
+
+;; (use-package tree-sitter-langs
+;; :ensure 'nil ;; built-in
+;; :after tree-sitter)
+
+;; ----------------------- Grammar Installation ----------------------
+;; installs tree-sitter grammars if they're absent
+
+(use-package treesit-auto
+ :defer .5
+ :custom
+ (treesit-auto-install t)
+;; (treesit-auto-install 'prompt)
+ :config
+ (treesit-auto-add-to-auto-mode-alist 'all)
+ (global-treesit-auto-mode))
+
+(provide 'treesitter-config)
+;;; treesitter-config.el ends here.
diff --git a/modules/ui-config.el b/modules/ui-config.el
new file mode 100644
index 00000000..5f0c2801
--- /dev/null
+++ b/modules/ui-config.el
@@ -0,0 +1,76 @@
+;;; ui-config --- User Interface Preferences -*- lexical-binding: t; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;; ----------------------------- System UI Settings ----------------------------
+
+(add-to-list 'initial-frame-alist '(fullscreen . maximized)) ;; start the initial frame maximized
+(add-to-list 'default-frame-alist '(fullscreen . maximized)) ;; start every frame maximized
+(setq pixel-scroll-precision-mode nil) ;; smooth scroll past images - enabled if nil!
+
+(setq-default frame-inhibit-implied-resize t) ;; don't resize frames when setting ui-elements
+(setq frame-title-format '("Emacs " emacs-version " - %b")) ;; the title is just the app name and version
+
+(setq use-file-dialog nil) ;; no file dialog
+(setq use-dialog-box nil) ;; no dialog boxes either
+(column-number-mode 1) ;; show column number in the modeline
+(setq switch-to-buffer-obey-display-actions t) ;; manual buffer switching obeys display action rules
+
+;; -------------------------------- Transparency -------------------------------
+
+(set-frame-parameter (selected-frame) 'alpha '(84 84))
+(add-to-list 'default-frame-alist '(alpha 84 84))
+
+;; ----------------------------------- Cursor ----------------------------------
+;; set cursor color according to mode
+;;
+;; #f06a3f indicates a read-only document
+;; #c48702 indicates overwrite mode
+;; #64aa0f indicates insert and read/write mode
+
+
+(defvar cj/set-cursor-color-color "")
+(defvar cj/set-cursor-color-buffer "")
+
+(defun cj/set-cursor-color-according-to-mode ()
+ "Change the cursor color based on selected minor modes.
+Cursor becomes a red hue when in a read-only buffer,
+turns goldenrod when in overwrite mode, and green otherwise."
+ ;; set-cursor-color is somewhat costly, so only call it when needed:
+ (let ((color
+ (if buffer-read-only "#f06a3f"
+ (if overwrite-mode "#c48702"
+ "#64aa0f"))))
+ (unless (and
+ (string= color cj/set-cursor-color-color)
+ (string= (buffer-name) cj/set-cursor-color-buffer))
+ (set-cursor-color (setq cj/set-cursor-color-color color))
+ (setq cj/set-cursor-color-buffer (buffer-name)))))
+(add-hook 'post-command-hook 'cj/set-cursor-color-according-to-mode)
+(setq cursor-in-non-selected-windows 'nil) ;; don't show cursor in unselected windows
+
+;; SET-CURSOR-TYPE
+(defun cj/set-cursor-type (new-cursor-type)
+ "Set the cursor type of the selected frame to NEW-CURSOR-TYPE.
+When called interactively, prompt for the type to use.
+To get the frame's current cursor type, use `frame-parameters'."
+ (interactive
+ (list (intern (completing-read
+ "Cursor type: "
+ (mapcar 'list '("box" "hollow" "bar" "hbar" nil))))))
+ (modify-frame-parameters (selected-frame)
+ (list (cons 'cursor-type new-cursor-type))))
+(cj/set-cursor-type 'box) ;; start with the box cursor
+
+;; --------------------------------- Nerd Icons --------------------------------
+;; use icons from nerd fonts in the Emacs UI
+
+(use-package nerd-icons
+ :demand t)
+
+
+(provide 'ui-config)
+;;; ui-config.el ends here
diff --git a/modules/ui-navigation.el b/modules/ui-navigation.el
new file mode 100644
index 00000000..44e5ae5d
--- /dev/null
+++ b/modules/ui-navigation.el
@@ -0,0 +1,165 @@
+;;; ui-navigation --- Managing Cursor Placement, Buffers, and Windows -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;; Window/Frame Navigation
+
+;; This section handles situations where we're nagivating with more than one window
+
+;; Shift + arrow keys = move the cursors around the windows/buffers
+;; Control + Shift + arrow keys = resize the windows
+;; Meta + Shift + arrow keys = move the windows around
+
+;; M-H - split windows, creating a new window horizontally to the right
+;; M-V - split windows, creating a new window vertically to the bottom
+;; M-T - toggle the orientation of the split between horizontal and vertical
+;; M-S - swap window positions
+;; M-C - kill the current window
+;; M-O - kill the other window
+
+;;; Code:
+
+
+;; ------------------------------ Window Placement -----------------------------
+
+(use-package windmove
+ :defer .5
+ :config
+ (windmove-default-keybindings)) ; move cursor around with shift+arrows
+
+;; ------------------------------ Window Resizing ------------------------------
+
+(use-package windsize
+ :defer .5
+ :config
+ (windsize-default-keybindings))
+
+;; M-shift = to balance multiple split windows
+(global-set-key (kbd "M-+") 'balance-windows)
+
+;; ------------------------------ Window Splitting -----------------------------
+
+;; SPLIT VERTICALLY
+(defun split-and-follow-right ()
+ "Open a new window horizontally to the right of the current window.
+Open ibuffer in the other window for easy buffer selection."
+ (interactive)
+ (split-window-right)
+ (other-window 1)
+ (ibuffer))
+(global-set-key (kbd "M-V") 'split-and-follow-right)
+
+;; SPLIT HORIZONTALLY
+(defun split-and-follow-below ()
+ "Open a new window vertically below the current window.
+Open ibuffer in the other window for easy buffer selection."
+ (interactive)
+ (split-window-below)
+ (other-window 1)
+ (ibuffer))
+(global-set-key (kbd "M-H") 'split-and-follow-below)
+
+;; ------------------------- Split Window Reorientation ------------------------
+
+(defun toggle-window-split ()
+ "Toggle the orientation fo the window split.
+If the window is split horizontally, change the split to be vertical.
+If it's vertical, change the split to be to horizontal.
+This function won't work with more than one split window."
+ (interactive)
+ (if (= (count-windows) 2)
+ (let* ((this-win-buffer (window-buffer))
+ (next-win-buffer (window-buffer (next-window)))
+ (this-win-edges (window-edges (selected-window)))
+ (next-win-edges (window-edges (next-window)))
+ (this-win-2nd (not (and (<= (car this-win-edges)
+ (car next-win-edges))
+ (<= (cadr this-win-edges)
+ (cadr next-win-edges)))))
+ (splitter
+ (if (= (car this-win-edges)
+ (car (window-edges (next-window))))
+ 'split-window-horizontally
+ 'split-window-vertically)))
+ (delete-other-windows)
+ (let ((first-win (selected-window)))
+ (funcall splitter)
+ (if this-win-2nd (other-window 1))
+ (set-window-buffer (selected-window) this-win-buffer)
+ (set-window-buffer (next-window) next-win-buffer)
+ (select-window first-win)
+ (if this-win-2nd (other-window 1))))))
+(global-set-key (kbd "M-T") 'toggle-window-split)
+
+;; SWAP WINDOW POSITIONS
+(global-set-key (kbd "M-S") 'window-swap-states)
+
+;; ------------------------------ Killing Windows ------------------------------
+
+;; KILL THIS WINDOW
+(global-set-key (kbd "M-C") 'kill-buffer-and-window)
+
+;; KILL OTHER WINDOW
+(defun cj/kill-other-window ()
+ "Close the next window and kill any buffer in it."
+ (interactive)
+ (other-window 1)
+ (kill-this-buffer)
+ (if (not (one-window-p))
+ (delete-window)))
+(global-set-key (kbd "M-O") 'cj/kill-other-window)
+
+
+;; KILL-ALL-OTHER-BUFFERS
+(defun cj/kill-all-other-buffers-and-windows ()
+ "Save buffers, then kill all other buffers and windows."
+ (interactive)
+ (save-some-buffers)
+ (delete-other-windows)
+ (mapc 'kill-buffer (delq (current-buffer) (buffer-list))))
+(global-set-key (kbd "M-M") 'cj/kill-all-other-buffers-and-windows)
+
+;; ---------------------------- Buffer Manipulation ----------------------------
+
+;; MOVE BUFFER
+(use-package buffer-move
+ ;; :straight (buffer-move :type git :host github :repo "lukhas/buffer-move"
+ ;; :fork (:host github :repo "cjennings/buffer-move"))
+ :bind
+ ("M-S-<down>" . 'buf-move-down)
+ ("M-S-<up>" . 'buf-move-up)
+ ("M-S-<left>" . 'buf-move-left)
+ ("M-S-<right>" . 'buf-move-right))
+
+
+;; UNDO KILL BUFFER
+(defun cj/undo-kill-buffer (arg)
+ "Re-open the last buffer killed. With ARG, re-open the nth buffer."
+ (interactive "p")
+ (let ((recently-killed-list (copy-sequence recentf-list))
+ (buffer-files-list
+ (delq nil (mapcar (lambda (buf)
+ (when (buffer-file-name buf)
+ (expand-file-name (buffer-file-name buf)))) (buffer-list)))))
+ (mapc
+ (lambda (buf-file)
+ (setq recently-killed-list
+ (delq buf-file recently-killed-list)))
+ buffer-files-list)
+ (find-file
+ (if arg (nth arg recently-killed-list)
+ (car recently-killed-list)))))
+(global-set-key (kbd "M-Z") 'cj/undo-kill-buffer)
+
+;; ---------------------------- Undo Layout Changes ----------------------------
+;; allows you to restore your window setup with C-c left-arrow
+;; or redo a window change with C-c right-arrow if you change your mind
+
+(use-package winner-mode
+ :ensure nil ;; built-in
+ :defer .5
+ :bind ("C-z C-z" . winner-undo))
+
+ (provide 'ui-navigation)
+;;; ui-navigation.el ends here
diff --git a/modules/ui-theme.el b/modules/ui-theme.el
new file mode 100644
index 00000000..609acab3
--- /dev/null
+++ b/modules/ui-theme.el
@@ -0,0 +1,110 @@
+;;; ui-theme.el --- UI Theme Configuration and Persistence -*- lexical-binding: t; -*-
+;; Craig Jennings <c@cjennings.net>
+;;
+;;; Commentary:
+
+;; Add custom theme files to "themes" subdirectory.
+;; Load other preferred themes via use-package.
+;; cj/switch-theme function unloads previous themes then applies the chosen theme.
+;; Persist themes between sessions using a file to store the theme name.
+;; The persist file can live in a sync'd dir, so theme choice may persist across machines.
+
+;;; Code:
+
+;; ----------------------------------- Themes ----------------------------------
+;; theme choices and settings
+
+;; downloaded custom themes go in themes subdirectory
+(setq custom-safe-themes t) ;; trust all custom themes
+(add-to-list 'custom-theme-load-path
+ (concat user-emacs-directory "themes"))
+
+(use-package ef-themes)
+(use-package github-dark-vscode-theme)
+(use-package madhat2r-theme)
+(use-package adwaita-dark-theme)
+
+;; ------------------------------- Switch Themes -------------------------------
+;; loads themes in completing read, then persists via the functions below
+
+(defun cj/switch-themes ()
+ "Function to switch themes and save chosen theme name for persistence.
+Unloads any other applied themes before applying the chosen theme."
+ (interactive)
+ (let ((chosentheme ""))
+ (setq chosentheme
+ (completing-read "Load custom theme: "
+ (mapcar 'symbol-name
+ (custom-available-themes))))
+ (mapcar #'disable-theme custom-enabled-themes)
+ (load-theme (intern chosentheme) t))
+ (cj/save-theme-to-file))
+(global-set-key (kbd "M-L") 'cj/switch-themes)
+
+;; ----------------------------- Theme Persistence -----------------------------
+;; persistence utility functions used by switch themes.
+
+(defvar theme-file (concat sync-dir "emacs-theme.persist")
+ "The location of the file to persist the theme name.")
+
+(defvar fallback-theme-name "wombat"
+ "The name of the theme to fallback on.
+This is used then there's no file, or the theme name doesn't match
+any of the installed themes. If theme name is 'nil', there will be
+no theme.")
+
+(defun cj/read-file-contents (filename)
+ "Read FILENAME and return its content as a string.
+If FILENAME isn't readable, return nil."
+ (if (file-readable-p filename)
+ (with-temp-buffer
+ (insert-file-contents filename)
+ (buffer-string))
+ 'nil))
+
+(defun cj/write-file-contents (content filename)
+ "Write CONTENT to FILENAME.
+If FILENAME isn't writeable, return nil. If successful, return t."
+ (if (file-writable-p filename)
+ (progn
+ (with-temp-buffer
+ (insert content)
+ (write-file filename))
+ 't)
+ 'nil))
+
+(defun cj/get-active-theme-name ()
+ "Return the name of the active UI theme as a string."
+ (symbol-name (car custom-enabled-themes)))
+
+(defun cj/save-theme-to-file ()
+ "Save the string representing the current theme to the theme-file."
+ (if (equal (cj/write-file-contents (cj/get-active-theme-name) theme-file) 'nil)
+ (message "Cannot save theme: %s is unwriteable" theme-file)
+ (message "%s theme saved to %s" (cj/get-active-theme-name) theme-file)))
+
+(defun cj/load-fallback-theme (msg)
+ "Display MSG and load ui-theme fallback-theme-name.
+Used to handle errors with loading persisted theme."
+ (message (concat msg (format " Loading fallback theme %s" fallback-theme-name)))
+ (load-theme fallback-theme-name t))
+
+(defun cj/load-theme-from-file ()
+ "Apply the thame name contained in theme-file as the active UI theme.
+If the theme is nil, it disables all current themes. If an error occurs
+loading the file name, the fallback-theme-name is applied and saved."
+ (let ((theme-name (cj/read-file-contents theme-file)))
+ ;; if theme-name is nil, unload all themes
+ (if (string= theme-name "nil")
+ (mapcar #'disable-theme custom-enabled-themes)
+ ;; apply theme name or if error, load fallback theme
+ (progn
+ (condition-case err
+ (load-theme (intern theme-name) t)
+ (error
+ (cj/load-fallback-theme (concat "Error loading " theme-name "."))))))))
+
+(cj/load-theme-from-file)
+
+(provide 'ui-theme)
+;;; ui-theme.el ends here.
diff --git a/modules/vc-config.el b/modules/vc-config.el
new file mode 100644
index 00000000..d2cb183b
--- /dev/null
+++ b/modules/vc-config.el
@@ -0,0 +1,97 @@
+
+;;; vc-config.el --- Version Control Configuration -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; C-x g is my general entry to Magit's version control via the status page.
+
+;; Navigating changes in file happens via git gutter
+;; - C-v d will allow jumping to changes
+;; - C-v n and p will bring to next/previous changes
+
+;; Reviewing previous versions happens through git timemahine
+;; - C-v t allows viewing this file by selecting a previous commit
+;; - Once in timemachine, n and p will take you to previous/next commits
+;; - To exit timemachine, press q in the read-only timemachine buffer
+
+;;; Code:
+
+;; ---------------------------- Magit Configuration ----------------------------
+
+(use-package magit
+ :defer .5
+ :bind ("C-x g" . magit-status)
+ :hook
+ (magit-log-mode . display-line-numbers-mode)
+ :custom
+ (magit-define-global-key-bindings 'default)
+ :config
+ (setf vc-handled-backends nil) ;; magit is the only vc interface I use
+ (setq magit-bury-buffer-function 'magit-restore-window-configuration)
+ (setq git-commit-major-mode 'org-mode) ;; edit commit messages in org-mode
+ (setq magit-display-buffer-function 'magit-display-buffer-fullframe-status-topleft-v1))
+
+;; -------------------------------- Magit Forge --------------------------------
+
+(use-package forge
+ :after magit)
+
+;; --------------------------------- Git Gutter --------------------------------
+;; mark changed lines since last commit in the margin
+
+(use-package git-gutter
+ :defer .5
+ :hook (prog-mode . git-gutter-mode)
+ :custom
+ (git-gutter:modified-sign "~")
+ (git-gutter:added-sign "+")
+ (git-gutter:deleted-sign "-")
+ (git-gutter:update-interval 0.05))
+
+;; ------------------------------ Git Timemachine ------------------------------
+;; walk through revisions of the current file in your buffer
+
+;; this function from:
+;; https://blog.binchen.org/posts/new-git-timemachine-ui-based-on-ivy-mode/
+(defun cj/git-timemachine-show-selected-revision ()
+ "Show last (current) revision of file."
+ (interactive)
+ (let* ((collection
+ (mapcar (lambda (rev)
+ ;; re-shape list for the ivy-read
+ (cons (concat (substring-no-properties (nth 0 rev) 0 7) " | " (nth 5 rev) " | " (nth 6 rev)) rev))
+ (git-timemachine--revisions))))
+ (ivy-read "commits:"
+ collection
+ :action (lambda (rev)
+ ;; compatible with ivy 9+ and ivy 8
+ (unless (string-match-p "^[a-z0-9]*$" (car rev))
+ (setq rev (cdr rev)))
+ (git-timemachine-show-revision rev)))))
+
+(defun cj/git-timemachine ()
+ "Open git snapshot with the selected version. Based on ivy-mode."
+ (interactive)
+ (unless (featurep 'git-timemachine)
+ (require 'git-timemachine))
+ (git-timemachine--start #'cj/git-timemachine-show-selected-revision))
+
+(use-package git-timemachine
+ :defer .5)
+
+;; --------------------------------- VC Keymap ---------------------------------
+;; version control keymap
+
+(global-unset-key (kbd "C-v"))
+(defvar cj/vc-keymap
+ (let ((map (make-sparse-keymap)))
+ (define-key map "t" 'cj/git-timemachine)
+ (define-key map "d" 'cj/goto-git-gutter-diff-hunks)
+ (define-key map "n" 'git-gutter:next-hunk)
+ (define-key map "p" 'git-gutter:previous-hunk)
+ map)
+ "My version control key map.")
+(global-set-key (kbd "C-v") cj/vc-keymap)
+
+
+(provide 'vc-config)
+;;; vc-config.el ends here.
diff --git a/modules/wrap-up.el b/modules/wrap-up.el
new file mode 100644
index 00000000..fcc0b788
--- /dev/null
+++ b/modules/wrap-up.el
@@ -0,0 +1,30 @@
+;;; wrapup --- Functions Run Before Init Completion -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;;; Code:
+
+;; -------------------------------- Bury Buffers -------------------------------
+;; wait a few seconds then bury compile-related buffers.
+
+(defun cj/bury-buffers ()
+ "Bury comint and compilation buffers."
+ (dolist (buf (buffer-list))
+ (with-current-buffer buf
+ (when (or (derived-mode-p 'comint-mode)
+ (derived-mode-p 'compilation-mode)
+ (derived-mode-p 'debugger-mode)
+ (derived-mode-p 'elisp-compile-mode)
+ (derived-mode-p 'messages-buffer-mode)
+ ) ;; byte-compilations
+ (bury-buffer)))))
+
+(defun cj/bury-buffers-after-delay ()
+ "Run cj/bury-buffers after a delay."
+ (run-with-timer 10 nil 'cj/bury-buffers))
+
+(add-hook 'emacs-startup-hook 'cj/bury-buffers-after-delay)
+
+(provide 'wrap-up)
+;;; wrap-up.el ends here