summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/auth-config.el82
-rw-r--r--modules/config-utilities.el12
-rw-r--r--modules/dirvish-config.el17
-rw-r--r--modules/flycheck-config.el54
-rw-r--r--modules/modeline-config.el171
-rw-r--r--modules/org-config.el13
-rw-r--r--modules/org-gcal-config.el100
-rw-r--r--modules/system-utils.el13
-rw-r--r--modules/transcription-config.el390
-rw-r--r--modules/ui-config.el9
-rw-r--r--modules/user-constants.el17
-rw-r--r--modules/vc-config.el5
-rw-r--r--modules/video-audio-recording.el12
-rw-r--r--modules/weather-config.el25
14 files changed, 819 insertions, 101 deletions
diff --git a/modules/auth-config.el b/modules/auth-config.el
index 6b8a8ddb..2b52087e 100644
--- a/modules/auth-config.el
+++ b/modules/auth-config.el
@@ -24,9 +24,11 @@
:ensure nil ;; built in
:demand t ;; load this package immediately
:config
- (setenv "GPG_AGENT_INFO" nil) ;; disassociate with external gpg agent
- (setq auth-sources `(,authinfo-file)) ;; use authinfo.gpg (see user-constants.el)
- (setq auth-source-debug t)) ;; echo debug info to Messages
+ ;; USE gpg-agent for passphrase caching (400-day cache from gpg-agent.conf)
+ ;; (setenv "GPG_AGENT_INFO" nil) ;; DISABLED: was preventing gpg-agent cache
+ (setq auth-sources `(,authinfo-file)) ;; use authinfo.gpg (see user-constants.el)
+ (setq auth-source-debug t) ;; echo debug info to Messages
+ (setq auth-source-cache-expiry 86400)) ;; cache decrypted credentials for 24 hours
;; ----------------------------- Easy PG Assistant -----------------------------
;; Key management, cryptographic operations on regions and files, dired
@@ -40,5 +42,79 @@
;; (setq epa-pinentry-mode 'loopback) ;; emacs request passwords in minibuffer
(setq epg-gpg-program "gpg2")) ;; force use gpg2 (not gpg v.1)
+;; ---------------------------------- Plstore ----------------------------------
+;; Encrypted storage used by oauth2-auto for Google Calendar tokens.
+;; CRITICAL: Enable passphrase caching to prevent password prompts every 10 min.
+
+(use-package plstore
+ :ensure nil ;; built-in
+ :demand t
+ :config
+ ;; Cache passphrase indefinitely (relies on gpg-agent for actual caching)
+ (setq plstore-cache-passphrase-for-symmetric-encryption t)
+ ;; Allow gpg-agent to cache the passphrase (400 days per gpg-agent.conf)
+ (setq plstore-encrypt-to nil)) ;; Use symmetric encryption, not key-based
+
+;; ------------------------ Authentication Reset Utility -----------------------
+
+(defun cj/reset-auth-cache (&optional include-gpg-agent)
+ "Reset authentication caches when wrong password was entered.
+
+By default, only clears Emacs-side caches (auth-source, EPA file
+handler) and leaves gpg-agent's long-term cache intact. This preserves
+your 400-day cache for GPG and SSH passphrases.
+
+With prefix argument INCLUDE-GPG-AGENT (\\[universal-argument]), also
+clears gpg-agent's password cache. Use this when gpg-agent itself has
+cached an incorrect password.
+
+Clears:
+1. auth-source cache (Emacs-level credential cache)
+2. EPA file handler cache (encrypted file cache)
+3. gpg-agent cache (only if INCLUDE-GPG-AGENT is non-nil)
+
+Use this when you see errors like:
+ - \"Bad session key\"
+ - \"Decryption failed\"
+ - GPG repeatedly using wrong cached password"
+ (interactive "P")
+ (message "Resetting authentication caches...")
+
+ ;; Clear auth-source cache (Emacs credential cache)
+ (auth-source-forget-all-cached)
+
+ ;; Clear EPA file handler cache
+ (when (fboundp 'epa-file-clear-cache)
+ (epa-file-clear-cache))
+
+ ;; Only clear gpg-agent cache if explicitly requested
+ (if include-gpg-agent
+ (let ((result (shell-command "echo RELOADAGENT | gpg-connect-agent")))
+ (if (zerop result)
+ (message "✓ Emacs and gpg-agent caches cleared. Next access will prompt for password.")
+ (message "⚠ Warning: Failed to clear gpg-agent cache")))
+ (message "✓ Emacs caches cleared. GPG/SSH passphrases preserved for session.")))
+
+(defun cj/kill-gpg-agent ()
+ "Force kill gpg-agent (it will restart automatically on next use).
+
+This is a more aggressive reset than `cj/reset-auth-cache'. Use this
+when gpg-agent is stuck or behaving incorrectly.
+
+The gpg-agent will automatically restart on the next GPG operation."
+ (interactive)
+ (let ((result (shell-command "gpgconf --kill gpg-agent")))
+ (if (zerop result)
+ (message "✓ gpg-agent killed. It will restart automatically on next use.")
+ (message "⚠ Warning: Failed to kill gpg-agent"))))
+
+;; Keybindings
+(with-eval-after-load 'keybindings
+ (keymap-set cj/custom-keymap "A" #'cj/reset-auth-cache))
+
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-; A" "reset auth cache"))
+
(provide 'auth-config)
;;; auth-config.el ends here.
diff --git a/modules/config-utilities.el b/modules/config-utilities.el
index 32018371..2af3effa 100644
--- a/modules/config-utilities.el
+++ b/modules/config-utilities.el
@@ -33,8 +33,7 @@
"C-c d i b" "info build"
"C-c d i p" "info packages"
"C-c d i f" "info features"
- "C-c d r" "reload init"
- "C-c d a" "reset auth cache"))
+ "C-c d r" "reload init"))
;;; --------------------------------- Profiling ---------------------------------
@@ -283,15 +282,6 @@ Recompile natively when supported, otherwise fall back to byte compilation."
(load-file user-init-file))
(keymap-set cj/debug-config-keymap "r" 'cj/reload-init-file)
-;; ----------------------------- Reset-Auth-Sources ----------------------------
-
-(defun cj/reset-auth-cache ()
- "Clear Emacs auth-source cache."
- (interactive)
- (auth-source-forget-all-cached)
- (message "Emacs auth-source cache cleared."))
-(keymap-set cj/debug-config-keymap "a" 'cj/reset-auth-cache)
-
;; ------------------------ Validate Org Agenda Entries ------------------------
(defun cj/validate-org-agenda-timestamps ()
diff --git a/modules/dirvish-config.el b/modules/dirvish-config.el
index 441ff61b..b10c97f0 100644
--- a/modules/dirvish-config.el
+++ b/modules/dirvish-config.el
@@ -8,15 +8,15 @@
;; ediff, playlist creation, path copying, and external file manager integration.
;;
;; Key Bindings:
-;; - d: Duplicate file at point (adds "-copy" before extension)
-;; - D: Delete marked files immediately (dired-do-delete)
+;; - d: Delete marked files (dired-do-delete)
+;; - D: Duplicate file at point (adds "-copy" before extension)
;; - g: Quick access menu (jump to predefined directories)
;; - G: Search with deadgrep in current directory
;; - f: Open system file manager in current directory
;; - o/O: Open file with xdg-open/custom command
;; - l: Copy file path (project-relative or home-relative)
;; - L: Copy absolute file path
-;; - P: Create M3U playlist from marked audio files
+;; - P: Copy file path (same as 'l', replaces dired-do-print)
;; - M-D: DWIM menu (context actions for files)
;; - TAB: Toggle subtree expansion
;; - F11: Toggle sidebar view
@@ -120,9 +120,9 @@ Filters for audio files, prompts for the playlist name, and saves the resulting
(setq dired-listing-switches "-l --almost-all --human-readable --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; just kill buffers associated with deleted files
- (setq dired-recursive-copies (quote always)) ;; “always” means no asking
- (setq dired-recursive-deletes (quote top))) ;; “top” means ask once
+ (setq dired-clean-confirm-killing-deleted-buffers nil) ;; don't ask; just kill buffers associated with deleted files
+ (setq dired-recursive-copies (quote always)) ;; "always" means no asking
+ (setq dired-recursive-deletes (quote top))) ;; "top" means ask once
;; note: disabled as it prevents marking and moving files to another directory
;; (setq dired-kill-when-opening-new-dired-buffer t) ;; don't litter by leaving buffers when navigating directories
@@ -322,13 +322,14 @@ regardless of what file or subdirectory the point is on."
("M-p" . dirvish-peek-toggle)
("M-s" . dirvish-setup-menu)
("TAB" . dirvish-subtree-toggle)
- ("d" . cj/dirvish-duplicate-file)
+ ("d" . dired-do-delete)
+ ("D" . cj/dirvish-duplicate-file)
("f" . cj/dirvish-open-file-manager-here)
("g" . dirvish-quick-access)
("o" . cj/xdg-open)
("O" . cj/open-file-with-command) ; Prompts for command to run
("r" . dirvish-rsync)
- ("P" . cj/dired-create-playlist-from-marked)
+ ("P" . cj/dired-copy-path-as-kill)
("s" . dirvish-quicksort)
("v" . dirvish-vc-menu)
("y" . dirvish-yank-menu)))
diff --git a/modules/flycheck-config.el b/modules/flycheck-config.el
index ea19f08f..e2e8abe9 100644
--- a/modules/flycheck-config.el
+++ b/modules/flycheck-config.el
@@ -6,30 +6,30 @@
;; This file configures Flycheck for on-demand syntax and grammar checking.
;; - Flycheck starts automatically only in sh-mode and emacs-lisp-mode
-;; - This binds a custom helper (=cj/flycheck-list-errors=) to “C-; ?”
+;; - This binds a custom helper (=cj/flycheck-list-errors=) to "C-; ?"
;; for popping up Flycheck's error list in another window.
-;; - It also customizes Checkdoc to suppress only the “sentence-end-double-space”
-;; and “warn-escape” warnings.
+;; - It also customizes Checkdoc to suppress only the "sentence-end-double-space"
+;; and "warn-escape" warnings.
-;; - It registers a Proselint checker for prose files
-;; (text-mode, markdown-mode, gfm-mode).
+;; - It registers LanguageTool for comprehensive grammar checking of prose files
+;; (text-mode, markdown-mode, gfm-mode, org-mode).
-;; Note: I do use proselint quite a bit in emails and org-mode files. However, some
-;; org-files can be large and running proselint on them will slow Emacs to a crawl.
-;; Therefore, hitting "C-; ?" also runs cj/flycheck-prose-on-demand if in an org buffer.
+;; Note: Grammar checking is on-demand only to avoid performance issues.
+;; Hitting "C-; ?" runs cj/flycheck-prose-on-demand if in an org buffer.
-;;
;; The cj/flycheck-prose-on-demand function:
;; - Turns on flycheck for the local buffer
-;; - ensures proselint is added
-;; - triggers an immediate check
-;;
-;; Since this is called within cj/flycheck-list-errors, flycheck's error list will still
-;; display and the focus transferred to that buffer.
+;; - Enables LanguageTool checker
+;; - Triggers an immediate check
+;; - Displays errors in the *Flycheck errors* buffer
-;; OS Dependencies:
-;; proselint (in the Arch AUR)
+;; Installation:
+;; On Arch Linux:
+;; sudo pacman -S languagetool
+;;
+;; The wrapper script at scripts/languagetool-flycheck formats LanguageTool's
+;; JSON output into flycheck-compatible format. It requires Python 3.
;;; Code:
@@ -62,20 +62,20 @@
;; use the load-path of the currently running Emacs instance
(setq flycheck-emacs-lisp-load-path 'inherit)
- ;; Define the prose checker (installed separately via OS).
- (flycheck-define-checker proselint
- "A linter for prose."
- :command ("proselint" source-inplace)
+ ;; Define LanguageTool checker for comprehensive grammar checking
+ (flycheck-define-checker languagetool
+ "A grammar checker using LanguageTool.
+Uses a wrapper script to format output for flycheck."
+ :command ("~/.emacs.d/scripts/languagetool-flycheck"
+ 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 org-mode))
- (add-to-list 'flycheck-checkers 'proselint)
+ (add-to-list 'flycheck-checkers 'languagetool)
(defun cj/flycheck-list-errors ()
"Display flycheck's error list and switch to its buffer.
-
Runs flycheck-prose-on-demand if in an org-buffer."
(interactive)
(when (derived-mode-p 'org-mode)
@@ -85,12 +85,14 @@ Runs flycheck-prose-on-demand if in an org-buffer."
(switch-to-buffer-other-window "*Flycheck errors*"))
(defun cj/flycheck-prose-on-demand ()
- "Enable Flycheck+Proselint in this buffer, run it, and show errors."
+ "Enable Flycheck with LanguageTool in this buffer, run it, and show errors."
(interactive)
;; turn on Flycheck locally
(flycheck-mode 1)
- ;; ensure proselint is valid for org/text
- (flycheck-add-mode 'proselint major-mode)
+ ;; ensure LanguageTool is valid for current mode
+ (flycheck-add-mode 'languagetool major-mode)
+ ;; select LanguageTool as the checker
+ (setq-local flycheck-checker 'languagetool)
;; trigger immediate check
(flycheck-buffer)))
diff --git a/modules/modeline-config.el b/modules/modeline-config.el
index af0c3524..b1403539 100644
--- a/modules/modeline-config.el
+++ b/modules/modeline-config.el
@@ -3,29 +3,170 @@
;;; Commentary:
-;; Minimal modeline configuration using mood-line.
-
-;; mood-line is a lightweight, minimal modeline inspired by doom-modeline
-;; but with much better performance and simpler configuration.
+;; Simple, minimal modeline using only built-in Emacs functionality.
+;; No external packages = no buffer issues, no native-comp errors.
;; Features:
-;; - Buffer status and modification indicators
-;; - Major mode display
+;; - Buffer status (modified, read-only)
+;; - Buffer name
+;; - Major mode
;; - Version control status
-;; - Flycheck/Flymake status
-;; - Cursor position and buffer percentage
-;; - Anzu and multiple-cursors counters
-;; - No dependencies
-;; - Minimal performance overhead
+;; - Line and column position
+;; - Buffer percentage
;;; Code:
-;; -------------------------------- mood-line ----------------------------------
+;; Use buffer status colors from user-constants
+(require 'user-constants)
+
+;; -------------------------- Modeline Configuration --------------------------
+
+;; Use Emacs 30's built-in right-alignment
+(setq mode-line-right-align-edge 'right-margin)
+
+;; String truncation length for narrow windows
+(defcustom cj/modeline-string-truncate-length 12
+ "String length after which truncation happens in narrow windows."
+ :type 'natnum
+ :group 'modeline)
+
+;; -------------------------- Helper Functions ---------------------------------
+
+(defun cj/modeline-window-narrow-p ()
+ "Return non-nil if window is narrow (less than 100 chars wide)."
+ (< (window-total-width) 100))
+
+(defun cj/modeline-string-truncate-p (str)
+ "Return non-nil if STR should be truncated."
+ (and (stringp str)
+ (not (string-empty-p str))
+ (cj/modeline-window-narrow-p)
+ (> (length str) cj/modeline-string-truncate-length)
+ (not (one-window-p :no-minibuffer))))
+
+(defun cj/modeline-string-cut-middle (str)
+ "Truncate STR in the middle if appropriate, else return STR.
+Example: `my-very-long-name.el' → `my-ver...me.el'"
+ (if (cj/modeline-string-truncate-p str)
+ (let ((half (floor cj/modeline-string-truncate-length 2)))
+ (concat (substring str 0 half) "..." (substring str (- half))))
+ str))
+
+;; -------------------------- Modeline Segments --------------------------------
+
+(defvar-local cj/modeline-buffer-name
+ '(:eval (let* ((state (cond
+ (buffer-read-only 'read-only)
+ (overwrite-mode 'overwrite)
+ (t 'normal)))
+ (color (alist-get state cj/buffer-status-colors))
+ (name (buffer-name))
+ (truncated-name (cj/modeline-string-cut-middle name)))
+ (propertize truncated-name
+ 'face `(:foreground ,color)
+ 'mouse-face 'mode-line-highlight
+ 'help-echo (concat
+ name "\n"
+ (or (buffer-file-name)
+ (format "No file. Directory: %s" default-directory)))
+ 'local-map (let ((map (make-sparse-keymap)))
+ (define-key map [mode-line mouse-1] 'previous-buffer)
+ (define-key map [mode-line mouse-3] 'next-buffer)
+ map))))
+ "Buffer name colored by read-only/read-write status.
+Green = writeable, Red = read-only, Gold = overwrite.
+Truncates in narrow windows. Click to switch buffers.")
+
+(defvar-local cj/modeline-position
+ '(:eval (format "L:%d C:%d" (line-number-at-pos) (current-column)))
+ "Line and column position as L:line C:col.")
+
+(defvar cj/modeline-vc-faces
+ '((added . vc-locally-added-state)
+ (edited . vc-edited-state)
+ (removed . vc-removed-state)
+ (missing . vc-missing-state)
+ (conflict . vc-conflict-state)
+ (locked . vc-locked-state)
+ (up-to-date . vc-up-to-date-state))
+ "VC state to face mapping.")
+
+(defvar-local cj/modeline-vc-branch
+ '(:eval (when (mode-line-window-selected-p) ; Only show in active window
+ (when-let* ((file (or buffer-file-name default-directory))
+ (backend (vc-backend file)))
+ (when-let* ((branch (vc-working-revision file backend)))
+ ;; For Git, try to get symbolic branch name
+ (when (eq backend 'Git)
+ (require 'vc-git)
+ (when-let* ((symbolic (vc-git--symbolic-ref file)))
+ (setq branch symbolic)))
+ ;; Get VC state for face
+ (let* ((state (vc-state file backend))
+ (face (alist-get state cj/modeline-vc-faces 'vc-up-to-date-state))
+ (truncated-branch (cj/modeline-string-cut-middle branch)))
+ (concat
+ (propertize (char-to-string #xE0A0) 'face 'shadow) ; Git branch symbol
+ " "
+ (propertize truncated-branch
+ 'face face
+ 'mouse-face 'mode-line-highlight
+ 'help-echo (format "Branch: %s\nState: %s\nmouse-1: vc-diff\nmouse-3: vc-root-diff" branch state)
+ 'local-map (let ((map (make-sparse-keymap)))
+ (define-key map [mode-line mouse-1] 'vc-diff)
+ (define-key map [mode-line mouse-3] 'vc-root-diff)
+ map))))))))
+ "Git branch with symbol and colored by VC state.
+Shows only in active window. Truncates in narrow windows.
+Click to show diffs with `vc-diff' or `vc-root-diff'.")
+
+(defvar-local cj/modeline-major-mode
+ '(:eval (let ((mode-str (format-mode-line mode-name)) ; Convert to string
+ (mode-sym major-mode))
+ (propertize mode-str
+ 'mouse-face 'mode-line-highlight
+ 'help-echo (if-let* ((parent (get mode-sym 'derived-mode-parent)))
+ (format "Major mode: %s\nDerived from: %s\nmouse-1: describe-mode" mode-sym parent)
+ (format "Major mode: %s\nmouse-1: describe-mode" mode-sym))
+ 'local-map (let ((map (make-sparse-keymap)))
+ (define-key map [mode-line mouse-1] 'describe-mode)
+ map))))
+ "Major mode name only (no minor modes).
+Click to show help with `describe-mode'.")
+
+(defvar-local cj/modeline-misc-info
+ '(:eval (when (mode-line-window-selected-p)
+ mode-line-misc-info))
+ "Misc info (chime notifications, etc).
+Shows only in active window.")
+
+;; -------------------------- Modeline Assembly --------------------------------
-(use-package mood-line
- :config
- (mood-line-mode))
+(setq-default mode-line-format
+ '("%e" ; Error message if out of memory
+ ;; LEFT SIDE
+ " "
+ cj/modeline-major-mode
+ " "
+ cj/modeline-buffer-name
+ " "
+ cj/modeline-position
+ ;; RIGHT SIDE (using Emacs 30 built-in right-align)
+ ;; Order: leftmost to rightmost as they appear in the list
+ mode-line-format-right-align
+ cj/modeline-vc-branch
+ " "
+ cj/modeline-misc-info
+ " "))
+;; Mark all segments as risky-local-variable (required for :eval forms)
+(dolist (construct '(cj/modeline-buffer-name
+ cj/modeline-position
+ cj/modeline-vc-branch
+ cj/modeline-vc-faces
+ cj/modeline-major-mode
+ cj/modeline-misc-info))
+ (put construct 'risky-local-variable t))
(provide 'modeline-config)
;;; modeline-config.el ends here
diff --git a/modules/org-config.el b/modules/org-config.el
index 555a966b..75d4c7db 100644
--- a/modules/org-config.el
+++ b/modules/org-config.el
@@ -68,11 +68,11 @@
(set-face-attribute 'org-link nil :underline t)
(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-emphasis-markers t) ;; hide emphasis markers (org-appear shows them when editing)
(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-emphasized-text 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))
@@ -221,6 +221,15 @@
(org-superstar-configure-like-org-bullets)
(setq org-superstar-leading-bullet ?\s))
+;; -------------------------------- Org-Appear ---------------------------------
+
+(use-package org-appear
+ :hook (org-mode . org-appear-mode)
+ :custom
+ (org-appear-autoemphasis t) ;; Show * / _ when cursor is on them
+ (org-appear-autolinks t) ;; Also works for links
+ (org-appear-autosubmarkers t)) ;; And sub/superscripts
+
;; ------------------------------- Org-Checklist -------------------------------
;; needed for org-habits to reset checklists once task is complete
diff --git a/modules/org-gcal-config.el b/modules/org-gcal-config.el
index dc083efe..97e8446a 100644
--- a/modules/org-gcal-config.el
+++ b/modules/org-gcal-config.el
@@ -13,9 +13,9 @@
;; - Events are managed by Org (changes in org file push back to Google Calendar)
;; This is controlled by org-gcal-managed-newly-fetched-mode and
;; org-gcal-managed-update-existing-mode set to "org"
-;; - Initial automatic sync post Emacs startup. No auto resync'ing.
-;; (my calendar doesn't change hourly and I want fewer distractions and slowdowns).
-;; if you need it: https://github.com/kidd/org-gcal.el?tab=readme-ov-file#sync-automatically-at-regular-times
+;; - Automatic sync timer (configurable via cj/org-gcal-sync-interval-minutes)
+;; Default: 30 minutes, set to nil to disable
+;; See: https://github.com/kidd/org-gcal.el?tab=readme-ov-file#sync-automatically-at-regular-times
;; - Validates existing oath2-auto.plist file or creates it to avoid the issue mentioned here:
;; https://github.com/kidd/org-gcal.el?tab=readme-ov-file#note
;;
@@ -27,7 +27,10 @@
;; 3. Define `gcal-file' in user-constants (location of org file to hold sync'd events).
;;
;; Usage:
-;; - Manual sync: C-; g (or M-x org-gcal-sync)
+;; - Manual sync: C-; g s (or M-x org-gcal-sync)
+;; - Toggle auto-sync on/off: C-; g t
+;; - Restart auto-sync (e.g., after changing interval): C-; g r
+;; - Clear sync lock (if sync gets stuck): C-; g c
;;
;; Note:
;; This configuration creates oauth2-auto.plist on first run to prevent sync errors.
@@ -43,6 +46,17 @@
(defvar org-gcal--sync-lock))
(declare-function org-gcal-reload-client-id-secret "org-gcal")
+;; User configurable sync interval
+(defvar cj/org-gcal-sync-interval-minutes 30
+ "Interval in minutes for automatic Google Calendar sync.
+Set to nil to disable automatic syncing.
+Changes take effect after calling `cj/org-gcal-restart-auto-sync'.")
+
+;; Internal timer object
+(defvar cj/org-gcal-sync-timer nil
+ "Timer object for automatic org-gcal sync.
+Use `cj/org-gcal-start-auto-sync' and `cj/org-gcal-stop-auto-sync' to control.")
+
(defun cj/org-gcal-clear-sync-lock ()
"Clear the org-gcal sync lock.
Useful when a sync fails and leaves the lock in place, preventing future syncs."
@@ -66,6 +80,50 @@ enabling bidirectional sync so changes push back to Google Calendar."
(save-buffer))
(message "Converted %d event(s) to Org-managed" count)))
+(defun cj/org-gcal-start-auto-sync ()
+ "Start automatic Google Calendar sync timer.
+Uses the interval specified in `cj/org-gcal-sync-interval-minutes'.
+Does nothing if interval is nil or timer is already running."
+ (interactive)
+ (when (and cj/org-gcal-sync-interval-minutes
+ (not (and cj/org-gcal-sync-timer
+ (memq cj/org-gcal-sync-timer timer-list))))
+ (let ((interval-seconds (* cj/org-gcal-sync-interval-minutes 60)))
+ (setq cj/org-gcal-sync-timer
+ (run-with-timer
+ 120 ;; Initial delay: 2 minutes after startup
+ interval-seconds
+ (lambda ()
+ (condition-case err
+ (org-gcal-sync)
+ (error (message "org-gcal: Auto-sync failed: %s" err))))))
+ (message "org-gcal: Auto-sync started (every %d minutes)"
+ cj/org-gcal-sync-interval-minutes))))
+
+(defun cj/org-gcal-stop-auto-sync ()
+ "Stop automatic Google Calendar sync timer."
+ (interactive)
+ (when (and cj/org-gcal-sync-timer
+ (memq cj/org-gcal-sync-timer timer-list))
+ (cancel-timer cj/org-gcal-sync-timer)
+ (setq cj/org-gcal-sync-timer nil)
+ (message "org-gcal: Auto-sync stopped")))
+
+(defun cj/org-gcal-toggle-auto-sync ()
+ "Toggle automatic Google Calendar sync timer on/off."
+ (interactive)
+ (if (and cj/org-gcal-sync-timer
+ (memq cj/org-gcal-sync-timer timer-list))
+ (cj/org-gcal-stop-auto-sync)
+ (cj/org-gcal-start-auto-sync)))
+
+(defun cj/org-gcal-restart-auto-sync ()
+ "Restart automatic Google Calendar sync timer.
+Useful after changing `cj/org-gcal-sync-interval-minutes'."
+ (interactive)
+ (cj/org-gcal-stop-auto-sync)
+ (cj/org-gcal-start-auto-sync))
+
;; Deferred library required by org-gcal
(use-package deferred
:ensure t)
@@ -77,8 +135,6 @@ enabling bidirectional sync so changes push back to Google Calendar."
(use-package org-gcal
:vc (:url "https://github.com/cjennings/org-gcal" :rev :newest)
:defer t ;; unless idle timer is set below
- :bind (("C-; g" . org-gcal-sync)
- ("C-; G" . cj/org-gcal-clear-sync-lock))
:init
;; Retrieve credentials from authinfo.gpg BEFORE package loads
@@ -109,9 +165,8 @@ enabling bidirectional sync so changes push back to Google Calendar."
(setq org-gcal-managed-update-existing-mode "gcal") ;; GCal wins on conflicts
:config
- ;; Enable plstore passphrase caching after org-gcal loads
- (require 'plstore)
- (setq plstore-cache-passphrase-for-symmetric-encryption t)
+ ;; Plstore caching is now configured globally in auth-config.el
+ ;; to ensure it loads before org-gcal needs it
;; set org-gcal timezone based on system timezone
(setq org-gcal-local-timezone (cj/detect-system-timezone))
@@ -133,19 +188,26 @@ enabling bidirectional sync so changes push back to Google Calendar."
;; Advise org-gcal--sync-unlock which is called when sync completes
(advice-add 'org-gcal--sync-unlock :after #'cj/org-gcal-save-files-after-sync))
-;; Set up automatic initial sync on boot with error handling
-;;(run-with-idle-timer
-;; 2 nil
-;; (lambda ()
-;; (condition-case err
-;; (org-gcal-sync)
-;; (error (message "org-gcal: Initial sync failed: %s" err)))))
+;; Start automatic sync timer based on user configuration
+;; Set cj/org-gcal-sync-interval-minutes to nil to disable
+(cj/org-gcal-start-auto-sync)
+
+;; Google Calendar keymap and keybindings
+(defvar-keymap cj/gcal-map
+ :doc "Keymap for Google Calendar operations"
+ "s" #'org-gcal-sync
+ "t" #'cj/org-gcal-toggle-auto-sync
+ "r" #'cj/org-gcal-restart-auto-sync
+ "c" #'cj/org-gcal-clear-sync-lock)
+(keymap-set cj/custom-keymap "g" cj/gcal-map)
-;; which-key labels
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
- "C-; g" "gcal sync"
- "C-; G" "clear sync lock"))
+ "C-; g" "gcal menu"
+ "C-; g s" "sync"
+ "C-; g t" "toggle auto-sync"
+ "C-; g r" "restart auto-sync"
+ "C-; g c" "clear sync lock"))
(provide 'org-gcal-config)
;;; org-gcal-config.el ends here
diff --git a/modules/system-utils.el b/modules/system-utils.el
index 2b39d862..e9686777 100644
--- a/modules/system-utils.el
+++ b/modules/system-utils.el
@@ -186,13 +186,22 @@ Logs output and exit code to buffer *external-open.log*."
;;; -------------------------- Scratch Buffer Happiness -------------------------
(defvar scratch-emacs-version-and-system
- (concat ";; Emacs " emacs-version
+ (concat "# Emacs " emacs-version
" on " system-configuration ".\n"))
(defvar scratch-greet
- (concat ";; Emacs ♥ you, " user-login-name ". Happy Hacking!\n\n"))
+ (concat "# Emacs ♥ you, " user-login-name ". Happy Hacking!\n\n"))
(setopt initial-scratch-message
(concat scratch-emacs-version-and-system scratch-greet))
+;; Set scratch buffer to org-mode
+(setopt initial-major-mode 'org-mode)
+
+;; Move cursor to end of scratch buffer on startup
+(add-hook 'emacs-startup-hook
+ (lambda ()
+ (when (string= (buffer-name) "*scratch*")
+ (goto-char (point-max)))))
+
;;; --------------------------------- Dictionary --------------------------------
(use-package quick-sdcv
diff --git a/modules/transcription-config.el b/modules/transcription-config.el
new file mode 100644
index 00000000..fd2f4aaa
--- /dev/null
+++ b/modules/transcription-config.el
@@ -0,0 +1,390 @@
+;;; transcription-config.el --- Audio transcription workflow -*- lexical-binding: t; -*-
+
+;; Author: Craig Jennings <c@cjennings.net>
+;; Created: 2025-11-04
+
+;;; Commentary:
+;;
+;; Audio transcription workflow with multiple backend options.
+;;
+;; USAGE:
+;; In dired: Press `T` on an audio file to transcribe
+;; Anywhere: M-x cj/transcribe-audio
+;; View active: M-x cj/transcriptions-buffer
+;; Switch backend: C-; T b (or M-x cj/transcription-switch-backend)
+;;
+;; OUTPUT FILES:
+;; audio.m4a → audio.txt (transcript)
+;; → audio.log (process logs, conditionally kept)
+;;
+;; BACKENDS:
+;; - 'openai-api: Fast cloud transcription
+;; API key retrieved from authinfo.gpg (machine api.openai.com)
+;; - 'assemblyai: Cloud transcription with speaker diarization
+;; API key retrieved from authinfo.gpg (machine api.assemblyai.com)
+;; - 'local-whisper: Local transcription (requires whisper installed)
+;;
+;; NOTIFICATIONS:
+;; - "Transcription started on <file>"
+;; - "Transcription complete. Transcript in <file.txt>"
+;; - "Transcription errored. Logs in <file.log>"
+;;
+;; MODELINE:
+;; Shows active transcription count: ⏺2
+;; Click to view *Transcriptions* buffer
+;;
+;;; Code:
+
+(require 'dired)
+(require 'notifications)
+(require 'auth-source)
+
+;; ----------------------------- Configuration ---------------------------------
+
+(defvar cj/transcribe-backend 'assemblyai
+ "Transcription backend to use.
+- `openai-api': Fast cloud transcription via OpenAI API
+- `assemblyai': Cloud transcription with speaker diarization via AssemblyAI
+- `local-whisper': Local transcription using installed Whisper")
+
+(defvar cj/transcription-keep-log-when-done nil
+ "Whether to keep log files after successful transcription.
+If nil, log files are deleted after successful completion.
+If t, log files are always kept.
+Log files are always kept on error regardless of this setting.")
+
+(defvar cj/transcriptions-list '()
+ "List of active transcriptions.
+Each entry: (process audio-file start-time status)
+Status: running, complete, error")
+
+;; ----------------------------- Pure Functions --------------------------------
+
+(defun cj/--audio-file-p (file)
+ "Return non-nil if FILE is an audio file based on extension."
+ (when (and file (stringp file))
+ (when-let ((ext (file-name-extension file)))
+ (member (downcase ext) cj/audio-file-extensions))))
+
+(defun cj/--transcription-output-files (audio-file)
+ "Return cons cell of (TXT-FILE . LOG-FILE) for AUDIO-FILE."
+ (let ((base (file-name-sans-extension audio-file)))
+ (cons (concat base ".txt")
+ (concat base ".log"))))
+
+(defun cj/--transcription-duration (start-time)
+ "Return duration string (MM:SS) since START-TIME."
+ (let* ((elapsed (float-time (time-subtract (current-time) start-time)))
+ (minutes (floor (/ elapsed 60)))
+ (seconds (floor (mod elapsed 60))))
+ (format "%02d:%02d" minutes seconds)))
+
+(defun cj/--should-keep-log (success-p)
+ "Return non-nil if log file should be kept.
+SUCCESS-P indicates whether transcription succeeded."
+ (or (not success-p) ; Always keep on error
+ cj/transcription-keep-log-when-done))
+
+(defun cj/--transcription-script-path ()
+ "Return absolute path to transcription script based on backend."
+ (let ((script-name (pcase cj/transcribe-backend
+ ('openai-api "oai-transcribe")
+ ('assemblyai "assemblyai-transcribe")
+ ('local-whisper "local-whisper"))))
+ (expand-file-name (concat "scripts/" script-name) user-emacs-directory)))
+
+(defun cj/--get-openai-api-key ()
+ "Retrieve OpenAI API key from authinfo.gpg.
+Expects entry in authinfo.gpg:
+ machine api.openai.com login api password sk-...
+Returns the API key string, or nil if not found."
+ (when-let* ((auth-info (car (auth-source-search
+ :host "api.openai.com"
+ :require '(:secret))))
+ (secret (plist-get auth-info :secret)))
+ (if (functionp secret)
+ (funcall secret)
+ secret)))
+
+(defun cj/--get-assemblyai-api-key ()
+ "Retrieve AssemblyAI API key from authinfo.gpg.
+Expects entry in authinfo.gpg:
+ machine api.assemblyai.com login api password <key>
+Returns the API key string, or nil if not found."
+ (when-let* ((auth-info (car (auth-source-search
+ :host "api.assemblyai.com"
+ :require '(:secret))))
+ (secret (plist-get auth-info :secret)))
+ (if (functionp secret)
+ (funcall secret)
+ secret)))
+
+;; ---------------------------- Process Management -----------------------------
+
+(defun cj/--notify (title message &optional urgency)
+ "Send desktop notification and echo area message.
+TITLE and MESSAGE are strings. URGENCY is normal or critical."
+ (message "%s: %s" title message)
+ (when (and (fboundp 'notifications-notify)
+ (getenv "DISPLAY"))
+ (notifications-notify
+ :title title
+ :body message
+ :urgency (or urgency 'normal))))
+
+(defun cj/--start-transcription-process (audio-file)
+ "Start async transcription process for AUDIO-FILE.
+Returns the process object."
+ (unless (file-exists-p audio-file)
+ (user-error "Audio file does not exist: %s" audio-file))
+
+ (unless (cj/--audio-file-p audio-file)
+ (user-error "Not an audio file: %s" audio-file))
+
+ (let* ((script (cj/--transcription-script-path))
+ (outputs (cj/--transcription-output-files audio-file))
+ (txt-file (car outputs))
+ (log-file (cdr outputs))
+ (buffer-name (format " *transcribe-%s*" (file-name-nondirectory audio-file)))
+ (process-name (format "transcribe-%s" (file-name-nondirectory audio-file))))
+
+ (unless (file-executable-p script)
+ (user-error "Transcription script not found or not executable: %s" script))
+
+ ;; Create log file
+ (with-temp-file log-file
+ (insert (format "Transcription started: %s\n" (current-time-string))
+ (format "Backend: %s\n" cj/transcribe-backend)
+ (format "Audio file: %s\n" audio-file)
+ (format "Script: %s\n\n" script)))
+
+ ;; Start process with environment
+ (let* ((process-environment
+ ;; Add API key to environment based on backend
+ (pcase cj/transcribe-backend
+ ('openai-api
+ (if-let ((api-key (cj/--get-openai-api-key)))
+ (cons (format "OPENAI_API_KEY=%s" api-key)
+ process-environment)
+ (user-error "OpenAI API key not found in authinfo.gpg for host api.openai.com")))
+ ('assemblyai
+ (if-let ((api-key (cj/--get-assemblyai-api-key)))
+ (cons (format "ASSEMBLYAI_API_KEY=%s" api-key)
+ process-environment)
+ (user-error "AssemblyAI API key not found in authinfo.gpg for host api.assemblyai.com")))
+ (_ process-environment)))
+ (process (make-process
+ :name process-name
+ :buffer (get-buffer-create buffer-name)
+ :command (list script audio-file)
+ :sentinel (lambda (proc event)
+ (cj/--transcription-sentinel proc event audio-file txt-file log-file))
+ :stderr log-file)))
+
+ ;; Track transcription
+ (push (list process audio-file (current-time) 'running) cj/transcriptions-list)
+ (force-mode-line-update t)
+
+ ;; Notify user
+ (cj/--notify "Transcription"
+ (format "Started on %s" (file-name-nondirectory audio-file)))
+
+ process)))
+
+(defun cj/--transcription-sentinel (process event audio-file txt-file log-file)
+ "Sentinel for transcription PROCESS.
+EVENT is the process event string.
+AUDIO-FILE, TXT-FILE, and LOG-FILE are the associated files."
+ (let* ((success-p (and (string-match-p "finished" event)
+ (= 0 (process-exit-status process))))
+ (process-buffer (process-buffer process))
+ (entry (assq process cj/transcriptions-list)))
+
+ ;; Write process output to txt file
+ (when (and success-p (buffer-live-p process-buffer))
+ (with-current-buffer process-buffer
+ (write-region (point-min) (point-max) txt-file nil 'silent)))
+
+ ;; Append process output to log file
+ (when (buffer-live-p process-buffer)
+ (with-temp-buffer
+ (insert-file-contents log-file)
+ (goto-char (point-max))
+ (insert "\n" (format-time-string "[%Y-%m-%d %H:%M:%S] ") event "\n")
+ (insert-buffer-substring process-buffer)
+ (write-region (point-min) (point-max) log-file nil 'silent)))
+
+ ;; Update transcription status
+ (when entry
+ (setf (nth 3 entry) (if success-p 'complete 'error)))
+
+ ;; Cleanup log file if successful and configured to do so
+ (when (and success-p (not (cj/--should-keep-log t)))
+ (delete-file log-file))
+
+ ;; Kill process buffer
+ (when (buffer-live-p process-buffer)
+ (kill-buffer process-buffer))
+
+ ;; Notify user
+ (if success-p
+ (cj/--notify "Transcription"
+ (format "Complete. Transcript in %s" (file-name-nondirectory txt-file)))
+ (cj/--notify "Transcription"
+ (format "Errored. Logs in %s" (file-name-nondirectory log-file))
+ 'critical))
+
+ ;; Clean up completed transcriptions after 10 minutes
+ (run-at-time 600 nil #'cj/--cleanup-completed-transcriptions)
+
+ ;; Update modeline
+ (force-mode-line-update t)))
+
+(defun cj/--cleanup-completed-transcriptions ()
+ "Remove completed/errored transcriptions from tracking list."
+ (setq cj/transcriptions-list
+ (seq-filter (lambda (entry)
+ (eq (nth 3 entry) 'running))
+ cj/transcriptions-list))
+ (force-mode-line-update t))
+
+(defun cj/--count-active-transcriptions ()
+ "Return count of running transcriptions."
+ (length (seq-filter (lambda (entry)
+ (eq (nth 3 entry) 'running))
+ cj/transcriptions-list)))
+
+;; ----------------------------- Modeline Integration --------------------------
+
+(defun cj/--transcription-modeline-string ()
+ "Return modeline string for active transcriptions."
+ (let ((count (cj/--count-active-transcriptions)))
+ (when (> count 0)
+ (propertize (format " ⏺%d " count)
+ 'face 'warning
+ 'help-echo (format "%d active transcription%s (click to view)"
+ count (if (= count 1) "" "s"))
+ 'mouse-face 'mode-line-highlight
+ 'local-map (let ((map (make-sparse-keymap)))
+ (define-key map [mode-line mouse-1]
+ #'cj/transcriptions-buffer)
+ map)))))
+
+;; Add to mode-line-format (will be activated when module loads)
+(add-to-list 'mode-line-misc-info
+ '(:eval (cj/--transcription-modeline-string))
+ t)
+
+;; --------------------------- Interactive Commands ----------------------------
+
+;;;###autoload
+(defun cj/transcribe-audio (audio-file)
+ "Transcribe AUDIO-FILE asynchronously.
+Creates AUDIO.txt with transcript and AUDIO.log with process logs.
+Uses backend specified by `cj/transcribe-backend'."
+ (interactive (list (read-file-name "Audio file to transcribe: "
+ nil nil t nil
+ #'cj/--audio-file-p)))
+ (cj/--start-transcription-process (expand-file-name audio-file)))
+
+;;;###autoload
+(defun cj/transcribe-audio-at-point ()
+ "Transcribe audio file at point in dired."
+ (interactive)
+ (unless (derived-mode-p 'dired-mode)
+ (user-error "Not in dired-mode"))
+ (let ((file (dired-get-filename nil t)))
+ (unless file
+ (user-error "No file at point"))
+ (cj/transcribe-audio file)))
+
+;;;###autoload
+(defun cj/transcriptions-buffer ()
+ "Show buffer with active transcriptions."
+ (interactive)
+ (let ((buffer (get-buffer-create "*Transcriptions*")))
+ (with-current-buffer buffer
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert (propertize "Active Transcriptions\n" 'face 'bold)
+ (propertize (make-string 50 ?─) 'face 'shadow)
+ "\n\n")
+ (if (null cj/transcriptions-list)
+ (insert "No active transcriptions.\n")
+ (dolist (entry cj/transcriptions-list)
+ (let* ((process (nth 0 entry))
+ (audio-file (nth 1 entry))
+ (start-time (nth 2 entry))
+ (status (nth 3 entry))
+ (duration (cj/--transcription-duration start-time))
+ (status-face (pcase status
+ ('running 'warning)
+ ('complete 'success)
+ ('error 'error))))
+ (insert (propertize (format "%-10s" status) 'face status-face)
+ " "
+ (file-name-nondirectory audio-file)
+ (format " (%s)\n" duration))))))
+ (goto-char (point-min))
+ (special-mode))
+ (display-buffer buffer)))
+
+;;;###autoload
+(defun cj/transcription-kill (process)
+ "Kill transcription PROCESS."
+ (interactive
+ (list (let ((choices (mapcar (lambda (entry)
+ (cons (file-name-nondirectory (nth 1 entry))
+ (nth 0 entry)))
+ cj/transcriptions-list)))
+ (unless choices
+ (user-error "No active transcriptions"))
+ (cdr (assoc (completing-read "Kill transcription: " choices nil t)
+ choices)))))
+ (when (process-live-p process)
+ (kill-process process)
+ (message "Killed transcription process")))
+
+;;;###autoload
+(defun cj/transcription-switch-backend ()
+ "Switch transcription backend.
+Prompts with completing-read to select from available backends."
+ (interactive)
+ (let* ((backends '(("assemblyai" . assemblyai)
+ ("openai-api" . openai-api)
+ ("local-whisper" . local-whisper)))
+ (current (symbol-name cj/transcribe-backend))
+ (prompt (format "Transcription backend (current: %s): " current))
+ (choice (completing-read prompt backends nil t))
+ (new-backend (alist-get choice backends nil nil #'string=)))
+ (setq cj/transcribe-backend new-backend)
+ (message "Transcription backend: %s" choice)))
+
+;; ------------------------------- Dired Integration ---------------------------
+
+(with-eval-after-load 'dired
+ (define-key dired-mode-map (kbd "T") #'cj/transcribe-audio-at-point))
+
+;; Dirvish inherits dired-mode-map, so T works automatically
+
+;; ------------------------------- Global Keybindings --------------------------
+
+;; Transcription keymap
+(defvar-keymap cj/transcribe-map
+ :doc "Keymap for transcription operations"
+ "a" #'cj/transcribe-audio
+ "b" #'cj/transcription-switch-backend
+ "v" #'cj/transcriptions-buffer
+ "k" #'cj/transcription-kill)
+(keymap-set cj/custom-keymap "T" cj/transcribe-map)
+
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-; T" "transcription menu"
+ "C-; T a" "transcribe audio"
+ "C-; T b" "switch backend"
+ "C-; T v" "view transcriptions"
+ "C-; T k" "kill transcription"))
+
+(provide 'transcription-config)
+;;; transcription-config.el ends here
diff --git a/modules/ui-config.el b/modules/ui-config.el
index 91dbaf31..837d2169 100644
--- a/modules/ui-config.el
+++ b/modules/ui-config.el
@@ -36,11 +36,8 @@
"Opacity level for Emacs frames when `cj/enable-transparency' is non-nil.
100 = fully opaque, 0 = fully transparent.")
-(defconst cj/cursor-colors
- '((read-only . "#f06a3f") ; red – buffer is read-only
- (overwrite . "#c48702") ; gold – overwrite mode
- (normal . "#64aa0f")) ; green – insert & read/write
- "Alist mapping cursor states to their colors.")
+;; Use buffer status colors from user-constants
+(require 'user-constants)
;; ----------------------------- System UI Settings ----------------------------
@@ -104,7 +101,7 @@ When `cj/enable-transparency' is nil, reset alpha to fully opaque."
(buffer-read-only 'read-only)
(overwrite-mode 'overwrite)
(t 'normal)))
- (color (alist-get state cj/cursor-colors)))
+ (color (alist-get state cj/buffer-status-colors)))
(unless (and (string= color cj/-cursor-last-color)
(string= (buffer-name) cj/-cursor-last-buffer))
(set-cursor-color color)
diff --git a/modules/user-constants.el b/modules/user-constants.el
index bcb34bcc..2a6d0ca2 100644
--- a/modules/user-constants.el
+++ b/modules/user-constants.el
@@ -38,6 +38,23 @@ Example: (setq cj/debug-modules '(org-agenda mail))
(defvar user-mail-address "c@cjennings.net"
"The user's email address.")
+;; ---------------------------- Buffer Status Colors ---------------------------
+
+(defconst cj/buffer-status-colors
+ '((read-only . "#f06a3f") ; red – buffer is read-only
+ (overwrite . "#c48702") ; gold – overwrite mode
+ (normal . "#64aa0f")) ; green – insert & read/write
+ "Alist mapping buffer states to their colors.
+Used by cursor color, modeline, and other UI elements.")
+
+;; --------------------------- Media File Extensions ---------------------------
+
+(defvar cj/audio-file-extensions
+ '("m4a" "mp3" "wav" "flac" "ogg" "opus" "aac"
+ "aiff" "aif" "wma" "ape" "alac" "weba")
+ "File extensions recognized as audio files.
+Used by transcription module and other audio-related functionality.")
+
;; ------------------------ Directory And File Constants -----------------------
;; DIRECTORIES
diff --git a/modules/vc-config.el b/modules/vc-config.el
index b9b61c29..141f6e17 100644
--- a/modules/vc-config.el
+++ b/modules/vc-config.el
@@ -125,10 +125,11 @@ interactive selection to jump to any changed line in the buffer."
;; -------------------------------- Difftastic ---------------------------------
;; Structural diffs for better git change visualization
+;; Requires: difft binary (installed via pacman -S difftastic)
(use-package difftastic
- :demand t
- :after magit
+ :defer t
+ :commands (difftastic-magit-diff difftastic-magit-show)
:bind (:map magit-blame-read-only-mode-map
("D" . difftastic-magit-show)
("S" . difftastic-magit-show))
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el
index c714a0a6..45bab267 100644
--- a/modules/video-audio-recording.el
+++ b/modules/video-audio-recording.el
@@ -4,7 +4,7 @@
;;; Commentary:
;; Use ffmpeg to record desktop video or just audio.
;; with audio from mic and audio from default audio sink
-;; Also supports audio-only recording in Opus format.
+;; Audio recordings use M4A/AAC format for best compatibility.
;;
;; Note: video-recordings-dir and audio-recordings-dir are defined
;; (and directory created) in user-constants.el
@@ -311,16 +311,16 @@ Otherwise use the default location in `audio-recordings-dir'."
(system-device (cdr devices))
(location (expand-file-name directory))
(name (format-time-string "%Y-%m-%d-%H-%M-%S"))
- (filename (expand-file-name (concat name ".opus") location))
+ (filename (expand-file-name (concat name ".m4a") location))
(ffmpeg-command
(format (concat "ffmpeg "
"-f pulse -i %s "
"-ac 1 "
"-f pulse -i %s "
- "-ac 2 "
- "-filter_complex \"[0:a]volume=%.1f[mic];[1:a]volume=%.1f[sys];[mic][sys]amerge=inputs=2\" "
- "-c:a libopus "
- "-b:a 96k "
+ "-ac 1 "
+ "-filter_complex \"[0:a]volume=%.1f[mic];[1:a]volume=%.1f[sys];[mic][sys]amerge=inputs=2[out];[out]pan=mono|c0=0.5*c0+0.5*c1\" "
+ "-c:a aac "
+ "-b:a 64k "
"%s")
mic-device
system-device
diff --git a/modules/weather-config.el b/modules/weather-config.el
index 31fb1b70..3a30aa17 100644
--- a/modules/weather-config.el
+++ b/modules/weather-config.el
@@ -10,8 +10,15 @@
;; ----------------------------------- Wttrin ----------------------------------
+;; Load wttrin from local development directory
+(add-to-list 'load-path "/home/cjennings/code/wttrin")
+
+;; Set debug flag BEFORE loading wttrin (checked at load time)
+(setq wttrin-debug nil)
+
(use-package wttrin
- :vc (:url "https://github.com/cjennings/emacs-wttrin" :rev :newest)
+ ;; Uncomment the next line to use vc-install instead of local directory:
+ ;; :vc (:url "https://github.com/cjennings/emacs-wttrin" :rev :newest)
:defer t
:preface
;; dependency for wttrin
@@ -21,6 +28,22 @@
("M-W" . wttrin)
:custom
(wttrin-unit-system "u")
+ (wttrin-mode-line-favorite-location "New Orleans, LA")
+ (wttrin-mode-line-refresh-interval 900) ; 15 minutes
+ :init
+ ;; Explicitly autoload the mode function (needed for local dev directory)
+ (autoload 'wttrin-mode-line-mode "wttrin" "Toggle weather display in mode-line." t)
+ ;; Enable mode-line widget AFTER Emacs finishes initializing
+ ;; (url-retrieve async needs full init to work without buffer errors)
+ (if (daemonp)
+ ;; Daemon mode: wait for first client to connect
+ (add-hook 'server-after-make-frame-hook
+ (lambda () (wttrin-mode-line-mode 1))
+ t) ; append to end of hook
+ ;; Normal Emacs: wait for startup to complete
+ (add-hook 'after-init-hook
+ (lambda () (wttrin-mode-line-mode 1))
+ t)) ; append to end of hook
:config
(setq wttrin-default-locations '(
"New Orleans, LA"