hamacs/ha-org.org
Howard Abrams f4a69065fc Integrate Mail into Emacs
The mail system connects to my Gmail account with isync and notmuch,
and allows me to interact with with it, like hey.com ... oh, and I can
compose email messsage in org format.

Pretty slick. Now, if I just used mail.
2021-11-14 22:08:09 -08:00

15 KiB

General Org-Mode Configuration

A literate programming file for configuring org-mode and those files.

Initialization Section

Org is an important part of my Emacs world, and with a lot of customization (even though Spacemacs and Doom do a good job getting things started).

  (setq org-return-follows-link t
        org-adapt-indentation nil   ; Don't physically change files
        org-startup-indented t      ; Visually show paragraphs indented
        org-list-indent-offset 2
        org-edit-src-content-indentation 2 ; Doom Emacs sets this to 0,
                                           ; but uses a trick to make it
                                           ; appear indented.

        ;; Speed Commands: If point is at the beginning of a headline or code
        ;; block in org-mode, single keys do fun things. See
        ;; org-speed-command-help for details (or hit the ? key at a headline).
        org-use-speed-commands t

        org-directory "~/personal"
        org-default-notes-file "~/personal/general-notes.txt"

        org-enforce-todo-dependencies t   ; Can't close a task until subtasks are done
        org-agenda-dim-blocked-tasks t
        org-log-done 'time

        org-completion-use-ido t
        org-outline-path-complete-in-steps nil
        org-src-tab-acts-natively t
        org-agenda-span 'day ; Default is 'week
        org-confirm-babel-evaluate nil
        org-src-fontify-natively t
        org-src-tab-acts-natively t)

Configuration Section

The following sections assume that org has been loaded, as this begin the configuration section:

:config

I pretend that my org files are word processing files that wrap automatically:

(add-hook 'org-mode-hook #'visual-line-mode)

Files that end in .txt are still org files to me:

(add-to-list 'auto-mode-alist '("\\.txt\\'" . org-mode))

Many of the files that I edit close some, but not all, of the headers using a file variable. Let's allow that to not insist that I need to approve that:

(add-to-list 'safe-local-variable-values '(org-content . 2))

Better Return

Hitting the Return key in an org file should format the following line based on context. For instance, at the end of a list, insert a new item. We begin with the interactive function that calls our code only if we are at the end of the line.

  (defun ha-org-return ()
    "If at the end of a line, do something special based on the
   information about the line by calling `ha-org-special-return',
   otherwise, just call `org-return' as usual."
    (interactive)
    (if (eolp)
        (ha-org-special-return)
      (org-return)))

And bind it to the Return key:

