diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/auth-config.el | 82 | ||||
| -rw-r--r-- | modules/config-utilities.el | 12 | ||||
| -rw-r--r-- | modules/dirvish-config.el | 17 | ||||
| -rw-r--r-- | modules/flycheck-config.el | 54 | ||||
| -rw-r--r-- | modules/modeline-config.el | 3 | ||||
| -rw-r--r-- | modules/org-gcal-config.el | 5 | ||||
| -rw-r--r-- | modules/system-utils.el | 13 | ||||
| -rw-r--r-- | modules/transcription-config.el | 100 | ||||
| -rw-r--r-- | modules/video-audio-recording.el | 12 | ||||
| -rw-r--r-- | modules/weather-config.el | 25 |
10 files changed, 244 insertions, 79 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 140d21cd..b1403539 100644 --- a/modules/modeline-config.el +++ b/modules/modeline-config.el @@ -156,7 +156,8 @@ Shows only in active window.") mode-line-format-right-align cj/modeline-vc-branch " " - cj/modeline-misc-info)) + cj/modeline-misc-info + " ")) ;; Mark all segments as risky-local-variable (required for :eval forms) (dolist (construct '(cj/modeline-buffer-name diff --git a/modules/org-gcal-config.el b/modules/org-gcal-config.el index 28cc1933..97e8446a 100644 --- a/modules/org-gcal-config.el +++ b/modules/org-gcal-config.el @@ -165,9 +165,8 @@ Useful after changing `cj/org-gcal-sync-interval-minutes'." (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)) 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 index 4c7d4943..fd2f4aaa 100644 --- a/modules/transcription-config.el +++ b/modules/transcription-config.el @@ -5,19 +5,23 @@ ;;; Commentary: ;; -;; Audio transcription workflow using OpenAI Whisper (API or local). +;; 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 (requires OPENAI_API_KEY) +;; - '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: @@ -33,12 +37,14 @@ (require 'dired) (require 'notifications) +(require 'auth-source) ;; ----------------------------- Configuration --------------------------------- -(defvar cj/transcribe-backend 'local-whisper +(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 @@ -83,9 +89,36 @@ SUCCESS-P indicates whether transcription succeeded." "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) @@ -125,14 +158,28 @@ Returns the process object." (format "Audio file: %s\n" audio-file) (format "Script: %s\n\n" script))) - ;; Start process - (let ((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))) + ;; 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) @@ -298,6 +345,21 @@ Uses backend specified by `cj/transcribe-backend'." (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 @@ -310,17 +372,19 @@ Uses backend specified by `cj/transcribe-backend'." ;; Transcription keymap (defvar-keymap cj/transcribe-map :doc "Keymap for transcription operations" - "t" #'cj/transcribe-audio - "b" #'cj/transcriptions-buffer + "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) +(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 t" "transcribe audio" - "C-; t b" "show transcriptions buffer" - "C-; t k" "kill transcription")) + "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/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" |
