-*- buffer-read-only: t -*- #+title: Emacs Configuration #+startup: overview #+DATE: 2026-03-24T10:00:00+11:00 #+DRAFT: false #+PROPERTY: header-args:emacs-lisp :results silent :tangle init.el * Early startup These settings have to exist before the first GUI frame is created, so they tangle into =early-init.el=. #+begin_src emacs-lisp :tangle early-init.el ;;; early-init.el --- generated from config.org -*- lexical-binding: t; -*- ;;; Commentary: ;; This file is generated from config.org. Do not edit it directly. ;;; Code: (dolist (parameter '((width . 140) (height . 42))) (add-to-list 'default-frame-alist parameter) (add-to-list 'initial-frame-alist parameter)) #+end_src * Bootstrapping This is the start of the main runtime entry point, which tangles into =init.el=. #+begin_src emacs-lisp ;;; init.el --- generated from config.org -*- lexical-binding: t; -*- ;;; Commentary: ;; This file is generated from config.org. Do not edit it directly. ;;; Code: (let ((minver "27.1")) (when (version< emacs-version minver) (error "Your Emacs is too old -- this config requires v%s or higher" minver))) (when (version< emacs-version "28.1") (message (concat "Your Emacs is old, and some functionality in this config will be " "disabled. Please upgrade if possible."))) #+end_src * Shared paths and system identity These definitions set up the shared paths used by the rest of the configuration, including the Org directory under =~/org/=. #+begin_src emacs-lisp (defconst *spell-check-support-enabled* nil) (defconst *is-a-windows* (memq system-type '(windows-nt ms-dos cygwin))) (defconst *is-a-linux* (eq system-type 'gnu/linux)) (defconst *is-a-mac* (eq system-type 'darwin)) (provide 'init-paths) ;; Keep custom-set-variables out of the main config. (setq custom-file (expand-file-name "custom.el" user-emacs-directory)) #+end_src * Package setup This section bootstraps packages and defines the archives the rest of the configuration relies on. #+begin_src emacs-lisp (require 'package) (setq package-archives (append '(("melpa" . "https://melpa.org/packages/")) package-archives) package-archive-priorities '(("gnu" . 10) ("nongnu" . 8) ("melpa" . 5)) package-install-upgrade-built-in t use-package-always-ensure nil) (package-initialize) (require 'use-package) #+end_src * Interface defaults This section sets the visual defaults: theme, fonts, and frame behavior. #+begin_src emacs-lisp (when (display-graphic-p) (set-frame-size (selected-frame) 140 42) (menu-bar-mode -1) (tool-bar-mode -1) (scroll-bar-mode -1) (tooltip-mode -1) (set-face-attribute 'default nil :family "JetBrains Mono" :height 140 :weight 'medium) (set-face-attribute 'fixed-pitch nil :family "JetBrains Mono" :weight 'medium) (set-face-attribute 'fixed-pitch-serif nil :family "JetBrains Mono" :weight 'medium)) (unless (display-graphic-p) (add-hook 'emacs-startup-hook (lambda () (menu-bar-mode -1)))) (setq inhibit-startup-message t inhibit-startup-screen t ring-bell-function 'ignore) (use-package modus-themes :ensure nil :no-require t :config (load-theme 'modus-vivendi t)) (line-number-mode 1) (column-number-mode 1) (show-paren-mode 1) ;; Disable all fringe indicators (setq-default indicate-empty-lines nil) (setq-default indicate-buffer-boundaries nil) (setq-default fringe-indicator-alist nil) #+end_src * Modeline #+begin_src emacs-lisp (use-package time :ensure nil :config ;; Enable 24-hour time display without load average. (setq display-time-24hr-format t display-time-day-and-date t display-time-default-load-average nil) (display-time-mode 1)) ;; Keep the theme's faces, but make the right edge alignment dynamic. (setq-default mode-line-format (list ;; Left padding " " "%e" mode-line-front-space mode-line-mule-info mode-line-client mode-line-modified mode-line-remote mode-line-frame-identification mode-line-buffer-identification " " mode-line-position '(vc-mode vc-mode) " " mode-line-modes ;; Right-align from here '(:eval (propertize " " 'display `((space :align-to (- right ,(+ 2 (string-width (format-mode-line mode-line-misc-info)))))))) mode-line-misc-info ;; Right padding " " mode-line-end-spaces)) #+end_src * Editing defaults This section covers global editing behavior and a few startup-time tuning choices. #+begin_src emacs-lisp (require 'abbrev) (set-language-environment "UTF-8") (set-default-coding-systems 'utf-8) (prefer-coding-system 'utf-8) (setq abbrev-file-name (expand-file-name "abbrev_defs" user-emacs-directory) save-abbrevs 'silently) (when (file-exists-p abbrev-file-name) (quietly-read-abbrev-file abbrev-file-name)) (defun ss/enable-prose-abbrev-mode () "Enable abbrev mode in prose buffers. We keep this mode-local so code buffers stay on their own completion rules." (abbrev-mode 1)) (dolist (hook '(text-mode-hook org-mode-hook)) (add-hook hook #'ss/enable-prose-abbrev-mode)) (setq auto-save-default nil backup-inhibited t echo-keystrokes 0.1 compilation-ask-about-save nil mouse-wheel-scroll-amount '(1 ((shift) . 1)) mouse-wheel-progressive-speed nil mouse-wheel-follow-mouse t scroll-step 1 scroll-conservatively 101 enable-recursive-minibuffers t gc-cons-threshold (* 128 1024 1024) read-process-output-max (* 4 1024 1024) process-adaptive-read-buffering nil) (fset 'yes-or-no-p 'y-or-n-p) (global-auto-revert-mode 1) (delete-selection-mode 1) (setq-default indent-tabs-mode nil fill-column 80 tab-width 2 indicate-empty-lines t sentence-end-double-space nil) #+end_src * Dired On macOS, the system =ls= does not support GNU's =--dired= flag. Keeping Dired on its built-in path avoids noisy directory listing errors when opening folders from note helpers. #+begin_src emacs-lisp (use-package dired :ensure nil :custom (dired-use-ls-dired nil)) #+end_src * Minibuffer completion Completion stays close to standard Emacs behaviour while improving the minibuffer prompts used throughout the notes workflow. Vertico provides the UI, Orderless handles flexible matching, and Marginalia adds lightweight annotations. #+begin_src emacs-lisp (use-package vertico :ensure t :pin melpa :init (vertico-mode 1)) (use-package orderless :ensure t :pin melpa :custom (completion-styles '(orderless basic)) (completion-category-defaults nil) (completion-category-overrides '((file (styles basic partial-completion))))) (use-package marginalia :ensure t :pin melpa :after vertico :init (marginalia-mode 1)) (use-package corfu :ensure t :pin gnu :init ;; Enable Corfu globally so text and Org buffers get in-buffer completion ;; popups when a CAPF provides candidates. (global-corfu-mode 1)) #+end_src * People rolodex The people workflow is a lightweight rolodex backed by the canonical =~/org/areas/people/people.org= file. Each top-level heading is a compact card with properties for lookup, completion, reports, and abbrevs. Abbrev remains the fast path for names you type all the time, while CAPF plus Corfu remains the discovery path. The machine-facing layer only reads heading text and properties; the =Context= and =TODOs= sections stay human-facing notes. The rolodex is designed around =ss/people-overview=: opening the file starts in overview mode, direct visits reset back to overview mode, and exiting card view means widening the buffer and restoring that overview. Keep =CURRENT_FOCUS= short and phrase-like so summaries and completion annotations stay readable. #+begin_src emacs-lisp (require 'seq) (require 'subr-x) (require 'marginalia nil t) (defconst ss/people-file (expand-file-name "areas/people/people.org" "~/org/") "Single source of truth for the lightweight people rolodex.") (defconst ss/people-engagement-values '("permanent" "contractor" "other") "Allowed engagement values for people cards.") (defconst ss/people-relationship-values '("direct-report" "stakeholder" "peer" "skip" "other") "Allowed relationship values for people cards.") (defvar ss/people--cache nil "Cached rolodex entries loaded from `ss/people-file'.") (defvar ss/people--cache-mtime nil "Modification time of the cached rolodex entries.") (defun ss/people--entry-name (entry) "Return the canonical name in ENTRY." (plist-get entry :name)) (defun ss/people--entry-abbrev (entry) "Return the abbrev trigger in ENTRY." (plist-get entry :abbrev)) (defun ss/people--entry-aliases (entry) "Return alias variants in ENTRY." (plist-get entry :aliases)) (defun ss/people--entry-role (entry) "Return the role in ENTRY." (plist-get entry :role)) (defun ss/people--entry-location (entry) "Return the location in ENTRY." (plist-get entry :location)) (defun ss/people--entry-engagement (entry) "Return the engagement in ENTRY." (plist-get entry :engagement)) (defun ss/people--entry-relationship (entry) "Return the relationship in ENTRY." (plist-get entry :relationship)) (defun ss/people--entry-current-focus (entry) "Return the current focus in ENTRY." (plist-get entry :current-focus)) (defun ss/people--entry-team (entry) "Return the team in ENTRY." (plist-get entry :team)) (defun ss/people-default-abbrev (name) "Suggest a short abbrev trigger for NAME." (let* ((parts (split-string (string-trim name) "[[:space:]]+" t)) (first (downcase (substring (car parts) 0 (min 2 (length (car parts)))))) (last (downcase (substring (car (last parts)) 0 1)))) (if (> (length parts) 1) (concat ";" first last) (concat ";" first)))) (defun ss/people--split-values (value) "Split VALUE on commas and trim each item." (when (and value (not (string-empty-p value))) (seq-filter (lambda (string) (not (string-empty-p string))) (mapcar #'string-trim (split-string value "," t))))) (defun ss/people--summary (entry) "Return the compact one-line summary for ENTRY." (string-join (seq-filter (lambda (string) (and string (not (string-empty-p string)))) (list (ss/people--entry-role entry) (ss/people--entry-location entry) (ss/people--entry-engagement entry) (ss/people--entry-current-focus entry))) " | ")) (defun ss/people--display (entry) "Return the compact display string for ENTRY." (let ((summary (ss/people--summary entry))) (if (string-empty-p summary) (ss/people--entry-name entry) (format "%s %s" (ss/people--entry-name entry) summary)))) (defun ss/people--property-line (key value) "Return an Org property line for KEY and VALUE." (if (and value (not (string-empty-p value))) (format ":%s: %s\n" key value) "")) (defun ss/people--require-file () "Return `ss/people-file', signaling when it is unavailable." (unless (file-exists-p ss/people-file) (user-error "People file does not exist: %s" ss/people-file)) ss/people-file) (defun ss/people-entries () "Return top-level people cards from `ss/people-file'." (let* ((file (ss/people--require-file)) (attributes (file-attributes file)) (mtime (file-attribute-modification-time attributes))) (unless (and ss/people--cache (equal mtime ss/people--cache-mtime)) (let ((entries (with-temp-buffer (insert-file-contents file) ;; Parse cards without running user hooks; otherwise the ;; rolodex's own Org hooks recurse back into this parser. (delay-mode-hooks (org-mode)) (goto-char (point-min)) (let (cards) (org-element-map (org-element-parse-buffer) 'headline (lambda (headline) (when (= 1 (org-element-property :level headline)) (goto-char (org-element-property :begin headline)) (push (list :name (org-element-property :raw-value headline) :abbrev (org-entry-get nil "ABBREV") :aliases (ss/people--split-values (org-entry-get nil "ALIASES")) :role (org-entry-get nil "ROLE") :location (org-entry-get nil "LOCATION") :engagement (org-entry-get nil "ENGAGEMENT") :relationship (org-entry-get nil "RELATIONSHIP") :current-focus (org-entry-get nil "CURRENT_FOCUS") :team (org-entry-get nil "TEAM")) cards)))) (sort cards (lambda (left right) (string< (ss/people--entry-name left) (ss/people--entry-name right)))))))) (setq ss/people--cache entries ss/people--cache-mtime mtime))) ss/people--cache)) (defun ss/people-reload () "Reload the people cache and refresh prose buffers." (interactive) (setq ss/people--cache nil ss/people--cache-mtime nil) (ss/people-refresh-buffers) (message "Reloaded people rolodex")) (defun ss/people--entry-by-name (name) "Return the people entry matching canonical NAME." (seq-find (lambda (entry) (string= name (ss/people--entry-name entry))) (ss/people-entries))) (defun ss/people--search-keys (entry) "Return canonical and alias search keys for ENTRY." (cons (ss/people--entry-name entry) (ss/people--entry-aliases entry))) (defun ss/people--match-p (query entry) "Return non-nil when QUERY matches ENTRY name or aliases." (let* ((parts (split-string (downcase (string-trim query)) "[[:space:]]+" t)) (keys (mapcar #'downcase (ss/people--search-keys entry)))) (seq-every-p (lambda (part) (seq-some (lambda (key) (string-match-p (regexp-quote part) key)) keys)) parts))) (defun ss/people--matching-entries (query) "Return entries whose canonical name or aliases match QUERY." (let ((entries (ss/people-entries))) (if (string-empty-p (string-trim query)) entries (seq-filter (lambda (entry) (ss/people--match-p query entry)) entries)))) (defun ss/people--completion-table (string pred action) "Complete canonical people names while matching aliases via STRING." (if (eq action 'metadata) '(metadata (category . ss-person)) (complete-with-action action (mapcar #'ss/people--entry-name (ss/people--matching-entries string)) string pred))) (defun ss/people-marginalia-annotator (candidate) "Return a Marginalia annotation for person CANDIDATE." (when-let ((entry (ss/people--entry-by-name candidate))) (concat " " (ss/people--summary entry)))) (defun ss/people-select-entry (&optional prompt) "Select a person entry using PROMPT." (let ((completion-extra-properties '(:annotation-function ss/people-marginalia-annotator))) (ss/people--entry-by-name (completing-read (or prompt "Person: ") #'ss/people--completion-table nil t)))) (defun ss/people-overview () "Open `ss/people-file' in overview mode, widening first when needed." (interactive) (unless (and buffer-file-name (string= (file-truename buffer-file-name) (file-truename ss/people-file))) (find-file (ss/people--require-file))) (widen) (goto-char (point-min)) (org-overview) (org-cycle-hide-drawers 'all)) (defun ss/people-open () "Open the people rolodex by delegating to `ss/people-overview'." (interactive) (ss/people-overview)) (defun ss/people--track-buffer () "Refresh rolodex caches when `ss/people-file' is saved." (when (and buffer-file-name (string= (file-truename buffer-file-name) (file-truename ss/people-file))) (add-hook 'after-save-hook #'ss/people-reload nil t))) (defun ss/people--source-buffer-p () "Return non-nil when the current buffer visits `ss/people-file'." (and buffer-file-name (string= (file-truename buffer-file-name) (file-truename ss/people-file)))) (defun ss/people--open-entry (entry) "Open the people rolodex file, then narrow to ENTRY for card view." (find-file (ss/people--require-file)) (widen) (let ((position (org-find-exact-headline-in-buffer (ss/people--entry-name entry)))) (unless position (user-error "No people card for %s" (ss/people--entry-name entry))) (goto-char position)) (org-narrow-to-subtree) (org-fold-show-subtree) (org-show-entry) (goto-char (point-min))) (defun ss/people-find () "Find a person and open that card." (interactive) (ss/people--open-entry (or (ss/people-select-entry "Find person: ") (user-error "No person selected")))) (defun ss/people-insert-name () "Insert a canonical person name at point." (interactive) (let ((entry (or (ss/people-select-entry "Insert person name: ") (user-error "No person selected")))) (insert (ss/people--entry-name entry)))) (defun ss/people-insert-summary () "Insert a compact person summary at point." (interactive) (let ((entry (or (ss/people-select-entry "Insert person summary: ") (user-error "No person selected")))) (insert (ss/people--display entry)))) (defun ss/people--report-buffer (title group-fn) "Render a grouped rolodex report titled TITLE using GROUP-FN." (let ((groups (sort (seq-group-by (lambda (entry) (let ((value (funcall group-fn entry))) (if (string-empty-p (or value "")) "(none)" value))) (ss/people-entries)) (lambda (left right) (string< (car left) (car right)))))) (with-current-buffer (get-buffer-create "*People Report*") (let ((inhibit-read-only t)) (erase-buffer) (org-mode) (insert "#+title: " title "\n\n") (dolist (group groups) (insert "* " (car group) "\n") (dolist (entry (sort (copy-sequence (cdr group)) (lambda (left right) (string< (ss/people--entry-name left) (ss/people--entry-name right))))) (insert "- " (ss/people--display entry) "\n"))) (goto-char (point-min)) (read-only-mode 1) (view-mode 1)) (pop-to-buffer (current-buffer))))) (defun ss/people-report-by-relationship () "Show people grouped by relationship." (interactive) (ss/people--report-buffer "People by relationship" #'ss/people--entry-relationship)) (defun ss/people-report-by-engagement () "Show people grouped by engagement." (interactive) (ss/people--report-buffer "People by engagement" #'ss/people--entry-engagement)) (defun ss/people-report-by-role () "Show people grouped by role." (interactive) (ss/people--report-buffer "People by role" #'ss/people--entry-role)) (defun ss/people-report-by-location () "Show people grouped by location." (interactive) (ss/people--report-buffer "People by location" #'ss/people--entry-location)) (defun ss/people-read-string (prompt &optional default) "Read PROMPT and trim the result." (string-trim (read-string prompt nil nil default))) (defun ss/people-read-required-string (prompt &optional default) "Read PROMPT and require a non-empty result." (let ((value (ss/people-read-string prompt default))) (if (string-empty-p value) (user-error "%s is required" (string-remove-suffix ": " prompt)) value))) (defun ss/people-read-optional-string (prompt) "Read PROMPT and return nil when the answer is empty." (let ((value (ss/people-read-string prompt))) (unless (string-empty-p value) value))) (defun ss/people-add () "Add a new compact person card to `ss/people-file'." (interactive) (let* ((name (ss/people-read-required-string "Full name: ")) (abbrev (ss/people-read-string "Abbrev: " (ss/people-default-abbrev name))) (aliases (ss/people-read-string "Aliases (comma-separated, optional): ")) (role (ss/people-read-required-string "Role: ")) (location (ss/people-read-required-string "Location: ")) (engagement (completing-read "Engagement: " ss/people-engagement-values nil t nil nil "permanent")) (relationship (completing-read "Relationship: " ss/people-relationship-values nil t)) (current-focus (ss/people-read-required-string "Current focus: ")) (team (ss/people-read-optional-string "Team (optional): "))) (when (ss/people--entry-by-name name) (user-error "A person card for %s already exists" name)) (when (string-empty-p abbrev) (setq abbrev (ss/people-default-abbrev name))) (find-file (ss/people--require-file)) (widen) (goto-char (point-max)) (unless (bolp) (insert "\n")) (unless (looking-back "\n\n" nil) (insert "\n")) (insert "* " name "\n" ":PROPERTIES:\n" (ss/people--property-line "ABBREV" abbrev) (ss/people--property-line "ALIASES" aliases) (ss/people--property-line "ROLE" role) (ss/people--property-line "LOCATION" location) (ss/people--property-line "ENGAGEMENT" engagement) (ss/people--property-line "RELATIONSHIP" relationship) (ss/people--property-line "CURRENT_FOCUS" current-focus) (ss/people--property-line "TEAM" team) ":END:\n\n" "** Context\n\n" "** TODOs\n") (save-buffer) (ss/people-reload) (ss/people--open-entry (ss/people--entry-by-name name)))) (defun ss/people--clear-installed-abbrevs () "Remove people-specific abbrevs from the current local table." (mapatoms (lambda (symbol) (when (abbrev-get symbol :ss/people) (define-abbrev local-abbrev-table (symbol-name symbol) nil))) local-abbrev-table)) (defun ss/people-install-abbrevs () "Install people abbrevs into the current buffer." (unless (ss/people--source-buffer-p) (setq-local local-abbrev-table (copy-abbrev-table local-abbrev-table)) (ss/people--clear-installed-abbrevs) (dolist (entry (ss/people-entries)) (let* ((name (ss/people--entry-name entry)) (abbrev (ss/people--entry-abbrev entry)) (abbrev-name (if (or (null abbrev) (string-empty-p abbrev)) (ss/people-default-abbrev name) abbrev))) (define-abbrev local-abbrev-table abbrev-name name) (when-let ((abbrev-symbol (abbrev-symbol abbrev-name local-abbrev-table))) (abbrev-put abbrev-symbol :ss/people t)))))) (defun ss/people-refresh-buffers () "Refresh people abbrevs in every prose buffer." (dolist (buffer (buffer-list)) (with-current-buffer buffer (when (and (bound-and-true-p abbrev-mode) (derived-mode-p 'text-mode 'org-mode)) (ss/people-install-abbrevs))))) (defun ss/people-capf () "Return canonical people completions at a word boundary." (let ((end (point))) (save-excursion (skip-syntax-backward "w_") (let ((beg (point))) (when (< beg end) ;; Verify this in real Org/text writing buffers, not just by inspection. (let ((annotation (lambda (candidate) (when-let ((entry (ss/people--entry-by-name candidate))) (concat " " (ss/people--summary entry))))) (docsig (lambda (candidate) (when-let ((entry (ss/people--entry-by-name candidate))) (ss/people--summary entry))))) (list beg end #'ss/people--completion-table :exclusive 'no :annotation-function annotation :company-docsig docsig))))))) (defun ss/enable-people-capf () "Add `ss/people-capf' once in prose buffers." (unless (or (ss/people--source-buffer-p) (memq #'ss/people-capf completion-at-point-functions)) (add-hook 'completion-at-point-functions #'ss/people-capf nil t))) (defun ss/people--maybe-overview-buffer () "Reset the people rolodex buffer to overview when visiting it directly." (when (and buffer-file-name (string= (file-truename buffer-file-name) (file-truename ss/people-file))) (widen) (goto-char (point-min)) (org-overview) (org-cycle-hide-drawers 'all))) (dolist (hook '(text-mode-hook org-mode-hook)) (add-hook hook #'ss/enable-people-capf) (add-hook hook #'ss/people-install-abbrevs)) (add-hook 'find-file-hook #'ss/people--track-buffer) (add-hook 'find-file-hook #'ss/people--maybe-overview-buffer) #+end_src * Notes workflow The note-taking system remains deliberately small. Fast operational capture goes into =~/org/journal.org=, while longer-lived notes use Denote inside the same root directory and rely on links for relationships. ** Org foundations The Org setup establishes the shared directories used by the workflow and provides helpers that open existing notes at point of use. Agenda views stay focused on the journal plus PARA notes, so quick operational tasks and durable project, area, and resource files can surface TODOs without pulling in archived notes. A curated =moc.org= in the Org root acts as the startup landing page and quick navigation surface. The config assumes that file already exists and opens it directly during startup rather than creating it on demand. #+begin_src emacs-lisp (use-package org :ensure nil :functions (denote-keywords-prompt) :defines (denote-directory denote-use-directory denote-use-keywords) :preface (defconst ss/org-directory (expand-file-name "~/org/") "Root directory for Org files.") (defconst ss/journal-file (expand-file-name "journal.org" ss/org-directory) "Single-file work journal for operational capture.") (defconst ss/org-projects-directory (expand-file-name "projects/" ss/org-directory) "Directory for project notes.") (defconst ss/org-areas-directory (expand-file-name "areas/" ss/org-directory) "Directory for area notes.") (defconst ss/org-resources-directory (expand-file-name "resources/" ss/org-directory) "Directory for resource notes.") (defconst ss/org-archives-directory (expand-file-name "archives/" ss/org-directory) "Directory for archived notes.") (defconst ss/moc-file (expand-file-name "moc.org" ss/org-directory) "Central MOC note.") (defconst ss/journal-section-headings '("Tasks" "Notes" "Meetings") "Per-day section headings maintained under each journal datetree entry.") (defconst ss/org-agenda-directories (list ss/org-projects-directory ss/org-areas-directory ss/org-resources-directory) "Directories whose Org files feed the agenda.") (defun ss/require-existing-directory (directory) "Return DIRECTORY, signaling when it does not exist." (unless (file-directory-p directory) (user-error "Directory does not exist: %s" directory)) directory) (defun ss/require-existing-file (file) "Return FILE, signaling when it does not exist." (unless (file-exists-p file) (user-error "File does not exist: %s" file)) file) (defun ss/denote-capture-in-directory (directory &optional keywords &rest prompts) "Start a Denote Org capture in DIRECTORY with KEYWORDS and PROMPTS. If PROMPTS is empty, rely on `denote-prompts'." (let* ((prompt-for-keywords (memq :keywords prompts)) (directory (ss/require-existing-directory directory)) (denote-directory directory) (denote-use-directory (unless (memq :subdirectory prompts) directory)) (denote-use-keywords (if prompt-for-keywords (delete-dups (append keywords (denote-keywords-prompt))) keywords))) (if prompts (denote-org-capture-with-prompts (memq :title prompts) nil (memq :subdirectory prompts) (memq :date prompts) (memq :template prompts)) (denote-org-capture)))) (defun ss/journal-capture-time () "Return the effective timestamp for the current journal capture." (or org-overriding-default-time (org-capture-get :default-time) (current-time))) (defun ss/journal-calendar-date (&optional time) "Return TIME as a Gregorian date list for datetree helpers." (calendar-gregorian-from-absolute (time-to-days (or time (current-time))))) (defun ss/journal-year-heading (&optional time) "Return the journal year heading text for TIME." (format-time-string "%Y" (or time (current-time)))) (defun ss/journal-day-heading (&optional time) "Return the journal day heading text for TIME." (format-time-string "%Y-%m-%d %A" (or time (current-time)))) (defun ss/journal-find-or-create-heading (level heading) "Move to HEADING at LEVEL, creating it when missing." (goto-char (point-min)) (if (re-search-forward (format "^%s %s$" (make-string level ?*) (regexp-quote heading)) nil t) (goto-char (match-beginning 0)) (goto-char (point-max)) (unless (bolp) (insert "\n")) (insert (make-string level ?*) " " heading "\n") (forward-line -1)) (org-back-to-heading t)) (defun ss/journal-goto-date (&optional time create) "Move to TIME's journal date heading. When CREATE is non-nil, create the datetree entry when missing." (goto-char (point-min)) (if create (let ((year-heading (ss/journal-year-heading time)) (day-heading (ss/journal-day-heading time))) (ss/journal-find-or-create-heading 1 year-heading) (save-restriction (org-narrow-to-subtree) (ss/journal-find-or-create-heading 2 day-heading)) t) (when (re-search-forward (format "^\\*\\* %s$" (regexp-quote (ss/journal-day-heading (or time (current-time))))) nil t) (goto-char (match-beginning 0)) t))) (defun ss/journal-ensure-day-sections () "Ensure the standard section headings exist under the current journal day." (org-back-to-heading t) (let ((section-level (1+ (org-outline-level)))) (save-excursion (save-restriction (org-narrow-to-subtree) (dolist (section ss/journal-section-headings) (goto-char (point-min)) (unless (org-find-exact-headline-in-buffer section) (goto-char (point-max)) (unless (bolp) (insert "\n")) (insert (make-string section-level ?*) " " section "\n"))))))) (defun ss/journal-goto-section (section &optional time) "Move to SECTION beneath TIME's journal date, creating structure as needed." (unless (member section ss/journal-section-headings) (user-error "Unknown journal section: %s" section)) (ss/journal-goto-date time 'create) (ss/journal-ensure-day-sections) (let ((section-level (1+ (org-outline-level))) position) (save-restriction (org-narrow-to-subtree) (goto-char (point-min)) (when (re-search-forward (format "^%s %s$" (make-string section-level ?*) (regexp-quote section)) nil t) (setq position (match-beginning 0)))) (unless position (user-error "Journal section not found: %s" section)) (goto-char position) (org-back-to-heading t))) (defun ss/journal-capture-target (section) "Select SECTION under today's journal datetree entry for capture." (set-buffer (find-file-noselect (ss/require-existing-file ss/journal-file))) (widen) (ss/journal-goto-section section (ss/journal-capture-time))) (defun ss/journal-meeting-heading () "Return the default heading for a journal meeting entry." (format-time-string "%H:%M " (ss/journal-capture-time))) (defun ss/refresh-org-agenda-files (&rest _) "Refresh `org-agenda-files' from the journal and PARA directories. Ignore any arguments passed by advice wrappers." (require 'org-agenda) (setq org-agenda-files (sort (delete-dups (append (list (ss/require-existing-file ss/journal-file)) (apply #'append (mapcar (lambda (directory) (directory-files-recursively (ss/require-existing-directory directory) "\\.org\\'")) ss/org-agenda-directories)))) #'string<))) (defun ss/open-journal () "Open `ss/journal-file', moving to today's entry when it exists." (interactive) (find-file (ss/require-existing-file ss/journal-file)) (widen) (unless (ss/journal-goto-date) (goto-char (point-max)))) (defun ss/open-moc () "Open the central MOC note." (interactive) (find-file (ss/require-existing-file ss/moc-file))) (defun ss/open-agenda () "Refresh agenda files and invoke `org-agenda'." (interactive) (call-interactively #'org-agenda)) :init (add-hook 'emacs-startup-hook (lambda () (find-file (ss/require-existing-file ss/moc-file)))) :bind (("C-c a" . ss/open-agenda) ("C-c c" . org-capture) ("C-c n M" . ss/open-moc) ("C-c n E" . ss/people-report-by-engagement) ("C-c n f" . ss/people-find) ("C-c n i" . ss/people-insert-name) ("C-c n I" . ss/people-insert-summary) ("C-c n L" . ss/people-report-by-location) ("C-c n d" . ss/open-journal) ("C-c n o" . ss/people-overview) ("C-c n O" . ss/people-report-by-role) ("C-c n p" . ss/people-open) ("C-c n P" . ss/people-add) ("C-c n R" . ss/people-report-by-relationship)) :config (setq org-directory ss/org-directory org-hide-emphasis-markers t org-agenda-search-headline-for-time t) (add-hook 'org-mode-hook (lambda () (setq-local org-hide-emphasis-markers t) (font-lock-flush) (font-lock-ensure))) (advice-add 'org-agenda :before #'ss/refresh-org-agenda-files)) #+end_src ** Capture entry points Fast operational capture goes to =~/org/journal.org= using a datetree with per-day =Tasks=, =Notes=, and =Meetings= headings. Denote capture uses Denote's own Org integration so note identity, metadata, and directories stay under Denote's control rather than custom code. The convenience templates keep the familiar entry points, but only project capture injects a structural keyword by default. The people rolodex lives outside =org-capture=: adding a person uses the dedicated =ss/people-add= command so =~/org/areas/people/people.org= stays a compact, structured card file rather than turning into another capture target. #+begin_src emacs-lisp (use-package org-capture :ensure nil :after (org denote) :config (setq org-capture-templates `(("j" "Journal") ("jt" "Task" entry (function (lambda () (ss/journal-capture-target "Tasks"))) "* TODO %?") ("jn" "Note" entry (function (lambda () (ss/journal-capture-target "Notes"))) "* %?") ("jm" "Meeting" entry (function (lambda () (ss/journal-capture-target "Meetings"))) "* %(ss/journal-meeting-heading)%?\n<%<%Y-%m-%d>>") ("n" "Denote") ("nn" "Generic" plain (file denote-last-path) (function (lambda () (denote-org-capture-with-prompts :title :keywords :subdirectory))) :no-save t :immediate-finish nil :kill-buffer t :jump-to-captured t) ("np" "Project" plain (file denote-last-path) (function (lambda () (ss/denote-capture-in-directory ss/org-projects-directory '("project") :title :keywords :subdirectory))) :no-save t :immediate-finish nil :kill-buffer t :jump-to-captured t) ("na" "Area" plain (file denote-last-path) (function (lambda () (ss/denote-capture-in-directory ss/org-areas-directory nil :title :keywords :subdirectory))) :no-save t :immediate-finish nil :kill-buffer t :jump-to-captured t) ("nr" "Resource" plain (file denote-last-path) (function (lambda () (ss/denote-capture-in-directory ss/org-resources-directory nil :title :keywords :subdirectory))) :no-save t :immediate-finish nil :kill-buffer t :jump-to-captured t)))) #+end_src ** Denote Denote manages the durable notes. Folder placement carries most of the structural meaning, while Denote handles naming, metadata, linking, and retrieval. Built-in keywords stay intentionally sparse, with =project= retained because project note titles are often ambiguous outside their folder. #+begin_src emacs-lisp (use-package denote :ensure t :after org :bind (("C-c n n" . denote-open-or-create) ("C-c n l" . denote-link)) :config (setq denote-directory ss/org-directory denote-known-keywords '("project") denote-prompts '(title keywords) denote-org-capture-specifiers "%?") (denote-rename-buffer-mode 1)) #+end_src ** Automatic git commits The notes tree can enable =git-auto-commit-mode= through a =.dir-locals.el= at =~/org/=. The Emacs config keeps the package available and sets the machine-specific shell command chaining based on the active shell, while the repo-local behavior lives with the notes tree. #+begin_src emacs-lisp (use-package git-auto-commit-mode :ensure t :pin melpa :commands (git-auto-commit-mode) :init (setq gac-shell-and (if (string-match-p "fish\\'" shell-file-name) " ; and " " && "))) #+end_src * Gptel workflow LLM chat remains a small workflow tool inside Emacs. GitHub Copilot authentication is handled on demand by gptel, so there is no token plumbing in this file. #+begin_src emacs-lisp (use-package gptel :ensure t :init (setq gptel-default-mode 'org-mode gptel-model 'gpt-4o gptel-backend (gptel-make-gh-copilot "Copilot")) :bind (("C-c n g" . gptel) ("C-c n s" . gptel-send) ("C-c n r" . gptel-rewrite) ("C-c n a" . gptel-add))) #+end_src * Generated file footers The closing blocks just finish the generated startup files cleanly. #+begin_src emacs-lisp (when (file-exists-p custom-file) (load custom-file nil 'nomessage)) ;;; init.el ends here #+end_src #+begin_src emacs-lisp :tangle early-init.el ;;; early-init.el ends here #+end_src