(define-key org-mode-map (kbd "RET")  #'ha-org-return)

What should we do if we are at the end of a line?

  • Given a prefix, call org-return as usual in an org file.
  • On a link, call org-return in order to open it.
  • On a header? Let's create a new header (or maybe not).
  • In a table? Let's create a new row.
  • If we are really in a list, we can create a new item.

I really should break this function into smaller bits …

  (defun ha-org-special-return (&optional ignore)
    "Add new list item, heading or table row with RET.
  A double return on an empty element deletes it.
  Use a prefix arg to get regular RET."
    (interactive "P")
    (if ignore
        (org-return)
      (cond
       ;; Open links like usual
       ((eq 'link (car (org-element-context)))
        (org-return))

       ((and (org-really-in-item-p) (not (bolp)))
        (if (org-element-property :contents-begin (org-line-element-context))
            (progn
              (end-of-line)
              (org-insert-item))
          (delete-region (line-beginning-position) (line-end-position))))

       ;; ((org-at-heading-p)
       ;;  (if (string= "" (org-element-property :title (org-element-context)))
       ;;      (delete-region (line-beginning-position) (line-end-position))
       ;;    (org-insert-heading-after-current)))

       ((org-at-table-p)
        (if (-any?
             (lambda (x) (not (string= "" x)))
             (nth
              (- (org-table-current-dline) 1)
              (org-table-to-lisp)))
            (org-return)
          ;; empty row
          (beginning-of-line)
          (setf (buffer-substring
          (line-beginning-position) (line-end-position)) "")
          (org-return)))

       (t
        (org-return)))))

How do we know if we are in a list item? Lists end with two blank lines, so we need to make sure we are also not at the beginning of a line to avoid a loop where a new entry gets created with only one blank line.

(defun org-really-in-item-p ()
  "Similar to `org-in-item-p', however, this works around an
issue where the point could actually be in some =code= words, but
still be on an item element."
  (save-excursion
    (let ((location (org-element-property :contents-begin (org-line-element-context))))
      (when location
        (goto-char location))
      (org-in-item-p))))

The org API allows getting the context associated with current element. However, this could be a line-level symbol, like paragraph or list-item only if the point isn't inside a bold or italics item. You know how HTML distinguishes between block and inline elements, org doesn't. So, let's make a function that makes that distinction:

(defun org-line-element-context ()
  "Return the symbol of the current block element, e.g. paragraph or list-item."
  (let ((context (org-element-context)))
    (while (member (car context) '(verbatim code bold italic underline))
      (setq context (org-element-property :parent context)))
    context))

Tasks

I need to add a blocked state:

(setq org-todo-keywords '((sequence "TODO(t)" "DOING(g)" "|" "DONE(d)" )
                          (sequence "BLOCKED(b)" "|" "CANCELLED(c)")))

And I would like to have cute little icons for those states:

(dolist (m '(org-mode org-journal-mode))
  (font-lock-add-keywords m                        ; A bit silly but my headers are now
                          `(("^\\*+ \\(TODO\\) "   ; shorter, and that is nice canceled
                             (1 (progn (compose-region (match-beginning 1) (match-end 1) "⚑") nil)))
                            ("^\\*+ \\(DOING\\) "
                             (1 (progn (compose-region (match-beginning 1) (match-end 1) "⚐") nil)))
                            ("^\\*+ \\(CANCELED\\) "
                             (1 (progn (compose-region (match-beginning 1) (match-end 1) "✘") nil)))
                            ("^\\*+ \\(BLOCKED\\) "
                             (1 (progn (compose-region (match-beginning 1) (match-end 1) "✋") nil)))
                            ("^\\*+ \\(DONE\\) "
                             (1 (progn (compose-region (match-beginning 1) (match-end 1) "✔") nil)))
                            ;; Here is my approach for quickly making the
                            ;; initial asterisks for listing items and whatnot,
                            ;; appear as Unicode bullets (without actually
                            ;; affecting the text file or the behavior).
                            ("^ +\\([-*]\\) "
                             (0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "•")))))))

Meetings

I've notice that while I really like taking notes in a meeting, I don't always like the multiple windows I have opened, so I created this function that I can easily call to eliminate distractions during a meeting.

(defun meeting-notes ()
    "Call this after creating an org-mode heading for where the notes for the meeting
     should be. After calling this function, call 'meeting-done' to reset the environment."
      (interactive)
      (outline-mark-subtree)                             ; Select org-mode section
      (narrow-to-region (region-beginning) (region-end)) ; Only show that region
      (deactivate-mark)
      (delete-other-windows)                             ; remove other windows
      (text-scale-set 2)                                 ; readable by others
      (fringe-mode 0)
      (message "When finished taking your notes, run meeting-done."))

Of course, I need an 'undo' feature when the meeting is over…

(defun meeting-done ()
      "Attempt to 'undo' the effects of taking meeting notes."
      (interactive)
      (widen)                    ; Opposite of narrow-to-region
      (text-scale-set 0)         ; Reset the font size increase
      (fringe-mode 1)
      (winner-undo))             ; Put the windows back in place

Misc

Babel Blocks

Whenever I edit Emacs Lisp blocks from my tangle-able configuration files, I get a lot of superfluous warnings. Let's turn them off.

(defun disable-flycheck-in-org-src-block ()
  (setq-local flycheck-disabled-checkers '(emacs-lisp-checkdoc)))

(add-hook 'org-src-mode-hook 'disable-flycheck-in-org-src-block)

And turn on ALL the languages:

  (org-babel-do-load-languages 'org-babel-load-languages
                               '((shell      . t)
                                 (js         . t)
                                 (emacs-lisp . t)
                                 (clojure    . t)
                                 (python     . t)
                                 (ruby       . t)
                                 (dot        . t)
                                 (css        . t)
                                 (plantuml   . t)))

Next Image

When I create images or other artifacts that I consider part of the org document, I want to have them based on the org file, but with a prepended number. Keeping track of what numbers are now free is difficult, so for a default let's figure it out:

(defun ha-org-next-image-number (&optional prefix)
  (when (null prefix)
    (if (null (buffer-file-name))
        (setq prefix "cool-image")
      (setq prefix (file-name-base (buffer-file-name)))))

  (save-excursion
    (goto-char (point-min))
    (let ((largest 0)
          (png-reg (rx (literal prefix) "-" (group (one-or-more digit)) (or ".png" ".svg"))))
      (while (re-search-forward png-reg nil t)
        (setq largest (max largest (string-to-number (match-string-no-properties 1)))))
      (format "%s-%02d" prefix (1+ largest)))))

In a PlantUML Block

To make the snippets more context aware, this predicate

(defun ha-org-nested-in-plantuml-block ()
  "Predicate is true if point is inside a Plantuml Source code block in org-mode."
  (equal "plantuml"
         (plist-get (cadr (org-element-at-point)) :language)))

Keybindings

Keybindings available to all file buffers:

(ha-leader
  "o l" '("store link" . org-store-link)
  "o x" '("org capture" . org-capture)
  "o c" '("clock out" . org-clock-out))

Bindings specific to org files:

  (general-evil-define-key 'normal org-mode-map
    :prefix "SPC m"
      "e" '("exports"     . org-export-dispatch)
      "l" '("insert link" . org-insert-link)
      "o" '("goto link"   . ace-link-org)

      "n"  '(:ignore t :which-key "narrow")
      "n s" '("subtree" . org-narrow-to-subtree)
      "n b" '("block"   . org-narrow-to-block)
      "n e" '("element" . org-narrow-to-element)
      "n w" '("widen"   . widen))

Oh, and we'll use ace-link for quickly jumping:

(define-key org-mode-map (kbd "M-o") 'ace-link-org)

Supporting Packages

At this point, we assume that the use-package for org is complete, so we can close it and allow other projects to be loaded:

)

Exporters

Need a few extra exporters:

(use-package ox-md
  :after org
  :straight nil
  :config
  (add-to-list 'org-export-backends 'md))

(use-package ox-confluence
  :after org
  :straight nil
  :load-path "~/.doom.d/elisp")

And Graphviz configuration using graphviz-dot-mode:

(use-package graphviz-dot-mode
    :mode "\\.dot\\'"
    :init
    (setq tab-width 4
          graphviz-dot-indent-width 2
          graphviz-dot-auto-indent-on-newline t
          graphviz-dot-auto-indent-on-braces t
          graphviz-dot-auto-indent-on-semi t))

And we can install company support:

(use-package company-graphviz-dot)

Writegood

The writegood-mode highlights passive and weasel words as typed. Shame it doesn't check for dangled prepositions.

(use-package writegood-mode
    :hook ((org-mode . writegood-mode)))