summaryrefslogtreecommitdiff
path: root/config.org
diff options
context:
space:
mode:
Diffstat (limited to 'config.org')
-rw-r--r--config.org642
1 files changed, 642 insertions, 0 deletions
diff --git a/config.org b/config.org
new file mode 100644
index 0000000..1992330
--- /dev/null
+++ b/config.org
@@ -0,0 +1,642 @@
+#+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))
+
+ (defun ss/home-path (path)
+ "Expand PATH relative to the user's home directory."
+ (expand-file-name path "~"))
+
+ (defun ss/config-path (path)
+ "Expand PATH relative to `user-emacs-directory'."
+ (expand-file-name path user-emacs-directory))
+
+ (defun ss/org-path (path)
+ "Expand PATH relative to the Org directory."
+ (expand-file-name path (ss/home-path "org/")))
+
+ (provide 'init-paths)
+
+ ;; Keep custom-set-variables out of the main config.
+ (setq custom-file (ss/config-path "custom.el"))
+#+end_src
+
+* Package setup
+
+This section bootstraps packages and defines the archives the rest of the
+configuration relies on.
+
+#+begin_src emacs-lisp
+ (eval-and-compile
+ (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
+ (defconst ss/font-family "JetBrains Mono"
+ "Preferred font family for GUI Emacs.")
+
+ (defconst ss/font-height 160
+ "Preferred default font height for GUI Emacs.")
+
+ (defconst ss/font-weight 'medium
+ "Preferred default font weight for GUI Emacs.")
+
+ (defconst ss/frame-width 140
+ "Preferred width for graphical Emacs frames, in columns.")
+
+ (defconst ss/frame-height 42
+ "Preferred height for graphical Emacs frames, in lines.")
+
+ (defun ss/apply-frame-size (&optional frame)
+ "Apply the preferred size to FRAME when it is graphical.
+ If FRAME is nil, use the selected frame."
+ (let ((target-frame (or frame (selected-frame))))
+ (when (display-graphic-p target-frame)
+ (set-frame-size target-frame ss/frame-width ss/frame-height))))
+
+ (defun ss/apply-font-faces ()
+ "Apply the original JetBrains-based face setup."
+ (set-face-attribute
+ 'default nil
+ :family ss/font-family :height ss/font-height :weight ss/font-weight)
+ (set-face-attribute
+ 'fixed-pitch nil
+ :family ss/font-family :weight ss/font-weight)
+ (set-face-attribute
+ 'fixed-pitch-serif nil
+ :family ss/font-family :weight ss/font-weight))
+
+ (defun ss/disable-menu-bar ()
+ "Disable the menu bar for the current frame/session."
+ (menu-bar-mode -1))
+
+ (add-hook 'after-make-frame-functions #'ss/apply-frame-size)
+
+ (when (display-graphic-p)
+ (ss/apply-frame-size)
+ (ss/disable-menu-bar)
+ (tool-bar-mode -1)
+ (scroll-bar-mode -1)
+ (tooltip-mode -1)
+ (when (find-font (font-spec :name ss/font-family))
+ (ss/apply-font-faces)))
+
+ (unless (display-graphic-p)
+ (add-hook 'emacs-startup-hook #'ss/disable-menu-bar))
+
+ (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))
+
+ ;; Customize modeline appearance - white background with line only on top
+ (set-face-attribute 'mode-line nil
+ :background "white"
+ :foreground "black"
+ :overline "gray50"
+ :underline nil
+ :box nil)
+
+ (set-face-attribute 'mode-line-inactive nil
+ :background "gray95"
+ :foreground "gray40"
+ :overline "gray30"
+ :underline nil
+ :box nil)
+
+ ;; Customize the modeline format with padding and right-aligned time
+ (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 22)))))
+ 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
+ (set-language-environment "UTF-8")
+ (set-default-coding-systems 'utf-8)
+ (prefer-coding-system 'utf-8)
+
+ (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
+
+This keeps completion 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))
+#+end_src
+
+* Notes workflow
+
+This keeps the note-taking system deliberately small. Daily notes stay as plain
+Org files in =~/org/daily/=, 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, ensures the PARA structure is
+present at startup, and provides a small helper for opening today's daily note
+with the standard section layout already in place. Agenda views stay focused on
+PARA notes, so project, area, and resource files can surface TODOs without
+pulling in daily or archived notes. A small directory helper keeps PARA
+subdirectories easy to create from the minibuffer before capturing into them.
+
+#+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/org-daily-directory (expand-file-name "daily/" ss/org-directory)
+ "Directory for plain daily Org notes.")
+
+ (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-people-directory (expand-file-name "areas/people/" ss/org-directory)
+ "Directory for people 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/org-note-directories
+ (list ss/org-directory
+ ss/org-daily-directory
+ ss/org-projects-directory
+ ss/org-areas-directory
+ ss/org-people-directory
+ ss/org-resources-directory
+ ss/org-archives-directory)
+ "Directories that make up the note-taking workflow.")
+
+ (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.")
+
+ (defconst ss/org-subdirectory-roots
+ `(("projects" . ,ss/org-projects-directory)
+ ("areas" . ,ss/org-areas-directory)
+ ("people" . ,ss/org-people-directory)
+ ("resources" . ,ss/org-resources-directory))
+ "PARA roots offered when creating note subdirectories.")
+
+ (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))
+ (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/note-subdirectory-candidates (root)
+ "Return existing subdirectories under ROOT as relative paths."
+ (sort
+ (delete-dups
+ (mapcar (lambda (path)
+ (directory-file-name (file-relative-name path root)))
+ (seq-filter
+ #'file-directory-p
+ (directory-files-recursively root directory-files-no-dot-files-regexp t t))))
+ #'string<))
+
+ (defun ss/create-note-subdirectory ()
+ "Create a PARA subdirectory using minibuffer completion."
+ (interactive)
+ (let* ((root-name (completing-read
+ "PARA root: "
+ (mapcar #'car ss/org-subdirectory-roots)
+ nil t))
+ (root (alist-get root-name ss/org-subdirectory-roots nil nil #'string=))
+ (completion-extra-properties '(:category file))
+ (subdirectory (completing-read
+ (format "Subdirectory in %s: " root-name)
+ (ss/note-subdirectory-candidates root)
+ nil nil))
+ (target (expand-file-name subdirectory root))
+ (existing (file-directory-p target)))
+ (make-directory target t)
+ (message "%s note directory: %s"
+ (if existing "Using existing" "Created")
+ target)))
+
+ (defun ss/ensure-org-note-directories ()
+ "Create the Org directories used by the notes workflow."
+ (mapc (lambda (directory)
+ (make-directory directory t))
+ ss/org-note-directories))
+
+ (defun ss/ensure-org-agenda-loaded ()
+ "Load Org agenda support before using agenda-specific helpers.
+This ensures `org-agenda-file-regexp' and `org-agenda' are available."
+ (require 'org-agenda))
+
+ (defun ss/org-agenda-files ()
+ "Return the Org files that should be scanned by the agenda."
+ (ss/ensure-org-agenda-loaded)
+ (delete-dups
+ (apply #'append
+ (mapcar (lambda (directory)
+ (if (file-directory-p directory)
+ (directory-files-recursively directory org-agenda-file-regexp)
+ nil))
+ ss/org-agenda-directories))))
+
+ (defun ss/refresh-org-agenda-files ()
+ "Refresh `org-agenda-files' from the current PARA directories."
+ (setq org-agenda-files (ss/org-agenda-files)))
+
+ (defun ss/daily-note-path (&optional time)
+ "Return the file name for the daily note at TIME.
+If TIME is nil, use the current date."
+ (expand-file-name
+ (format-time-string "%Y-%m-%d.org" time)
+ ss/org-daily-directory))
+
+ (defun ss/daily-note-template (&optional time)
+ "Return the initial contents for the daily note at TIME."
+ (format "#+title: %s\n\n* Tasks\n\n* Meetings\n\n* Notes\n\n* Open loops\n"
+ (format-time-string "%Y-%m-%d" (or time (current-time)))))
+
+ (defun ss/ensure-daily-note (&optional time)
+ "Create the daily note for TIME when it does not exist.
+Return the path to the note."
+ (let* ((date (or time (current-time)))
+ (file (ss/daily-note-path date)))
+ (unless (file-exists-p file)
+ (make-directory (file-name-directory file) t)
+ (with-temp-file file
+ (insert (ss/daily-note-template date))))
+ file))
+
+ (defun ss/open-todays-note ()
+ "Open today's daily Org note."
+ (interactive)
+ (find-file (ss/ensure-daily-note)))
+
+ (defun ss/open-agenda ()
+ "Refresh agenda files and invoke `org-agenda'."
+ (interactive)
+ (ss/ensure-org-agenda-loaded)
+ (ss/refresh-org-agenda-files)
+ (call-interactively #'org-agenda))
+ :bind (("C-c a" . ss/open-agenda)
+ ("C-c c" . org-capture)
+ ("C-c n m" . ss/create-note-subdirectory)
+ ("C-c n d" . ss/open-todays-note))
+ :config
+ (setq org-directory ss/org-directory
+ org-hide-emphasis-markers t)
+ (add-hook 'org-mode-hook
+ (lambda ()
+ (setq-local org-hide-emphasis-markers t)
+ (font-lock-flush)
+ (font-lock-ensure)))
+ (ss/refresh-org-agenda-files)
+ (add-hook 'org-capture-after-finalize-hook #'ss/refresh-org-agenda-files)
+ (ss/ensure-org-note-directories))
+#+end_src
+
+** Capture entry points
+
+Daily capture goes to today's plain Org file. 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 set a few durable
+defaults and prompt for subdirectory placement within the relevant PARA root.
+
+#+begin_src emacs-lisp
+ (use-package org-capture
+ :ensure nil
+ :after (org denote)
+ :config
+ (setq org-capture-templates
+ `(("d" "Daily")
+ ("dt" "Task" entry
+ (file+headline ,#'ss/ensure-daily-note "Tasks")
+ "* TODO %?")
+ ("dn" "Note" entry
+ (file+headline ,#'ss/ensure-daily-note "Notes")
+ "* %?")
+ ("dm" "Meeting" entry
+ (file+headline ,#'ss/ensure-daily-note "Meetings")
+ "* %<%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 '("area") :title :keywords :subdirectory)))
+ :no-save t
+ :immediate-finish nil
+ :kill-buffer t
+ :jump-to-captured t)
+ ("nP" "Person" plain
+ (file denote-last-path)
+ (function
+ (lambda ()
+ (ss/denote-capture-in-directory
+ ss/org-people-directory '("person") :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 '("resource") :title :keywords :subdirectory)))
+ :no-save t
+ :immediate-finish nil
+ :kill-buffer t
+ :jump-to-captured t)
+ ("nm" "Meeting" plain
+ (file denote-last-path)
+ (function
+ (lambda ()
+ (ss/denote-capture-in-directory
+ ss/org-directory '("meeting") :title :keywords :subdirectory)))
+ :no-save t
+ :immediate-finish nil
+ :kill-buffer t
+ :jump-to-captured t))))
+#+end_src
+
+** Denote
+
+Denote manages the durable notes. The folder layout reflects lifecycle, while
+Denote handles naming, metadata, linking, and retrieval.
+
+#+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 '("area" "project" "person" "meeting" "1on1" "resource" "decision")
+ denote-prompts '(title keywords)
+ denote-org-capture-specifiers "%?")
+ (denote-rename-buffer-mode 1))
+#+end_src
+
+* Gptel workflow
+
+This keeps LLM chat available as 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