diff options
| author | Craig Jennings <c@cjennings.net> | 2024-04-07 13:41:34 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2024-04-07 13:41:34 -0500 |
| commit | 754bbf7a25a8dda49b5d08ef0d0443bbf5af0e36 (patch) | |
| tree | f1190704f78f04a2b0b4c977d20fe96a828377f1 /modules | |
new repository
Diffstat (limited to 'modules')
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 |
