hamacs/ha-org.org
Howard Abrams 8b53a1654f Add functionality for typographic quotes
Finally getting around to adding nicer quotes from Artur's Endless
Parenthesis blog. Org is getting prettier and prettier.
2022-02-17 09:50:34 -08:00

548 lines
21 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#+TITLE: General Org-Mode Configuration
#+AUTHOR: Howard X. Abrams
#+DATE: 2020-09-18
#+FILETAGS: :emacs:
A literate programming file for configuring org-mode and those files.
#+BEGIN_SRC emacs-lisp :exports none
;;
;; Copyright (C) 2020 Howard X. Abrams
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams
;; Created: September 18, 2020
;;
;; This file is not part of GNU Emacs.
;;
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
;; ~/other/hamacs/ha-org.org
;; And tangle the file to recreate this one.
;;
;;; Code:
#+END_SRC
* Use Package
Org is a /large/ complex beast with a gazillion settings, so I discuss these later in this document.
#+BEGIN_SRC emacs-lisp
(use-package org
;; :straight (:type built-in) ; Use with problems from 9.4.4 version
: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>>)
#+END_SRC
One other helper routine is a =general= macro for org-mode files:
#+NAME: ha-org-leader
#+BEGIN_SRC emacs-lisp :tangle no
(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")
#+END_SRC
* 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).
#+NAME: variables
#+BEGIN_SRC emacs-lisp :tangle no
(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.
sentence-end-double-space nil ; I jump around by sentences, but seldom have two spaces.
;; 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-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 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)
#+END_SRC
* Configuration Section
I pretend that my org files are word processing files that wrap automatically:
#+NAME: visual-hook
#+BEGIN_SRC emacs-lisp :tangle no
(add-hook 'org-mode-hook #'visual-line-mode)
#+END_SRC
Files that end in =.txt= are still org files to me:
#+NAME: text-files
#+BEGIN_SRC emacs-lisp :tangle no
(add-to-list 'auto-mode-alist '("\\.txt\\'" . org-mode))
(add-to-list 'safe-local-variable-values '(org-content . 2))
#+END_SRC
*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.
#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC
And bind it to the Return key:
#+NAME: org-return-key
#+BEGIN_SRC emacs-lisp :tangle no
(define-key org-mode-map (kbd "RET") #'ha-org-return)
#+END_SRC
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 ...
#+BEGIN_SRC emacs-lisp
(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)))))
#+END_SRC
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.
#+BEGIN_SRC emacs-lisp
(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))))
#+END_SRC
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:
#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC
** Tasks
I need to add a /blocked/ state:
#+NAME: org-todo
#+BEGIN_SRC emacs-lisp :tangle no
(setq org-todo-keywords '((sequence "TODO(t)" "DOING(g)" "|" "DONE(d)")
(sequence "BLOCKED(b)" "|" "CANCELLED(c)")))
#+END_SRC
And I would like to have cute little icons for those states:
#+NAME: org-font-lock
#+BEGIN_SRC emacs-lisp
(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) "")))))))
#+END_SRC
** 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.
#+BEGIN_SRC emacs-lisp
(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."))
#+END_SRC
Of course, I need an 'undo' feature when the meeting is over...
#+BEGIN_SRC emacs-lisp
(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
#+END_SRC
** 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.
#+NAME: no-flycheck-in-org
#+BEGIN_SRC emacs-lisp :tangle no
(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)
#+END_SRC
And turn on ALL the languages:
#+NAME: ob-languages
#+BEGIN_SRC emacs-lisp :tangle no
(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)))
#+END_SRC
*** 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:
#+BEGIN_SRC emacs-lisp
(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)))))
#+END_SRC
*** In a PlantUML Block
To make the snippets more context aware, this predicate
#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC
** Keybindings
Keybindings available to all file buffers:
#+NAME: global-keybindings
#+BEGIN_SRC emacs-lisp :tangle no
(ha-leader
"o l" '("store link" . org-store-link)
"o x" '("org capture" . org-capture)
"o c" '("clock out" . org-clock-out))
#+END_SRC
Bindings specific to org files:
#+NAME: org-keybindings
#+BEGIN_SRC emacs-lisp :tangle no
(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))
#+END_SRC
Oh, and we'll use [[https://github.com/abo-abo/ace-link][ace-link]] for quickly jumping:
#+BEGIN_SRC emacs-lisp
(use-package ace-link
:after org
:config
(define-key org-mode-map (kbd "s-o") 'ace-link-org))
#+END_SRC
* Supporting Packages
** Exporters
Limit the number of exporters to just the ones that I would use:
#+NAME: ox-exporters
#+BEGIN_SRC emacs-lisp
(setq org-export-backends '(ascii html icalendar md odt))
#+END_SRC
I have a special version of tweaked [[file:elisp/ox-confluence.el][Confluence exporter]] for my org files:
#+BEGIN_SRC emacs-lisp
(use-package ox-confluence
:after org
:straight nil ; Located in my "elisp" directory
:config
(ha-org-leader
"E" '("to confluence" . ox-export-to-confluence)))
#+END_SRC
And Graphviz configuration using [[https://github.com/ppareit/graphviz-dot-mode][graphviz-dot-mode]]:
#+BEGIN_SRC emacs-lisp
(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))
#+END_SRC
And we can install company support:
#+BEGIN_SRC emacs-lisp :tangle no
(use-package company-graphviz-dot)
#+END_SRC
** Spell Checking
Let's hook some spell-checking into org files, and actually all text files. We'll use [[https://www.emacswiki.org/emacs/FlySpell][flyspell]] mode to highlight the misspelled words, and use the venerable [[https://www.emacswiki.org/emacs/InteractiveSpell][ispell]] for correcting (as it is less mousy).
#+BEGIN_SRC emacs-lisp
(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)))
(ha-local-leader :keymaps 'text-mode-map
"s" '(:ignore t :which-key "spellcheck")
"s c" '("correct last misspell" . ha-fix-last-spelling)
"s p" '("previous misspell" . evil-prev-flyspell-error)
"s n" '("next misspell" . evil-next-flyspell-error)))
#+END_SRC
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.
According to [[http://endlessparentheses.com/ispell-and-apostrophes.html][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:
#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC
The end result? No misspellings. Isnt this nice?
Of course I need a thesaurus, and I'm installing [[https://github.com/SavchenkoValeriy/emacs-powerthesaurus][powerthesaurus]]:
#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC
The key-bindings, keystrokes, key-connections work well with just ~M-T~ (notice the Shift), but to jump to specifics, we use a leader. However, the /definitions/ do not work, so let's use abo-abo's [[https://github.com/abo-abo/define-word][define-word]] project:
#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC
** Writegood
The [[https://github.com/bnbeckwith/writegood-mode][writegood-mode]] highlights passive and weasel words as typed. Shame it doesn't check for dangled prepositions.
#+BEGIN_SRC emacs-lisp
(use-package writegood-mode
:hook ((org-mode . writegood-mode)))
#+END_SRC
** Writeroom
For a complete focused, /distraction-free/ environment, for writing or just concentrating, I'm using [[https://github.com/joostkremers/writeroom-mode][Writeroom-mode]]:
#+BEGIN_SRC emacs-lisp
(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)))
#+END_SRC
* Technical Artifacts :noexport:
Let's provide a name so that the file can be required:
#+BEGIN_SRC emacs-lisp :exports none
(provide 'ha-org)
;;; ha-org.el ends here
#+END_SRC
Before you can build this on a new system, make sure that you put the cursor over any of these properties, and hit: ~C-c C-c~
#+DESCRIPTION: A literate programming file for configuring org-mode and those files.
#+PROPERTY: header-args:sh :tangle no
#+PROPERTY: header-args:emacs-lisp :tangle yes :noweb yes
#+PROPERTY: header-args :results none :eval no-export :comments no mkdirp yes
#+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil
#+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil
#+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js