summaryrefslogtreecommitdiff
path: root/config.org
diff options
context:
space:
mode:
authorSzymon Szukalski <szymon@szymonszukalski.com>2026-04-09 10:53:27 +1000
committerSzymon Szukalski <szymon@szymonszukalski.com>2026-04-09 10:53:27 +1000
commitbc75732b9d37b77945a977ee9f7892cf6efc79c3 (patch)
tree4d9273ccc12c29eccc44fdc12372bea047414353 /config.org
parent12a5b1464bb919ba23f2aa6c22d44de81e382151 (diff)
Refactor Emacs config into modules
Diffstat (limited to 'config.org')
-rw-r--r--config.org1211
1 files changed, 0 insertions, 1211 deletions
diff --git a/config.org b/config.org
deleted file mode 100644
index b0081c1..0000000
--- a/config.org
+++ /dev/null
@@ -1,1211 +0,0 @@
--*- 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
- calendar-latitude -37.7667
- calendar-longitude 145.0
- calendar-location-name "Melbourne, VIC")
- (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 CRM
-
-The people workflow is a CRM 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
-CRM stays individual-focused: =TEAM= captures the current working team and
-=MANAGER= captures the formal organisational manager, but organisational
-structure lives in separate notes rather than in the CRM itself.
-
-The CRM is designed around =ss/crm-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.
-Person cards use a flat property model in this order: =ROLE=, =TEAM=,
-=MANAGER=, =ENGAGEMENT=, =SUPPLIER=, =LOCATION=, and =CURRENT_FOCUS=.
-=ENGAGEMENT= and =SUPPLIER= both use fixed lookup lists in =ss/crm-add= so
-reports stay consistent. =ABBREV= and =ALIASES= remain optional helpers for
-lookup and insertion.
-
-#+begin_src emacs-lisp
- (require 'seq)
- (require 'subr-x)
- (require 'marginalia nil t)
-
- (defconst ss/crm-file
- (expand-file-name "areas/people/people.org" "~/org/")
- "Single source of truth for the people CRM.")
-
- (defconst ss/crm-engagement-options
- '("Perm" "SOW" "SOW Fixed Outcome" "NCS India")
- "Canonical engagement values for people cards.")
-
- (defconst ss/crm-supplier-options
- '("Accenture Song"
- "INFOSYS TECHNOLOGIES LIMITED"
- "MAKK Integrations Pty Ltd"
- "NCSI Technologies India Private Limited"
- "TECH MAHINDRA LTD")
- "Canonical supplier values for people cards.")
-
- (defvar ss/crm--cache nil
- "Cached CRM entries loaded from `ss/crm-file'.")
-
- (defvar ss/crm--cache-mtime nil
- "Modification time of the cached CRM entries.")
-
- (defun ss/crm--entry-name (entry)
- "Return the canonical name in ENTRY."
- (plist-get entry :name))
-
- (defun ss/crm--entry-abbrev (entry)
- "Return the abbrev trigger in ENTRY."
- (plist-get entry :abbrev))
-
- (defun ss/crm--entry-aliases (entry)
- "Return alias variants in ENTRY."
- (plist-get entry :aliases))
-
- (defun ss/crm--entry-role (entry)
- "Return the role in ENTRY."
- (plist-get entry :role))
-
- (defun ss/crm--entry-location (entry)
- "Return the location in ENTRY."
- (plist-get entry :location))
-
- (defun ss/crm--entry-engagement (entry)
- "Return the engagement in ENTRY."
- (plist-get entry :engagement))
-
- (defun ss/crm--entry-supplier (entry)
- "Return the supplier in ENTRY."
- (plist-get entry :supplier))
-
- (defun ss/crm--entry-manager (entry)
- "Return the manager in ENTRY."
- (plist-get entry :manager))
-
- (defun ss/crm--entry-current-focus (entry)
- "Return the current focus in ENTRY."
- (plist-get entry :current-focus))
-
- (defun ss/crm--entry-team (entry)
- "Return the team in ENTRY."
- (plist-get entry :team))
-
- (defun ss/crm-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/crm--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/crm--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/crm--entry-role entry)
- (ss/crm--entry-team entry)
- (ss/crm--entry-engagement entry)
- (ss/crm--entry-current-focus entry)))
- " | "))
-
- (defun ss/crm--display (entry)
- "Return the compact display string for ENTRY."
- (let ((summary (ss/crm--summary entry)))
- (if (string-empty-p summary)
- (ss/crm--entry-name entry)
- (format "%s %s" (ss/crm--entry-name entry) summary))))
-
- (defun ss/crm--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/crm--require-file ()
- "Return `ss/crm-file', signaling when it is unavailable."
- (unless (file-exists-p ss/crm-file)
- (user-error "People file does not exist: %s" ss/crm-file))
- ss/crm-file)
-
- (defun ss/crm-entries ()
- "Return top-level people cards from `ss/crm-file'."
- (let* ((file (ss/crm--require-file))
- (attributes (file-attributes file))
- (mtime (file-attribute-modification-time attributes)))
- (unless (and ss/crm--cache
- (equal mtime ss/crm--cache-mtime))
- (let ((entries
- (with-temp-buffer
- (insert-file-contents file)
- ;; Parse cards without running user hooks; otherwise the
- ;; CRM'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/crm--split-values
- (org-entry-get nil "ALIASES"))
- :role (org-entry-get nil "ROLE")
- :team (org-entry-get nil "TEAM")
- :manager (org-entry-get nil "MANAGER")
- :engagement (org-entry-get nil "ENGAGEMENT")
- :supplier (org-entry-get nil "SUPPLIER")
- :location (org-entry-get nil "LOCATION")
- :current-focus (org-entry-get nil "CURRENT_FOCUS")
- )
- cards))))
- (sort cards
- (lambda (left right)
- (string< (ss/crm--entry-name left)
- (ss/crm--entry-name right))))))))
- (setq ss/crm--cache entries
- ss/crm--cache-mtime mtime)))
- ss/crm--cache))
-
- (defun ss/crm-reload ()
- "Reload the people cache and refresh prose buffers."
- (interactive)
- (setq ss/crm--cache nil
- ss/crm--cache-mtime nil)
- (ss/crm-refresh-buffers)
- (message "Reloaded people CRM"))
-
- (defun ss/crm--entry-by-name (name)
- "Return the people entry matching canonical NAME."
- (seq-find
- (lambda (entry)
- (string= name (ss/crm--entry-name entry)))
- (ss/crm-entries)))
-
- (defun ss/crm--search-keys (entry)
- "Return canonical and alias search keys for ENTRY."
- (cons (ss/crm--entry-name entry)
- (ss/crm--entry-aliases entry)))
-
- (defun ss/crm--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/crm--search-keys entry))))
- (seq-every-p
- (lambda (part)
- (seq-some (lambda (key)
- (string-match-p (regexp-quote part) key))
- keys))
- parts)))
-
- (defun ss/crm--matching-entries (query)
- "Return entries whose canonical name or aliases match QUERY."
- (let ((entries (ss/crm-entries)))
- (if (string-empty-p (string-trim query))
- entries
- (seq-filter (lambda (entry)
- (ss/crm--match-p query entry))
- entries))))
-
- (defun ss/crm--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/crm--entry-name (ss/crm--matching-entries string))
- string
- pred)))
-
- (defun ss/crm-marginalia-annotator (candidate)
- "Return a Marginalia annotation for person CANDIDATE."
- (when-let ((entry (ss/crm--entry-by-name candidate)))
- (concat " " (ss/crm--summary entry))))
-
- (defun ss/crm-select-entry (&optional prompt)
- "Select a person entry using PROMPT."
- (let ((completion-extra-properties '(:annotation-function ss/crm-marginalia-annotator)))
- (ss/crm--entry-by-name
- (completing-read (or prompt "Person: ")
- #'ss/crm--completion-table
- nil
- t))))
-
- (defun ss/crm-overview ()
- "Open `ss/crm-file' in overview mode, widening first when needed."
- (interactive)
- (unless (and buffer-file-name
- (string= (file-truename buffer-file-name)
- (file-truename ss/crm-file)))
- (find-file (ss/crm--require-file)))
- (widen)
- (goto-char (point-min))
- (org-overview)
- (org-cycle-hide-drawers 'all))
-
- (defun ss/crm-open ()
- "Open the people CRM by delegating to `ss/crm-overview'."
- (interactive)
- (ss/crm-overview))
-
- (defun ss/crm--track-buffer ()
- "Refresh CRM caches when `ss/crm-file' is saved."
- (when (and buffer-file-name
- (string= (file-truename buffer-file-name)
- (file-truename ss/crm-file)))
- (add-hook 'after-save-hook #'ss/crm-reload nil t)))
-
- (defun ss/crm--source-buffer-p ()
- "Return non-nil when the current buffer visits `ss/crm-file'."
- (and buffer-file-name
- (string= (file-truename buffer-file-name)
- (file-truename ss/crm-file))))
-
- (defun ss/crm--open-entry (entry)
- "Open the people CRM file, then narrow to ENTRY for card view."
- (find-file (ss/crm--require-file))
- (widen)
- (let ((position (org-find-exact-headline-in-buffer
- (ss/crm--entry-name entry))))
- (unless position
- (user-error "No people card for %s" (ss/crm--entry-name entry)))
- (goto-char position))
- (org-narrow-to-subtree)
- (org-fold-show-subtree)
- (org-show-entry)
- (goto-char (point-min)))
-
- (defun ss/crm-find ()
- "Find a person and open that card."
- (interactive)
- (ss/crm--open-entry
- (or (ss/crm-select-entry "Find person: ")
- (user-error "No person selected"))))
-
- (defun ss/crm-insert-name ()
- "Insert a canonical person name at point."
- (interactive)
- (let ((entry (or (ss/crm-select-entry "Insert person name: ")
- (user-error "No person selected"))))
- (insert (ss/crm--entry-name entry))))
-
- (defun ss/crm-insert-summary ()
- "Insert a compact person summary at point."
- (interactive)
- (let ((entry (or (ss/crm-select-entry "Insert person summary: ")
- (user-error "No person selected"))))
- (insert (ss/crm--display entry))))
-
- (defun ss/crm--report-buffer (title group-fn)
- "Render a grouped CRM 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/crm-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/crm--entry-name left)
- (ss/crm--entry-name right)))))
- (insert "- " (ss/crm--display entry) "\n")))
- (goto-char (point-min))
- (read-only-mode 1)
- (view-mode 1))
- (pop-to-buffer (current-buffer)))))
-
- (defun ss/crm-report-by-team ()
- "Show people grouped by team."
- (interactive)
- (ss/crm--report-buffer
- "People by team"
- #'ss/crm--entry-team))
-
- (defun ss/crm-report-by-manager ()
- "Show people grouped by manager."
- (interactive)
- (ss/crm--report-buffer
- "People by manager"
- #'ss/crm--entry-manager))
-
- (defun ss/crm-report-by-engagement ()
- "Show people grouped by engagement."
- (interactive)
- (ss/crm--report-buffer
- "People by engagement"
- #'ss/crm--entry-engagement))
-
- (defun ss/crm-report-by-supplier ()
- "Show non-empty suppliers grouped by supplier."
- (interactive)
- (let ((groups
- (sort (seq-group-by
- #'ss/crm--entry-supplier
- (seq-filter
- (lambda (entry)
- (not (string-empty-p (or (ss/crm--entry-supplier entry) ""))))
- (ss/crm-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: People by supplier\n\n")
- (dolist (group groups)
- (insert "* " (car group) "\n")
- (dolist (entry (sort (copy-sequence (cdr group))
- (lambda (left right)
- (string< (ss/crm--entry-name left)
- (ss/crm--entry-name right)))))
- (insert "- " (ss/crm--display entry) "\n")))
- (goto-char (point-min))
- (read-only-mode 1)
- (view-mode 1))
- (pop-to-buffer (current-buffer)))))
-
- (defun ss/crm-report-by-role ()
- "Show people grouped by role."
- (interactive)
- (ss/crm--report-buffer
- "People by role"
- #'ss/crm--entry-role))
-
- (defun ss/crm-report-by-location ()
- "Show people grouped by location."
- (interactive)
- (ss/crm--report-buffer
- "People by location"
- #'ss/crm--entry-location))
-
- (defun ss/crm-read-string (prompt &optional default)
- "Read PROMPT and trim the result."
- (string-trim (read-string prompt nil nil default)))
-
- (defun ss/crm-read-required-string (prompt &optional default)
- "Read PROMPT and require a non-empty result."
- (let ((value (ss/crm-read-string prompt default)))
- (if (string-empty-p value)
- (user-error "%s is required" (string-remove-suffix ": " prompt))
- value)))
-
- (defun ss/crm-read-optional-string (prompt)
- "Read PROMPT and return nil when the answer is empty."
- (let ((value (ss/crm-read-string prompt)))
- (unless (string-empty-p value)
- value)))
-
- (defun ss/crm-add ()
- "Add a new compact person card to `ss/crm-file'."
- (interactive)
- (let* ((name (ss/crm-read-required-string "Full name: "))
- (abbrev (ss/crm-read-string "Abbrev: " (ss/crm-default-abbrev name)))
- (aliases (ss/crm-read-string "Aliases (comma-separated, optional): "))
- (role (ss/crm-read-required-string "Role: "))
- (team (ss/crm-read-required-string "Team: "))
- (manager (ss/crm-read-required-string "Manager: "))
- (engagement (completing-read "Engagement: "
- ss/crm-engagement-options nil t))
- (supplier (completing-read "Supplier: "
- ss/crm-supplier-options nil t))
- (location (ss/crm-read-required-string "Location: "))
- (current-focus (ss/crm-read-required-string "Current focus: "))
- )
- (when (ss/crm--entry-by-name name)
- (user-error "A person card for %s already exists" name))
- (when (string-empty-p abbrev)
- (setq abbrev (ss/crm-default-abbrev name)))
- (find-file (ss/crm--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/crm--property-line "ABBREV" abbrev)
- (ss/crm--property-line "ALIASES" aliases)
- (ss/crm--property-line "ROLE" role)
- (ss/crm--property-line "TEAM" team)
- (ss/crm--property-line "MANAGER" manager)
- (ss/crm--property-line "ENGAGEMENT" engagement)
- (ss/crm--property-line "SUPPLIER" supplier)
- (ss/crm--property-line "LOCATION" location)
- (ss/crm--property-line "CURRENT_FOCUS" current-focus)
- ":END:\n\n"
- "** Context\n\n"
- "** TODOs\n")
- (save-buffer)
- (ss/crm-reload)
- (ss/crm--open-entry (ss/crm--entry-by-name name))))
-
- (defun ss/crm--clear-installed-abbrevs ()
- "Remove people-specific abbrevs from the current local table."
- (mapatoms
- (lambda (symbol)
- (when (abbrev-get symbol :ss/crm)
- (define-abbrev local-abbrev-table (symbol-name symbol) nil)))
- local-abbrev-table))
-
- (defun ss/crm-install-abbrevs ()
- "Install people abbrevs into the current buffer."
- (unless (ss/crm--source-buffer-p)
- (setq-local local-abbrev-table (copy-abbrev-table local-abbrev-table))
- (ss/crm--clear-installed-abbrevs)
- (dolist (entry (ss/crm-entries))
- (let* ((name (ss/crm--entry-name entry))
- (abbrev (ss/crm--entry-abbrev entry))
- (abbrev-name
- (if (or (null abbrev) (string-empty-p abbrev))
- (ss/crm-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/crm t))))))
-
- (defun ss/crm-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/crm-install-abbrevs)))))
-
- (defun ss/crm-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/crm--entry-by-name candidate)))
- (concat " " (ss/crm--summary entry)))))
- (docsig
- (lambda (candidate)
- (when-let ((entry (ss/crm--entry-by-name candidate)))
- (ss/crm--summary entry)))))
- (list beg end #'ss/crm--completion-table
- :exclusive 'no
- :annotation-function annotation
- :company-docsig docsig)))))))
-
- (defun ss/enable-people-capf ()
- "Add `ss/crm-capf' once in prose buffers."
- (unless (or (ss/crm--source-buffer-p)
- (memq #'ss/crm-capf completion-at-point-functions))
- (add-hook 'completion-at-point-functions #'ss/crm-capf nil t)))
-
- (defun ss/crm--maybe-overview-buffer ()
- "Reset the people CRM buffer to overview when visiting it directly."
- (when (and buffer-file-name
- (string= (file-truename buffer-file-name)
- (file-truename ss/crm-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/crm-install-abbrevs))
- (add-hook 'find-file-hook #'ss/crm--track-buffer)
- (add-hook 'find-file-hook #'ss/crm--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/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/crm-report-by-engagement)
- ("C-c n f" . ss/crm-find)
- ("C-c n i" . ss/crm-insert-name)
- ("C-c n I" . ss/crm-insert-summary)
- ("C-c n L" . ss/crm-report-by-location)
- ("C-c n d" . ss/open-journal)
- ("C-c n o" . ss/crm-overview)
- ("C-c n O" . ss/crm-report-by-role)
- ("C-c n p" . ss/crm-open)
- ("C-c n P" . ss/crm-add)
- ("C-c n R" . ss/crm-report-by-manager)
- ("C-c n S" . ss/crm-report-by-supplier)
- ("C-c n T" . ss/crm-report-by-team))
- :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 CRM lives outside =org-capture=: adding a person uses the
-dedicated =ss/crm-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")))
- "* <%<%Y-%m-%d %H:%M>> %?")
- ("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