;;; dot-org-mode.el --- -*- lexical-binding: t; -*- ;;; Commentary: ;; Load org-mode modules. ;;; Code: ;; ----------------------------------------- ;; LaTeX Configuration (elpaca-nil (setup tex-mode ; built-in (:when-loaded (defun dot/tex-mode-init () "" (setq indent-tabs-mode t) (setq tab-width 4) (setq tex-indent-basic 4)) (:hook dot/tex-mode-init) (with-eval-after-load 'project (defun compile-latex () "Compile LaTeX project." (interactive) (let ((default-directory (dot/find-project-root))) (dot/project-save-project-buffers) (shell-command "make"))))))) ;; ----------------------------------------- ;; Org Configuration ;; Base Org. (elpaca-nil (setup org ; built-in (setq org-directory (expand-file-name "documents/org" (getenv "HOME"))) (setq org-default-notes-file (expand-file-name "notes.org" org-directory)) (:with-mode before-save (:hook dot/org-set-last-modified)) (:when-loaded (setq org-adapt-indentation nil) (setq org-ellipsis "⤵") (setq org-image-actual-width nil) (setq org-startup-folded nil) ;; Enable structured template completion (add-to-list 'org-modules 'org-tempo t) (add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp")) (defun dot/org-find-or-create-heading (heading level) "Find or create heading" (if (re-search-forward (format org-complex-heading-regexp-format (regexp-quote heading)) nil t) (beginning-of-line) (goto-char (point-max)) (unless (bolp) (insert "\n")) (insert (concat (make-string level ?*) " ") heading "\n") (beginning-of-line 0))) (defun dot/org-find-month-week-day () "Find or create the journal tree for the current week under current month" (unless (derived-mode-p 'org-mode) (error "Target buffer \"%s\" for dot/find-org-week should be in Org mode" (current-buffer))) (org-capture-put-target-region-and-position) (widen) (goto-char (point-min)) (dot/org-find-or-create-heading "Worklog" 1) (org-narrow-to-subtree) (dot/org-find-or-create-heading (format-time-string "%B") 2) ;; month (org-narrow-to-subtree) (dot/org-find-or-create-heading (format-time-string "Week %W") 3) (org-narrow-to-subtree) (dot/org-find-or-create-heading (format-time-string "%A") 4) ;; day (org-end-of-subtree)) ;; Capture templates (setq org-capture-templates `(("i" "Clock in" plain (file+function ,(expand-file-name "worklog.org" org-directory) dot/org-find-month-week-day) "| %<%Y-%m-%d> | IN | %<%H:%M> | %^{Location|Office|Home|Office|Visit} |\n===================================%?" :immediate-finish t) ("o" "Clock out" plain (file+function ,(expand-file-name "worklog.org" org-directory) dot/org-find-month-week-day) "===================================\n| %<%Y-%m-%d> | OUT | %<%H:%M> |%?" :immediate-finish t) ("s" "Switch task" plain (file+function ,(expand-file-name "worklog.org" org-directory) dot/org-find-month-week-day) "| %<%Y-%m-%d> | %<%H:%M> | %^{Item ID} | X | %^{Description|-} |%?" :immediate-finish t))) ;; Keybind functions (defun dot/org-clock-in () (interactive) (org-capture nil "i")) (defun dot/org-clock-out () (interactive) (org-capture nil "o")) (defun dot/org-switch-task () (interactive) (org-capture nil "s")) (with-eval-after-load 'evil-commands (defun dot/org-ret-at-point () "Org return key at point. If point is on: checkbox -- toggle it link -- follow it table -- go to next row otherwise -- run the default (evil-ret) expression" (interactive) (let ((type (org-element-type (org-element-context)))) (pcase type ('link (if org-return-follows-link (org-open-at-point) (evil-ret))) ((guard (org-at-item-checkbox-p)) (org-toggle-checkbox)) ('table-cell (org-table-next-row)) (_ (evil-ret)) )))) (defun dot/org-find-file-property (property &optional anywhere) "Return the position of the file PROPERTY if it exists. When ANYWHERE is non-nil, search beyond the preamble." (save-excursion (goto-char (point-min)) (let ((first-heading (save-excursion (re-search-forward org-outline-regexp-bol nil t)))) (when (re-search-forward (format "^#\\+%s:" property) (if anywhere nil first-heading) t) (point))))) (defun dot/org-set-file-property (property value) "Set the file PROPERTY in the preamble." (when-let ((pos (dot/org-find-file-property property))) (save-excursion (goto-char pos) (if (looking-at-p " ") (forward-char) (insert " ")) (delete-region (point) (line-end-position)) (insert value)))) (defun dot/org-set-last-modified () "Update the LAST_MODIFIED file property in the preamble." (when (derived-mode-p 'org-mode) (dot/org-set-file-property "LAST_MODIFIED" (format-time-string "[%Y-%m-%d %a %H:%M]"))))))) ;; Org agenda. (elpaca-nil (setup org-agenda ; built-in (:load-after evil org) (:when-loaded (setq org-agenda-files `(,org-directory ,user-emacs-directory)) (setq org-agenda-span 14) (setq org-agenda-window-setup 'current-window) (evil-set-initial-state 'org-agenda-mode 'motion)))) ;; Org capture. (elpaca-nil (setup org-capture ; built-in ;; Org-capture in new tab, rather than split window (:hook delete-other-windows))) ;; Org keys. (elpaca-nil (setup org-keys ; built-in (:when-loaded (setq org-return-follows-link t)))) ;; Org links. (elpaca-nil (setup ol ; built-in (:when-loaded ;; Do not open links to .org files in a split window (add-to-list 'org-link-frame-setup '(file . find-file))))) ;; Org source code blocks. (elpaca-nil (setup org-src ; built-in (:when-loaded (setq org-edit-src-content-indentation 0) (setq org-src-fontify-natively t) (setq org-src-preserve-indentation t) (setq org-src-tab-acts-natively t) (setq org-src-window-setup 'current-window)))) ;; Org exporter. (elpaca-nil (setup ox ; built-in (:when-loaded (setq org-export-coding-system 'utf-8-unix)))) ;; Org latex exporter. (elpaca-nil (setup ox-latex ; built-in (:when-loaded ;; Define how minted (highlighted src code) is added to src code blocks ;; Requires packages: texlive-bin, texlive-latexextra and python-pygments (setq org-latex-listings 'minted) (setq org-latex-minted-options '(("frame" "lines") ("linenos=true"))) ;; Set 'Table of Contents' layout (setq org-latex-toc-command "\\newpage \\tableofcontents \\newpage") ;; Add minted package to every LaTeX header (add-to-list 'org-latex-packages-alist '("" "minted")) ;; Add -shell-escape so pdflatex exports minted correctly (setcar org-latex-pdf-process (replace-regexp-in-string "-%latex -interaction" "-%latex -shell-escape -interaction" (car org-latex-pdf-process)))))) ;;; Org Bullets (elpaca-setup org-bullets (:hook-into org-mode)) ;;; Org Export Packages ;; HTML exporter. (elpaca-setup htmlize (:when-loaded (setq org-export-html-postamble nil))) ;;org-export-html-postamble-format ; TODO ;; GitHub flavored Markdown exporter. (elpaca-setup ox-gfm) ;;; Org Roam (elpaca-setup emacsql-sqlite-builtin) (elpaca-setup org-roam (:autoload org-roam-node-find) ;; TODO, is this enough? (setq org-roam-v2-ack t) (setq org-roam-database-connector 'sqlite-builtin) (:when-loaded (setq org-roam-db-location (expand-file-name "org-roam.db" dot-cache-dir)) (setq org-roam-directory org-directory) ;; Exclude Syncthing backup directory (setq org-roam-file-exclude-regexp "\\.stversions") (setq org-roam-verbose nil) (setq org-roam-capture-templates '(("d" "default" plain "%?" :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+TITLE: ${title}\n#+LAST_MODIFIED:\n#+FILETAGS: %^{File tags||:structure:}\n") :unnarrowed t))) (defun dot/org-roam-node-insert-immediate (arg &rest args) (interactive "P") (let ((args (push arg args)) (org-roam-capture-templates (list (append (car org-roam-capture-templates) '(:immediate-finish t))))) (apply #'org-roam-node-insert args))) (cl-defmethod org-roam-node-slug ((node org-roam-node)) "Return the slug of NODE, strip out common words." (let* ((title (org-roam-node-title node)) (words (split-string title " ")) (common-words '("a" "an" "and" "as" "at" "by" "is" "it" "of" "the" "to")) (title (string-join (seq-remove (lambda (element) (member element common-words)) words) "_")) (pairs '(("c\\+\\+" . "cpp") ;; convert c++ -> cpp ("c#" . "cs") ;; convert c# -> cs ("[^[:alnum:][:digit:]]" . "_") ;; convert anything not alphanumeric ("__*" . "_") ;; remove sequential underscores ("^_" . "") ;; remove starting underscore ("_$" . "")))) ;; remove ending underscore (cl-flet ((cl-replace (title pair) (replace-regexp-in-string (car pair) (cdr pair) title))) (downcase (-reduce-from #'cl-replace title pairs))))) ;; Right-align org-roam-node-tags in the completion menu without a length limit ;; Source: https://github.com/org-roam/org-roam/issues/1775#issue-971157225 (setq org-roam-node-display-template "${title} ${tags:0}") (setq org-roam-node-annotation-function #'dot/org-roam-annotate-tag) (defun dot/org-roam-annotate-tag (node) (let ((tags (mapconcat 'identity (org-roam-node-tags node) " #"))) (unless (string-empty-p tags) (concat (propertize " " 'display `(space :align-to (- right ,(+ 2 (length tags))))) (propertize (concat "#" tags) 'face 'bold))))) (org-roam-setup))) ;; Enable https://www.orgroam.com/manual.html#org_002droam_002dprotocol, needed to process org-protocol:// links (elpaca-nil (setup org-roam-protocol ; org-roam-protocol.el is part of org-roam (:load-after org-roam) (:when-loaded ;; Templates used when creating a new file from a bookmark (setq org-roam-capture-ref-templates '(("r" "ref" plain "%?" :target (file+head "${slug}.org" "#+TITLE: ${title}\n \n${body}") :unnarrowed t)))))) ;; The roam-ref protocol bookmarks to add: ;; javascript:location.href = ;; 'org-protocol://roam-ref?template=r' ;; + '&ref=' + encodeURIComponent(location.href) ;; + '&title=' + encodeURIComponent(document.title) ;; + '&body=' + encodeURIComponent(window.getSelection()) ;; Setup org-roam-ui, runs at http://127.0.0.1:35901. (elpaca-setup org-roam-ui (:load-after org-roam) (:when-loaded (setq org-roam-ui-follow t) (setq org-roam-ui-open-on-start t) (setq org-roam-ui-sync-theme nil) ;; FIXME: Make this work (org-roam-ui-get-theme) (setq org-roam-ui-update-on-save t))) ;; Easily searchable .org files via Deft. (elpaca-setup deft (:hook dot/hook-disable-line-numbers) (:when-loaded (setq deft-auto-save-interval 0) (setq deft-default-extension "org") (setq deft-directory org-directory) (setq deft-file-naming-rules '((noslash . "-") (nospace . "-") (case-fn . downcase))) (setq deft-new-file-format "%Y%m%d%H%M%S-deft") (setq deft-recursive t) ;; Exclude Syncthing backup directory (setq deft-recursive-ignore-dir-regexp (concat "\\.stversions\\|" deft-recursive-ignore-dir-regexp)) ;; Remove file variable -*- .. -*- and Org Mode :PROPERTIES: lines (setq deft-strip-summary-regexp (concat "\\(^.*-\\*-.+-\\*-$\\|^:[[:alpha:]_]+:.*$\\)\\|" deft-strip-summary-regexp)) (setq deft-use-filename-as-title nil) (setq deft-use-filter-string-for-filename t) (add-to-list 'deft-extensions "tex") ;; Start filtering immediately (with-eval-after-load 'evil (evil-set-initial-state 'deft-mode 'insert)) (defun deft-parse-title (file contents) "Parse the given FILE and CONTENTS and determine the title." (if (string-match "#\\+\\(TITLE\\|title\\):\s*\\(.*\\)$" contents) (match-string 2 contents) (deft-base-filename file))))) ;;; Org "Table of Contents" ;; Generate table of contents without exporting. (elpaca-setup toc-org (:hook-into org-mode)) (provide 'dot-org-mode) ;;; dot-org-mode.el ends here