diff options
| author | Szymon Szukalski <szymon@szymonszukalski.com> | 2026-04-01 15:39:17 +1100 |
|---|---|---|
| committer | Szymon Szukalski <szymon@szymonszukalski.com> | 2026-04-01 15:39:17 +1100 |
| commit | 08bfb73d73d0b0d63a93a341704d16ef8e3d6cab (patch) | |
| tree | c7ed62958ce10b2a514de1696ad4094975f935cd /config.org | |
initial commit
Diffstat (limited to 'config.org')
| -rw-r--r-- | config.org | 642 |
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 |
