diff options
Diffstat (limited to 'config.org')
| -rw-r--r-- | config.org | 1211 |
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 |
