hamacs/ha-org.org

24 KiB
Raw Blame History

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
    :mode ("\\.org" . org-mode)    ; Addresses an odd warning
    :init
    <<variables>>
    <<org-todo>>
    <<ob-configuration>>

    :config
    <<ha-org-leader>>
    <<visual-hook>>
    <<text-files>>
    <<org-font-lock>>
    <<no-flycheck-in-org>>
    <<ob-languages>>
    <<ob-graphviz>>
    <<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"
      :global-prefix "<f17>"
      :non-normal-prefix "S-SPC")

Initialization Section

Begin by initializing these org variables:

  (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.

        org-imenu-depth 4
        sentence-end-double-space nil   ; I jump around by sentences, but seldom have two spaces.

        org-export-with-sub-superscripts nil

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

        org-enforce-todo-dependencies t   ; Can't close a task without completed subtasks
        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: Org mode files with the org-content variable setting will collapse two levels headers. Let's allow that without the 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 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, `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 and open it.
  • On a header? Create a new header.
  • In a table? Create a new row.
  • In a list, create a new item.

I 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 one blank line.

  (defun org-really-in-item-p ()
    "Return item beginning position when in a plain list, nil otherwise.
  Unlike `org-in-item-p', 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 the current element. This could be a line-level symbol, like paragraph or list-item, but always when 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 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 showing a screen while taking meeting notes, I don't always like showing other windows, so I created this function to remove 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)) ; 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

I use org-babel (obviously) and dont need confirmation before evaluating a block:

    (setq org-confirm-babel-evaluate nil
          org-src-fontify-natively t
          org-src-tab-acts-natively t)

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

The graphviz project can be written in org blocks, and then rendered as an image:

    (add-to-list 'org-src-lang-modes '("dot" . "graphviz-dot"))

For example:

  digraph G {
    A -> B -> E;
    A -> D;
    A -> C;
    E -> F;
    E -> H
    D -> F;
    A -> H;
    E -> G;
  }

/git/howard/hamacs/media/commit/e06ccd08b97d35bfa46f7bb4002151bcf2d137d9/ha-org-graphviz-example.png

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

Global 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)
      "I" '("insert id" .  org-id-get-create)
      "l" '("insert link" . org-insert-link)
      "N" '("store link" .  org-store-link)
      "o" '("goto link"   . ace-link-org)
      "P" '("set property" .  org-set-property)
      "q" '("set tags" .  org-set-tags-command)
      "t" '("todo"   . org-todo)
      "T" '("list todos" .  org-todo-list)

      "h" '("toggle heading" .  org-toggle-heading)
      "i" '("toggle item" .  org-toggle-item)
      "x" '("toggle checkbox" .  org-toggle-checkbox)

      "." '("goto heading" . consult-org-heading)
      "/" '("agenda" . consult-org-agenda)
      "'" '("edit" . org-edit-special)
      "*" '("C-c *" . org-ctrl-c-star)
      "+" '("C-c -" . org-ctrl-c-minus)

      "d"  '(:ignore t :which-key "dates")
      "d s" '("schedule" . org-schedule)
      "d d" '("deadline" . org-deadline)
      "d t" '("timestamp" . org-time-stamp)
      "d T" '("inactive time" . org-time-stamp-inactive)


      "b"  '(:ignore t :which-key "tables")
      "b -" '("insert hline" . org-table-insert-hline)
      "b a" '("align" . org-table-align)
      "b b" '("blank field" . org-table-blank-field)
      "b c" '("create teable" . org-table-create-or-convert-from-region)
      "b e" '("edit field" . org-table-edit-field)
      "b f" '("edit formula" . org-table-edit-formulas)
      "b h" '("field info" . org-table-field-info)
      "b s" '("sort lines" . org-table-sort-lines)
      "b r" '("recalculate" . org-table-recalculate)
      "b d"  '(:ignore t :which-key "delete")
      "b d c" '("delete column" . org-table-delete-column)
      "b d r" '("delete row" . org-table-kill-row)
      "b i"  '(:ignore t :which-key "insert")
      "b i c" '("insert column" . org-table-insert-column)
      "b i h" '("insert hline" . org-table-insert-hline)
      "b i r" '("insert row" . org-table-insert-row)
      "b i H" '("insert hline ↓" . org-table-hline-and-move)

      "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 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 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   ; Located in my "elisp" directory
    :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)

Spell Checking

Let's hook some spell-checking into org files, and actually all text files. We'll use flyspell mode to highlight the misspelled words, and use the venerable ispell for correcting.

  (use-package flyspell
    :hook (text-mode . flyspell-mode)
    :bind ("M-S" . ha-fix-last-spelling)
    :init
    ;; Tell ispell.el that  can be part of a word.
    (setq ispell-local-dictionary-alist
          `((nil "[[:alpha:]]" "[^[:alpha:]]"
                 "['\x2019]" nil ("-B") nil utf-8)))

    :config
    (defun ha-fix-last-spelling (count)
      "Jump to the last misspelled word, and correct it."
      (interactive "p")
      (save-excursion
        (evil-prev-flyspell-error count)
        (ispell-word)))

    (evil-define-key  'insert text-mode-map (kbd "M-s M-s") 'ha-fix-last-spelling)

    (ha-local-leader :keymaps 'text-mode-map
      "s"  '(:ignore t :which-key "spellcheck")
      "s s" '("correct last misspell" . ha-fix-last-spelling)
      "s b" '("check buffer" . flyspell-buffer)
      "s c" '("correct word" . flyspell-auto-correct-word)
      "s p" '("previous misspell" . evil-prev-flyspell-error)
      "s n" '("next misspell" . evil-next-flyspell-error)))

Sure, the keys, [ s and ] s can jump to misspelled words, and use M-$ to correct them, but I'm getting used to these leaders.

A real issue I often face is I can be typing along and notice a mistake after entering a more words. Since this happens in insert mode, I have bound M-s (twice) to fixing this spelling mistake, and jumping back to where I am. If the spelling mistake is obvious, I use C-; to call flyspell-auto-correct-word.

According to Artur Malabarba, we can turn on rounded apostrophe's, like (left single quotation mark). The idea is to not send the quote to the sub-process:

  (defun endless/replace-apostrophe (args)
    "Don't send  to the subprocess."
    (cons (replace-regexp-in-string
           "" "'" (car args))
          (cdr args)))

  (advice-add #'ispell-send-string :filter-args #'endless/replace-apostrophe)

  (defun endless/replace-quote (args)
    "Convert ' back to  from the subprocess."
    (if (not (derived-mode-p 'org-mode))
        args
      (cons (replace-regexp-in-string
             "'" "" (car args))
            (cdr args))))

  (advice-add #'ispell-parse-output :filter-args #'endless/replace-quote)

The end result? No misspellings. Isnt this nice?

Of course I need a thesaurus, and I'm installing powerthesaurus:

  (use-package powerthesaurus
    :bind ("M-T" . powerthesaurus-lookup-dwim)
    :config
    (ha-local-leader :keymaps 'text-mode-map
      "s t" '("thesaurus" . powerthesaurus-lookup-dwim)
      "s s" '("synonyms" . powerthesaurus-lookup-synonyms-dwim)
      "s a" '("antonyms" . powerthesaurus-lookup-antonyms-dwim)
      "s r" '("related" . powerthesaurus-lookup-related-dwim)
      "s S" '("sentence" . powerthesaurus-lookup-sentences-dwim)))

The key-bindings, keystrokes, and key-connections work well with M-T (notice the Shift), but to jump to specifics, we use a leader. Since the definitions do not work, so let's use abo-abo's define-word project:

  (use-package define-word
    :config
    (ha-local-leader :keymaps 'text-mode-map
      "s d" '("define this" . define-word-at-point)
      "s D" '("define word" . define-word)))

Focused Work

CLOCK: [2022-02-11 Fri 11:05][2022-02-11 Fri 11:21] => 0:16

I've been working on my own approach to focused work,

  (use-package async)

  (use-package ha-focus
    :straight (:type built-in)
    :config
    (ha-leader
     "o f" '("begin focus" . ha-focus-begin)
     "o F" '("break focus" . ha-focus-break)))

Grammar and Prose Linting

Flagging cliches, weak phrasing and other poor grammar choices.

Writegood

The writegood-mode is effective at highlighting passive and weasel words, but isnt integrated into flycheck:

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

We install the write-good NPM:

  npm install -g write-good

And check that the following works:

  write-good --text="So it is what it is."

Now, lets connect it to flycheck:

  (use-package flycheck
    :config
    (flycheck-define-checker write-good
                             "A checker for prose"
                             :command ("write-good" "--parse" source-inplace)
                             :standard-input nil
                             :error-patterns
                             ((warning line-start (file-name) ":" line ":" column ":" (message) line-end))
                             :modes (markdown-mode org-mode text-mode))

    (add-to-list 'flycheck-checkers 'vale 'append))

Proselint

With overlapping goals to write-good, the proselint project, once installed, can check for some English phrasings. I like write-good better, but I want this available for its level of pedantic-ness.

  brew install proselint

Next, create a configuration file, ~/.config/proselint/config file, to turn on/off checks:

{
  "checks": {
    "typography.diacritical_marks": false,
    "consistency.spacing": false
  }
}

And tell flycheck to use this:

  (use-package flycheck
    :config (add-to-list 'flycheck-checkers 'proselint))

Write-room

For a complete focused, distraction-free environment, for writing or concentrating, I'm using Writeroom-mode:

  (use-package writeroom-mode
    :hook (writeroom-mode-disable . winner-undo)
    :config
    (ha-leader "t W" '("writeroom" . writeroom-mode))
    (ha-leader :keymaps 'writeroom-mode-map
      "=" '("adjust width" . writeroom-adjust-width)
      "<" '("decrease width" . writeroom-decrease-width)
      ">" '("increase width" . writeroom-increase-width))

    :bind (:map writeroom-mode-map
                ("C-M-<" . writeroom-decrease-width)
                ("C-M->" . writeroom-increase-width)
                ("C-M-=" . writeroom-adjust-width)))