;;; init.el --- minimal single-file Org workflow -*- lexical-binding: t; -*- ;;; Code: ;; -------------------------------------------------- ;; Core paths ;; -------------------------------------------------- (setq org-directory (expand-file-name "~/org")) (setq org-default-notes-file (expand-file-name "obtf.org" org-directory)) (setq custom-file (expand-file-name "custom.el" (file-name-directory (or load-file-name user-init-file default-directory)))) (defun ss-validate-org-layout () "Warn once at startup when required Org workflow paths are missing." (let ((missing-paths nil)) (unless (file-directory-p org-directory) (push org-directory missing-paths)) (unless (file-exists-p org-default-notes-file) (push org-default-notes-file missing-paths)) (when missing-paths (display-warning 'ss-org (format "Missing required Org workflow paths: %s. Org workflow features may not work as expected." (mapconcat #'identity (nreverse missing-paths) ", ")) :warning)))) (add-hook 'emacs-startup-hook #'ss-validate-org-layout) ;; -------------------------------------------------- ;; Startup: agenda as modal view ;; -------------------------------------------------- (setq initial-buffer-choice (lambda () (if (file-exists-p org-default-notes-file) (progn (require 'org-agenda) (org-agenda nil "h") (current-buffer)) (get-buffer-create "*scratch*")))) ;; -------------------------------------------------- ;; Package bootstrap ;; -------------------------------------------------- (require 'package) (setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/") ("nongnu" . "https://elpa.nongnu.org/nongnu/") ("melpa" . "https://melpa.org/packages/"))) (package-initialize) (dolist (pkg '(vertico marginalia orderless consult modus-themes olivetti)) (unless (package-installed-p pkg) (unless package-archive-contents (package-refresh-contents)) (package-install pkg))) (require 'use-package) (setq use-package-always-ensure t) (use-package git-auto-commit-mode :pin melpa :commands (git-auto-commit-mode) :init (setq gac-shell-and (if (string-match-p "fish\\'" shell-file-name) " ; and " " && "))) ;; -------------------------------------------------- ;; UI ;; -------------------------------------------------- (setq inhibit-startup-message t inhibit-startup-screen t auto-save-default nil backup-inhibited t compilation-ask-about-save nil echo-keystrokes 0.1 enable-recursive-minibuffers t gc-cons-threshold (* 128 1024 1024) gc-cons-percentage 0.1 mouse-wheel-follow-mouse t mouse-wheel-progressive-speed nil mouse-wheel-scroll-amount '(1 ((shift) . 1)) process-adaptive-read-buffering nil read-process-output-max (* 4 1024 1024) ring-bell-function #'ignore scroll-conservatively 101 scroll-margin 2 scroll-preserve-screen-position t scroll-step 1) (column-number-mode 1) (show-paren-mode 1) (global-auto-revert-mode 1) (delete-selection-mode 1) (defalias 'yes-or-no-p 'y-or-n-p) (setq-default abbrev-mode t fill-column 80 indent-tabs-mode nil indicate-empty-lines t sentence-end-double-space nil tab-width 2) (when (file-readable-p abbrev-file-name) (quietly-read-abbrev-file)) (when (display-graphic-p) (tool-bar-mode -1) (scroll-bar-mode -1) ;; Reapply the startup frame font to the selected GUI frame. (let ((font (alist-get 'font default-frame-alist))) (when font (set-face-attribute 'default t :font font)))) (use-package modus-themes :config (load-theme 'modus-vivendi t)) (use-package olivetti :bind (("C-c z" . olivetti-mode)) :config (setq olivetti-body-width 100)) ;; -------------------------------------------------- ;; Completion ;; -------------------------------------------------- (use-package vertico :init (vertico-mode 1)) (use-package marginalia :init (marginalia-mode 1)) (use-package orderless :init (setq completion-styles '(orderless basic))) (use-package consult :bind (("C-s" . consult-line) ("C-c o" . consult-outline))) ;; -------------------------------------------------- ;; Org ;; -------------------------------------------------- (use-package org :ensure nil :bind (("C-c c" . org-capture) ("C-c a" . org-agenda) ("C-c r" . org-refile)) :init (setq org-agenda-files (list org-default-notes-file) org-agenda-custom-commands '(("h" "Home" ((agenda "" ((org-agenda-overriding-header ":: THIS WEEK ::"))) (todo "TODO" ((org-agenda-overriding-header ":: TASKS ::"))) (todo "CLARIFY" ((org-agenda-overriding-header ":: OPEN QUESTIONS ::"))) (tags "CATEGORY=\"inbox\"+LEVEL>1" ((org-agenda-overriding-header ":: REFILE ::")))))) org-agenda-window-setup 'only-window org-startup-folded 'overview org-cycle-hide-drawer-startup t org-drawers '("PROPERTIES" "LOGBOOK") org-todo-keywords '((sequence "TODO" "CLARIFY" "|" "DONE(d!)")) org-use-speed-commands t org-refile-use-outline-path 'file org-outline-path-complete-in-steps nil org-refile-targets '((org-agenda-files :maxlevel . 2)) org-id-link-to-org-use-id nil org-special-ctrl-a/e t org-insert-heading-respect-content t org-log-done 'time org-log-into-drawer "LOGBOOK") :config ;; Keep capture modal in the current window. (add-to-list 'display-buffer-alist '("\\*Org Capture\\*" (display-buffer-reuse-window display-buffer-same-window))) (add-hook 'org-capture-mode-hook (lambda () (delete-other-windows)))) ;; -------------------------------------------------- ;; Capture templates ;; -------------------------------------------------- (setq org-capture-templates `(("i" "Inbox" entry (file+headline ,org-default-notes-file "Inbox") "* %?\n:PROPERTIES:\n:CAPTURED: %U\n:END:\n%a\n") ("t" "Task" entry (file+headline ,org-default-notes-file "Tasks") "* TODO %?\n:PROPERTIES:\n:CAPTURED: %U\n:END:\n%a\n") ("q" "Question" entry (file+headline ,org-default-notes-file "Questions") "* CLARIFY %?\n:PROPERTIES:\n:CAPTURED: %U\n:END:\n%a\n") ("m" "Meeting" entry (file+headline ,org-default-notes-file "Meetings") "* %<%H:%M> %?\n<%<%Y-%m-%d %a %H:%M>>\n") ("r" "Recurring" entry (file+headline ,org-default-notes-file "Recurring") "* TODO %?\n:PROPERTIES:\n:CAPTURED: %U\n:END:\nSCHEDULED: %^t\n"))) ;; Load Custom state last so Customize values can override defaults above. (load custom-file t) (provide 'init)