hamacs/ha-org.org
Howard Abrams 334517380c Add motion to SPC prefix general leader
And fixed a bug to make them consistent.
2021-12-08 13:57:42 -08:00

16 KiB

General Org-Mode Configuration

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

Use Package

Org is a large complex beast with a gazillion settings, so I discuss these later in this document.

  (use-package org
    :straight (:type built-in)  ; Problems with the 9.4.4 version
    ;; :straight (:type git :protocol ssh :repo
    ;; 		   "git://git.sv.gnu.org/emacs/org-mode.git")
    :mode ("\\.org" . org-mode) ; Addresses an odd warning
    :init
    <<variables>>
    <<org-todo>>

    :config
    <<ha-org-leader>>
    <<visual-hook>>
    <<text-files>>
    <<org-font-lock>>
    <<no-flycheck-in-org>>
    <<ob-languages>>
    <<ox-exporters>>
    <<org-return-key>>
    <<global-keybindings>>
    <<org-keybindings>>)

One other helper routine is a general macro for org-mode files:

  (general-create-definer ha-org-leader
      :states '(normal visual motion)
      :keymaps 'org-mode-map
      :prefix "SPC m")

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

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))

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

Note: 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.

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:

  (ha-org-leader
      "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:

  (use-package ace-link
    :after org
    :config
    (define-key org-mode-map (kbd "s-o") 'ace-link-org))

Supporting Packages

Exporters

Limit the number of exporters to just the ones that I would use:

(setq org-export-backends '(ascii html icalendar md odt))

I have a special version of tweaked Confluence exporter for my org files:

  (use-package ox-confluence
    :after org
    :straight nil
    :config
      (ha-org-leader
        "E" '("to confluence"     . ox-export-to-confluence)))

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)))