1303 lines
55 KiB
Org Mode
1303 lines
55 KiB
Org Mode
#+title: General Org-Mode Configuration
|
||
#+author: Howard X. Abrams
|
||
#+date: 2020-09-18
|
||
#+tags: emacs org
|
||
#+startup: inlineimages
|
||
#+lastmod: [2024-10-25 Fri]
|
||
|
||
A literate programming file for configuring org-mode and those files.
|
||
|
||
#+begin_src emacs-lisp :exports none
|
||
;;; ha --- Org configuration. -*- lexical-binding: t; -*-
|
||
;;
|
||
;; © 2020-2023 Howard X. Abrams
|
||
;; Licensed under a Creative Commons Attribution 4.0 International License.
|
||
;; See http://creativecommons.org/licenses/by/4.0/
|
||
;;
|
||
;; 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 :noweb yes
|
||
(use-package org
|
||
;; TODO: Using the latest org-mode
|
||
;; :straight (:type built-in)
|
||
:mode (("\\.org" . org-mode))
|
||
:init
|
||
<<variables>>
|
||
<<org-todo>>
|
||
<<org-todo-clock>>
|
||
<<ob-configuration>>
|
||
<<html-exporting>>
|
||
|
||
: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>>)
|
||
#+end_src
|
||
* Initialization Section
|
||
Begin by initializing these org variables:
|
||
#+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.
|
||
|
||
org-imenu-depth 4
|
||
sentence-end-double-space nil ; I jump around by sentences, but seldom have two spaces.
|
||
|
||
org-html-validation-link nil
|
||
org-export-with-sub-superscripts nil
|
||
org-export-with-drawers nil
|
||
org-export-with-author nil
|
||
org-export-with-email nil
|
||
org-export-with-date nil
|
||
org-export-with-todo-keywords nil
|
||
org-export-with-broken-links nil
|
||
org-export-with-toc nil ; Only for my hamacs publishing
|
||
org-export-with-date nil
|
||
org-export-with-title nil
|
||
org-export-with-section-numbers nil
|
||
org-export-with-creator nil
|
||
org-export-with-smart-quotes t
|
||
org-export-with-timestamps nil
|
||
org-export-time-stamp-file nil
|
||
org-export-date-timestamp-format "%e %B %Y"
|
||
|
||
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
|
||
|
||
;; Updates the lastmod: when set in the file:
|
||
time-stamp-active t
|
||
time-stamp-start "#\\+lastmod:[ \t]*"
|
||
time-stamp-end "$"
|
||
time-stamp-format "[%04Y-%02m-%02d %a]")
|
||
#+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)
|
||
(add-hook 'before-save-hook 'time-stamp nil)
|
||
#+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:* 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.
|
||
|
||
#+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, `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= 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 ...
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun ha-org-special-return (&optional ignore)
|
||
"Add new list item 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))))
|
||
|
||
(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 one blank line.
|
||
#+begin_src emacs-lisp
|
||
(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))))
|
||
#+end_src
|
||
|
||
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:
|
||
#+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, and wouldn’t /doing/ be better than /in progress/ (you know, without a space):
|
||
#+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
|
||
|
||
[[https://janusworx.com/blog/what-i-learned-today-2023-02-10/][Mario Braganza]] had an interesting idea of starting the clock when a task changes to /in progress/:
|
||
#+name: org-todo-clock
|
||
#+begin_src emacs-lisp
|
||
(defun ha-org-clock-todo-change ()
|
||
"Called from hook `org-after-todo-state-change-hook'.
|
||
Clock in if a task changes to DOING (i.e. IN_PROGRESS),
|
||
and clocks out with any other state change."
|
||
(if (string= org-state "DOING")
|
||
(org-clock-in)
|
||
(org-clock-out-if-current)))
|
||
|
||
(add-hook 'org-after-todo-state-change-hook 'ha-org-clock-todo-change)
|
||
#+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 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 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.
|
||
|
||
#+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)) ; 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
|
||
** Searching
|
||
Came up with a great way to search a project for Org-specific files, and wrote [[https://howardism.org/Technical/Emacs/org-find-file.html][an essay]] describing the approach and the code. The idea is that I can call =find-file=, but the list of files is not only the filename, but the Org =#+title:= as well as any tags located in the file.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package org-find-file
|
||
:straight nil
|
||
:config
|
||
(ha-leader "f o" '("load org" . org-find-file)))
|
||
#+end_src
|
||
|
||
Now that my paragraphs in an org file are on a single line, I could use =rg= (or some other =grep= program), but being able to use an /indexed search system/, like [[https://ss64.com/osx/mdfind.html][mdfind]] on Macos, or [[https://www.lesbonscomptes.com/recoll/][recoll]] on Linux, gives better results than line-oriented search systems. Let’s create operating-system functions the command line for searching:
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun ha-search-notes--macos (phrase path)
|
||
"Return the indexed search system command on MACOS, mdfind.
|
||
Including the parameters using the PHRASE on the PATH(s)."
|
||
(let ((paths (if (listp path)
|
||
(mapconcat (lambda (p) (concat "-onlyin " p)) path " ")
|
||
(concat "-onlyin " path))))
|
||
(format "mdfind %s -interpret %s" paths phrase)))
|
||
|
||
(defun ha-search-notes--linux (phrase path)
|
||
"Return the indexed search system command on Linux, recoll.
|
||
Including the parameters using the PHRASE on the PATH(s)."
|
||
(format "recoll -t -a -b %s" phrase))
|
||
#+end_src
|
||
|
||
And let’s see how that works:
|
||
#+begin_src emacs-lisp :tangle no :results replace
|
||
(ha-search-notes--macos "crossway stream" "~/Notes")
|
||
#+end_src
|
||
|
||
This function calls the above-mentioned operating-system-specific functions, but returns the matching files as a /single string/ (where single quotes wrap each file, and all joined together, separated by spaces). This function also allows me to /not-match/ backup files and whatnot.
|
||
#+begin_src emacs-lisp
|
||
(defun ha-search-notes--files (phrase path)
|
||
"Return an escaped string of all files matching PHRASE.
|
||
On a Mac, the PATH limits the scope of the search."
|
||
(let ((command (if (ha-running-on-macos?)
|
||
(ha-search-notes--macos phrase path)
|
||
(ha-search-notes--linux phrase path))))
|
||
(->> command
|
||
(shell-command-to-list)
|
||
(--remove (s-matches? "~$" it))
|
||
(--remove (s-matches? "#" it))
|
||
(--map (format "'%s'" it))
|
||
(s-join " "))))
|
||
#+end_src
|
||
Let’s see it in action:
|
||
#+begin_src emacs-lisp :tangle no :results replace
|
||
(ha-search-notes--files "openstack grafana" '("~/Notes"))
|
||
#+end_src
|
||
|
||
Returns this string:
|
||
#+begin_example
|
||
"'/Users/howard.abrams/Notes/Sprint-2022-25.org' '/Users/howard.abrams/Notes/Sprint-2022-03.org' '/Users/howard.abrams/Notes/Sprint-2020-45.org' '/Users/howard.abrams/Notes/Sprint-2022-09.org' '/Users/howard.abrams/Notes/Sprint-2022-05.org' '/Users/howard.abrams/Notes/Sprint-2022-01.org' '/Users/howard.abrams/Notes/Sprint-2022-19.org'"
|
||
#+end_example
|
||
|
||
The =ha-search-notes= function prompts for the phrase to search, and then searches through the =org-directory= path, acquiring matching files, to feed to =grep= (and the [[help:grep][grep function]]) to display a list of matches that I can jump to.
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun ha-search-notes (phrase &optional path)
|
||
"Search files in PATH for PHRASE and display in a grep mode buffer."
|
||
(interactive "sSearch notes for: ")
|
||
(let* ((command (if (ha-running-on-macos?) "ggrep" "grep"))
|
||
(regexp (string-replace " " "\\|" phrase))
|
||
(use-paths (or path (list org-directory org-journal-dir)))
|
||
(files (ha-search-notes--files phrase use-paths))
|
||
(cmd-line (format "%s -ni -m 1 '%s' %s" command regexp files)))
|
||
(grep cmd-line)))
|
||
#+end_src
|
||
|
||
Add a keybinding to the function:
|
||
#+begin_src emacs-lisp
|
||
(ha-leader "f n" '("find notes" . ha-search-notes))
|
||
#+end_src
|
||
** Misc
|
||
*** Babel Blocks
|
||
I use [[https://orgmode.org/worg/org-contrib/babel/intro.html][org-babel]] (obviously) and don’t need confirmation before evaluating a block:
|
||
|
||
#+name: ob-configuration
|
||
#+begin_src emacs-lisp :tangle no
|
||
(setq org-confirm-babel-evaluate nil
|
||
org-src-fontify-natively t
|
||
org-src-tab-acts-natively t
|
||
org-src-window-setup 'current-window)
|
||
#+end_src
|
||
|
||
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
|
||
|
||
The [[https://github.com/isamert/corg.el][corg project]] does completing feature for all the block header values. To do this, type ~M-Tab~ (not just regular ~Tab~).
|
||
|
||
#+begin_src emacs-lisp :results list :hlines yes
|
||
(use-package corg
|
||
:straight (:host github :repo "isamert/corg.el")
|
||
:hook (org-mode . 'corg-setup))
|
||
#+end_src
|
||
|
||
*** Searching Literate Files
|
||
A noweb definition, e.g. =<<something-something>>= could /jump/ to the =#name= definition.
|
||
Since [[https://github.com/BurntSushi/ripgrep][ripgrep]] is pretty fast, I’ll call it instead of attempting to build a [[https://stackoverflow.com/questions/41933837/understanding-the-ctags-file-format][CTAGS]] table. Oooh, the =rg= takes a =—json= option, which makes it easier to parse.
|
||
|
||
#+begin_src emacs-lisp :noweb no
|
||
(defun ha-org-noweb-block-jump (str pos)
|
||
"Go to a literate org file containing a symbol, STR.
|
||
The POS is ignored."
|
||
;; Sometimes I wrap a function name in `=' characters, and these should be removed:
|
||
(when (string-match (rx "<<" (group (one-or-more any)) ">>") str)
|
||
(setq str (match-string 1 str)))
|
||
(ignore-errors
|
||
(let* ((default-directory (project-root (project-current)))
|
||
(command (format "rg --ignore-case --json '#\\+name: +%s' *.org" str))
|
||
|
||
(results (thread-last command
|
||
shell-command-to-list
|
||
second
|
||
json-parse-string))
|
||
(file (thread-last results
|
||
(gethash "data")
|
||
(gethash "path")
|
||
(gethash "text")))
|
||
(line (thread-last results
|
||
(gethash "data")
|
||
(gethash "line_number"))))
|
||
(find-file file)
|
||
(goto-line line))))
|
||
|
||
(when (fboundp 'evil-goto-definition-functions)
|
||
(add-to-list 'evil-goto-definition-functions 'ha-org-noweb-block-jump))
|
||
#+end_src
|
||
|
||
*** REST Web Services
|
||
Emacs has two ways to query and investigate REST-oriented web services. The [[https://github.com/zweifisch/ob-http][ob-http]] adds HTTP calls to standard org blocks.
|
||
#+begin_src emacs-lisp
|
||
(use-package ob-http
|
||
:init
|
||
(add-to-list 'org-babel-load-languages '(http . t)))
|
||
#+end_src
|
||
And let’s see how it works:
|
||
#+begin_src http :pretty :results value replace :wrap src js :var user-agent="my-super-agent"
|
||
GET https://api.github.com/repos/zweifisch/ob-http/languages
|
||
Accept: application/json
|
||
User-Agent: ${user-agent}
|
||
#+end_src
|
||
|
||
#+results:
|
||
#+begin_src js
|
||
{
|
||
"Emacs Lisp": 15327,
|
||
"Shell": 139
|
||
}
|
||
#+end_src
|
||
|
||
Another approach is [[https://github.com/alf/ob-restclient.el][ob-restclient]], that may be based on the [[https://github.com/pashky/restclient.el][restclient]] project.
|
||
#+begin_src emacs-lisp
|
||
(use-package ob-restclient
|
||
:init
|
||
(add-to-list 'org-babel-load-languages '(restclient . t)))
|
||
#+end_src
|
||
|
||
And let’s try this:
|
||
#+begin_src restclient :results value replace :wrap src js :var user-agent="my-super-agent"
|
||
GET https://api.github.com/repos/zweifisch/ob-http/languages
|
||
Accept: application/vnd.github.moondragon+json
|
||
User-Agent: ${user-agent}
|
||
#+end_src
|
||
|
||
#+results:
|
||
#+begin_src js
|
||
{
|
||
"Emacs Lisp": 15327,
|
||
"Shell": 139
|
||
}
|
||
#+end_src
|
||
|
||
*** Graphviz
|
||
The [[https://graphviz.org/][graphviz project]] can be written in org blocks, and then rendered as an image:
|
||
#+name: ob-graphviz
|
||
#+begin_src emacs-lisp :tangle no
|
||
(add-to-list 'org-src-lang-modes '("dot" . "graphviz-dot"))
|
||
#+end_src
|
||
|
||
For example:
|
||
#+begin_src dot :file support/ha-org-graphviz-example.png :exports file :results replace file
|
||
digraph G {
|
||
graph [bgcolor=transparent];
|
||
edge [color=white];
|
||
node[style=filled];
|
||
|
||
A -> B -> E;
|
||
A -> D;
|
||
A -> C;
|
||
E -> F;
|
||
E -> H
|
||
D -> F;
|
||
A -> H;
|
||
E -> G;
|
||
}
|
||
#+end_src
|
||
|
||
#+attr_org: :width 400px
|
||
#+results:
|
||
[[file:support/ha-org-graphviz-example.png]]
|
||
*** PlantUML
|
||
Need to install and configure Emacs to work with [[https://plantuml.com/][PlantUML]]. Granted, this is easier now that [[http://orgmode.org/worg/org-contrib/babel][Org-Babel]] natively supports [[http://eschulte.github.io/babel-dev/DONE-integrate-plantuml-support.html][blocks of plantuml code]]. First, [[https://plantuml.com/download][download the Jar]].
|
||
|
||
#+begin_src sh
|
||
curl -o ~/bin/plantuml.jar https://github.com/plantuml/plantuml/releases/download/v1.2022.4/plantuml-1.2022.4.jar
|
||
#+end_src
|
||
|
||
After installing the [[https://github.com/skuro/plantuml-mode][plantuml-mode]], we need to reference the location:
|
||
#+begin_src emacs-lisp
|
||
(use-package plantuml-mode
|
||
:straight (:host github :repo "skuro/plantuml-mode")
|
||
:init
|
||
(setq org-plantuml-jar-path (expand-file-name "~/bin/plantuml.jar")))
|
||
#+end_src
|
||
|
||
With some [[file:snippets/org-mode/plantuml][YASnippets]], I have =<p= to start a general diagram, and afterwards (while still in the org-mode file), type one of the following to expand as an example:
|
||
- =activity= :: https://plantuml.com/activity-diagram-betastart
|
||
- =component= :: https://plantuml.com/component-diagram
|
||
- =deployment= :: https://plantuml.com/deployment-diagram
|
||
- =object= :: https://plantuml.com/object-diagram
|
||
- =sequence= :: https://plantuml.com/sequence-diagram
|
||
- =state= :: https://plantuml.com/state-diagram
|
||
- =timing= :: https://plantuml.com/timing-diagram
|
||
- =use-case= :: https://plantuml.com/use-case-diagram
|
||
|
||
You may be wondering how such trivial terms can be used as expansions in an org file. Well, the trick is that each snippets has a =condition= that calls the following predicate function, that make the snippets context aware:
|
||
|
||
#+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
|
||
|
||
Here is a sequence diagram example to show how is looks/works:
|
||
#+begin_src plantuml :file ha-org-plantuml-example.png :exports file :results file
|
||
@startuml
|
||
!include https://raw.githubusercontent.com/ptrkcsk/one-dark-plantuml-theme/v1.0.0/theme.puml
|
||
' See details at https://plantuml.com/sequence-diagram
|
||
Alice -> Bob: Authentication Request
|
||
Bob --> Alice: Authentication Response
|
||
|
||
Alice -> Bob: Another authentication Request
|
||
Alice <-- Bob: Another authentication Response
|
||
@enduml
|
||
#+end_src
|
||
|
||
#+attr_org: :width 800px
|
||
[[file:ha-org-plantuml-example.png]]
|
||
*** Pikchr
|
||
No, not Pikachu, but close. The [[https://pikchr.org/home/doc/trunk/homepage.md][Pikchr project]] is similar to Graphviz and Plantuml, but makes the boxes more positional and really allows one to place things more precisely. Yet another steep learning curve.
|
||
|
||
Not sure if anyone has made a /package/, so we need to download and build locally:
|
||
#+begin_src sh :dir ~/bin
|
||
curl -o ~/bin/pikchr.c https://pikchr.org/home/raw/9aac00a46506e993db45b740f7a7957f8f381b37001e196199dfc25642c44f06?at=pikchr.c
|
||
# gcc -c pikchr.c # to build the Pikchr library
|
||
gcc -DPIKCHR_SHELL -o ~/bin/pikchr ~/bin/pikchr.c -lm # to build the pikchr command-line tool
|
||
#+end_src
|
||
|
||
Of course, since we are dealing with Emacs, any good idea will be assimilated. Johann Klähn created [[https://github.com/kljohann/pikchr-mode][pikchr-mode]]:
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package pikchr-mode
|
||
:straight (:local-repo "~/src/pikchr-mode")
|
||
:custom
|
||
(pikchr-executable "~/bin/pikchr"))
|
||
#+end_src
|
||
|
||
Let’s see this in action:
|
||
#+begin_src pikchr :file ha-org-pikchr-01.svg :results file :exports both
|
||
bgcolor = 0x1d2021
|
||
fgcolor = 0xeeeeee
|
||
line; box "Hello," "World!"; arrow
|
||
#+end_src
|
||
|
||
Results in:
|
||
#+ATTR_HTML: :width 300 :style font-family:Sans,Arial
|
||
[[file:ha-org-pikchr-01.svg]]
|
||
|
||
And this example shows off the syntax colorization:
|
||
#+begin_src pikchr :file ha-org-pikchr-02.svg :results file :exports both
|
||
A: box "head" fit
|
||
B: box "tail" fit
|
||
C: box "something" with .sw at A.nw fit wid dist(A.w, B.e)
|
||
#+end_src
|
||
|
||
For the results:
|
||
#+ATTR_HTML: :width 300 :background white
|
||
[[file:ha-org-pikchr-02.svg]]
|
||
*** Mermaid
|
||
At work, I’ve been integrating [[https://mermaidjs.github.io/][Mermaid]] into our documentation, =foobar=.
|
||
|
||
Assuming we have installed the [[https://github.com/mermaid-js/mermaid-cli][Mermaid CLI]]:
|
||
|
||
#+BEGIN_SRC sh
|
||
npm install -g @mermaid-js/mermaid-cli
|
||
#+END_SRC
|
||
|
||
We edit the code with [[https://github.com/abrochard/mermaid-mode][mermaid-mode]]:
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(use-package mermaid-mode
|
||
:config
|
||
(setq mermaid-mmdc-location "/opt/homebrew/bin/mmdc"))
|
||
#+END_SRC
|
||
|
||
We can make Mermaid diagrams in Emacs Org files using [[https://github.com/arnm/ob-mermaid][ob-mermaid]]:
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(use-package ob-mermaid
|
||
:config
|
||
(setq ob-mermaid-cli-path "/opt/homebrew/bin/mmdc"))
|
||
#+END_SRC
|
||
|
||
#+BEGIN_SRC mermaid :file ha-org-mermaid.png :theme dark :background-color transparent
|
||
sequenceDiagram
|
||
A-->B: Works!
|
||
#+END_SRC
|
||
|
||
[[file:ha-org-mermaid.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:
|
||
|
||
#+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
|
||
** Keybindings
|
||
Global 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))
|
||
|
||
(ha-leader :keymaps 'org-mode-map
|
||
"o h" '("go headings" . consult-org-heading)
|
||
"o e" '("exports" . org-export-dispatch)
|
||
"o L" '("insert link" . org-insert-link)
|
||
"o P" '("set property" . org-set-property)
|
||
"o g" '("set tags" . org-set-tags-command)
|
||
"o t" '("todo" . org-todo)
|
||
"o T" '("list todos" . org-todo-list)
|
||
|
||
"o i" '(:ignore t :which-key "insert")
|
||
"o i i" '("item" . org-insert-item)
|
||
"o i I" '("insert id" . org-id-get-create)
|
||
"o i l" '("link" . org-insert-link)
|
||
"o i d" '("drawer" . org-insert-drawer)
|
||
"o i h" '("heading" . org-insert-heading)
|
||
"o i s" '("subheading" . org-insert-subheading)
|
||
|
||
"o o" '(:ignore t :which-key "toggles")
|
||
"o o h" '("heading" . org-toggle-heading)
|
||
"o o i" '("item" . org-toggle-item)
|
||
"o o x" '("checkbox" . org-toggle-checkbox)
|
||
"o o I" '("images" . org-toggle-inline-images)
|
||
"o o m" '("markup" . (lambda () (interactive)
|
||
(setq org-hide-emphasis-markers (not org-hide-emphasis-markers)) (font-lock-update)))
|
||
|
||
"o /" '("agenda" . consult-org-agenda)
|
||
"o '" '("edit" . org-edit-special)
|
||
"o *" '("C-c *" . org-ctrl-c-star)
|
||
"o +" '("C-c -" . org-ctrl-c-minus)
|
||
|
||
"o c" '(:ignore t :which-key "clocks")
|
||
"o c i" '("clock in" . org-clock-in)
|
||
"o c l" '("clock in last" . org-clock-in-last)
|
||
"o c o" '("clock out" . org-clock-out)
|
||
"o c c" '("cancel" . org-clock-cancel)
|
||
"o c d" '("mark default task" . org-clock-mark-default-task)
|
||
"o c e" '("modify effort" . org-clock-modify-effort-estimate)
|
||
"o c E" '("set effort" . org-set-effort)
|
||
"o c g" '("goto clock" . org-clock-goto)
|
||
"o c r" '("resolve clocks" . org-resolve-clocks)
|
||
"o c R" '("clock report" . org-clock-report)
|
||
"o c t" '("eval range" . org-evaluate-time-range)
|
||
"o c =" '("timestamp up" . org-clock-timestamps-up)
|
||
"o c -" '("timestamp down" . org-clock-timestamps-down)
|
||
|
||
"o d" '(:ignore t :which-key "dates")
|
||
"o d s" '("schedule" . org-schedule)
|
||
"o d d" '("deadline" . org-deadline)
|
||
"o d t" '("timestamp" . org-time-stamp)
|
||
"o d T" '("inactive time" . org-time-stamp-inactive)
|
||
|
||
|
||
"o b" '(:ignore t :which-key "tables")
|
||
"o b -" '("insert hline" . org-table-insert-hline)
|
||
"o b a" '("align" . org-table-align)
|
||
"o b b" '("blank field" . org-table-blank-field)
|
||
"o b c" '("create teable" . org-table-create-or-convert-from-region)
|
||
"o b e" '("edit field" . org-table-edit-field)
|
||
"o b f" '("edit formula" . org-table-edit-formulas)
|
||
"o b h" '("field info" . org-table-field-info)
|
||
"o b s" '("sort lines" . org-table-sort-lines)
|
||
"o b r" '("recalculate" . org-table-recalculate)
|
||
"o b d" '(:ignore t :which-key "delete")
|
||
"o b d c" '("delete column" . org-table-delete-column)
|
||
"o b d r" '("delete row" . org-table-kill-row)
|
||
"o b i" '(:ignore t :which-key "insert")
|
||
"o b i c" '("insert column" . org-table-insert-column)
|
||
"o b i h" '("insert hline" . org-table-insert-hline)
|
||
"o b i r" '("insert row" . org-table-insert-row)
|
||
"o b i H" '("insert hline ↓" . org-table-hline-and-move)
|
||
|
||
"o n" '(:ignore t :which-key "narrow")
|
||
"o n s" '("subtree" . org-narrow-to-subtree)
|
||
"o n S" '("tree -> win" . org-tree-to-indirect-buffer)
|
||
"o n b" '("block" . org-narrow-to-block)
|
||
"o n e" '("element" . org-narrow-to-element)
|
||
"o n w" '("widen" . widen))
|
||
#+end_src
|
||
|
||
Bindings specific to org files:
|
||
#+name: org-keybindings
|
||
#+begin_src emacs-lisp :tangle no
|
||
(when (fboundp 'evil-define-key)
|
||
(evil-define-key '(normal motion operator visual)
|
||
org-mode-map
|
||
"gj" '("next heading" . #'org-forward-heading-same-level)
|
||
"gk" '("prev heading" . #'org-backward-heading-same-level)
|
||
"gb" '("next block" . #'org-next-block)
|
||
"gB" '("prev block" . #'org-previous-block)))
|
||
#+end_src
|
||
* Supporting Packages
|
||
** Exporters
|
||
Limit the number of exporters to the ones that I would use:
|
||
#+name: ox-exporters
|
||
#+begin_src emacs-lisp
|
||
(setq org-export-backends '(ascii html md texinfo 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-leader :keymaps 'org-mode-map
|
||
"o 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
|
||
|
||
*** HTML Style
|
||
|
||
I’m not afraid of HTML, but I like the idea of doing my HTML work in a Lisp-like way using the [[https://github.com/tonyaldon/jack][jack-html project]]:
|
||
#+begin_src emacs-lisp
|
||
(use-package jack
|
||
:straight (:host github :repo "tonyaldon/jack")
|
||
:commands (jack-html))
|
||
#+end_src
|
||
|
||
So the Lisp code:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(jack-html '(:p "Hello there"))
|
||
#+end_src
|
||
|
||
Returns the string:
|
||
#+begin_example
|
||
<p>Hello there</p>
|
||
#+end_example
|
||
|
||
Splitting out HTML snippets is often a way that I can transfer org-formatted content to other applications.
|
||
|
||
#+name: html-exporting
|
||
#+begin_src emacs-lisp
|
||
(use-package jack
|
||
:after org
|
||
:config
|
||
(setq org-html-head-extra
|
||
(jack-html `((:link (@ :rel "stylesheet"
|
||
:type "text/css"
|
||
:href "https://fonts.googleapis.com/css2?family=Literata:ital,wght@0,300;0,600;1,300;1,600&display=swap"))
|
||
(:link (@ :rel "stylesheet"
|
||
:type "text/css"
|
||
:href "https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,300;0,600;1,300;1,600&display=swap"))
|
||
(:style ,(string-join '(
|
||
"body { font-family: 'Literata', sans-serif; color: #333; }"
|
||
"h1,h2,h3,h4,h5 { font-family: 'Overpass', sans-serif; color: #333; }"
|
||
"code { color: steelblue }"
|
||
"pre { background-color: #eee; border-color: #aaa; }"
|
||
"a { text-decoration-style: dotted }"
|
||
"@media (prefers-color-scheme: dark) {"
|
||
" body { background-color: #1d1f21; color: white; }"
|
||
" h1,h2,h3,h4,h5 { color: #fcca1b; }"
|
||
" code { color: lightsteelblue; }"
|
||
" pre { background-color: black; border-color: #777; }"
|
||
" a:link { color: lightblue }"
|
||
" a:visited { color: violet }"
|
||
"}")
|
||
hard-newline))))))
|
||
#+end_src
|
||
** Focused Work
|
||
:LOGBOOK:
|
||
CLOCK: [2022-02-11 Fri 11:05]--[2022-02-11 Fri 11:21] => 0:16
|
||
:END:
|
||
I've been working on my own [[http://www.howardism.org/Technical/Emacs/focused-work.html][approach to focused work]],
|
||
|
||
#+begin_src emacs-lisp
|
||
(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))
|
||
:bind
|
||
(("<f12>" . ha-focus-begin)
|
||
("S-<f12>" . ha-focus-interrupt)
|
||
("s-<f12>" . ha-focus-timer-left)))
|
||
#+end_src
|
||
** Spell Checking
|
||
Let's hook some spell-checking into org files, and actually all text files. I’m making this particularly delicious.
|
||
|
||
*** abbrev
|
||
First, we turn on =abbrev-mode=. While this package comes with Emacs, check out [[https://masteringemacs.org/article/correcting-typos-misspellings-abbrev][Mickey Petersen's overview]] of using this package for auto-correcting typos.
|
||
|
||
#+begin_src emacs-lisp
|
||
(setq-default abbrev-mode t)
|
||
#+end_src
|
||
|
||
In general, /fill/ the list, by moving the point to the /end/ of some word, and type ~C-x a g~ (or, in /normal state/, type ~SPC x d~):
|
||
|
||
#+begin_src emacs-lisp
|
||
(ha-leader "x d" '("add abbrev" . kadd-global-abbrev))
|
||
#+end_src
|
||
|
||
The idea is that you can correct a typo /and remember/ it. Perhaps calling [[help:edit-abbrevs][edit-abbrevs]] to making any fixes to that list.
|
||
|
||
*** jinx
|
||
Once upon a time, I used [[https://www.emacswiki.org/emacs/FlySpell][flyspell]] mode to highlight the misspelled words, and the venerable [[https://www.emacswiki.org/emacs/InteractiveSpell][ispell]] for correcting. To be able to correct spelling mistakes /from a distance/, without navigation, I wrote a function that took advantage of Evil’s [[help:evil-prev-flyspell-error][evil-prev-flyspell-error]] to jump back to the last spelling mistake.
|
||
|
||
Now, I’m using [[https://github.com/minad/jinx][jinx]], as it is the /complete basket/. It spellchecks based on the fontlock face and uses an external [[https://github.com/AbiWord/enchant][enchant program]] (to make spell-checking fast and asynchronous). Like =flymake=, Jinx does on-the-fly spellchecking of code comments and strings.
|
||
|
||
I keep =jinx-correct= bound to ~C-;~ à la flyspell because it is so darn helpful. Supports checking documents with mixed languages.
|
||
|
||
Requires the =libenchant= from the [[https://abiword.github.io/enchant/][Enchant project]], so on MacOS, I install it via:
|
||
|
||
#+begin_src sh
|
||
brew install enchant
|
||
#+end_src
|
||
|
||
And on Linux:
|
||
|
||
#+begin_src sh
|
||
sudo apt install libenchant-2-dev
|
||
#+end_src
|
||
|
||
And the Emacs interface to that:
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package jinx
|
||
:hook (emacs-startup . global-jinx-mode)
|
||
:bind (("M-$" . jinx-correct-nearest)
|
||
("s-;" . jinx-correct-nearest))
|
||
:general
|
||
(:states '(normal insert) :keymaps 'text-mode-map
|
||
"M-s M-s" 'jinx-correct)
|
||
:config
|
||
(ha-leader
|
||
"s i" '("spellcheck buffer" . jinx-correct-all)
|
||
"S b" '("spellcheck buffer" . jinx-correct-all)))
|
||
#+end_src
|
||
|
||
Jinx works really good, as the mini-buffer allows you to use letters to filter the choice, and numbers (or Return) to select the choice. Selecting ~@~ adds the word to your personal dictionary, and ~*~ adds it to the /local words/ for the file (search for =jinx-local-words=). Also, it appears that calling =jinx-correct= goes back to the first incorrect spelling, letting you correct it, and then pops the point back. That is pretty slick.
|
||
|
||
It also, supposedly, fixes =camelCase= words. This doesn’t work in a text document. I appreciate that in org-mode files, text surrounded with = characters are no longer marked for misspellings.
|
||
|
||
Since this auto-correction needs to happen in /insert/ mode, I have bound a few keys, including ~CMD-s~ and ~M-s~ (twice) to fixing this spelling mistake, and jumping back to where I am.
|
||
*** Thesaurus
|
||
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 ("s-t" . powerthesaurus-lookup-dwim)
|
||
:config
|
||
(ha-leader
|
||
"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, and key-connections work well with ~M-T~ (notice the Shift), but to jump to specifics, we use a leader.
|
||
|
||
*** Definitions
|
||
Since the /definitions/ do not work, so let's use the [[https://github.com/abo-abo/define-word][define-word]] project:
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package define-word
|
||
;; :bind ("s-d" . define-word-at-point)
|
||
:config
|
||
(ha-leader :keymaps 'text-mode-map
|
||
"S D" '("define word" . define-word)))
|
||
#+end_src
|
||
|
||
After my enamoring of Noah Webster’s 1913 dictionary (originally due to reading [[https://janusworx.com/blog/thank-god-for-noah/][this essay]] by Mario Jason Braganza who referred to James Somers’ original [[https://jsomers.net/blog/dictionary][2014 blog entry]]), I easily followed the instructions from [[https://github.com/ponychicken/WebsterParser][WebsterParser]], a Github project, with the dictionary:
|
||
1. Download [[https://github.com/ponychicken/WebsterParser/releases/latest/download/websters-1913.dictionary.zip][the dictionary]] file.
|
||
2. Unzip the archive … have a *Finder* window open to the =.dictionary= file.
|
||
3. Open the =Dictionary.app= program.
|
||
4. Select the menu entry, *Dictionary –> File –> Open Dictionaries Folder*
|
||
5. Drag the downloaded =Websters-1913.dictionary= file into the folder
|
||
6. Select the menu entry, *Dictionary –> Dictionary –> Preferences*
|
||
7. Check the now last dictionary in the list
|
||
|
||
If you want to always see Webster’s results by default, go to the Dictionary app’s preferences and drag Webster’s to the top of the list.
|
||
|
||
Now that I’m mostly on version 28 and above of Emacs, we can take advantage of [[help:dictionary-search][dictionary-search]] for looking up dictionaries online, and out of all the word definitions packages for Emacs, this looks the best and is easiest to read:
|
||
#+begin_src emacs-lisp
|
||
(setq dictionary-server "dict.org")
|
||
|
||
(ha-leader "S d" '("define this" . dictionary-search))
|
||
#+end_src
|
||
Once in the dictionary buffer, acquiesce these keybindings:
|
||
- ~q~ close the dictionary buffer
|
||
- ~s~ ask for a new word to search
|
||
- ~d~ search the word at point
|
||
|
||
Also note that the dictionary has links to other pages, so ~n~ and ~TAB~ jumps to the next link and ~RET~ opens that link.
|
||
** Grammar and Prose Linting
|
||
Flagging cliches, weak phrasing and other poor grammar choices.
|
||
|
||
We are trying a lot of checkers, so we need to /chain/ them with a call to =flycheck-add-next-checker=:
|
||
|
||
=write-good= —> =proselint= —> =textlint= —> =languagetool=?
|
||
*** Writegood
|
||
The [[https://github.com/bnbeckwith/writegood-mode][writegood-mode]] is effective at highlighting passive and weasel words.
|
||
#+begin_src emacs-lisp
|
||
(use-package writegood-mode)
|
||
#+end_src
|
||
And it reports obnoxious messages.
|
||
|
||
Hrm::hook ((org-mode . writegood-mode)
|
||
(gfm-mode . writegood-mode)
|
||
(markdown-mode) . writegood-mode)
|
||
|
||
We install the =write-good= NPM:
|
||
#+begin_src shell
|
||
npm install -g write-good
|
||
#+end_src
|
||
|
||
And check that the following works:
|
||
#+begin_src sh :results output
|
||
write-good --text="So it is what it is."
|
||
#+end_src
|
||
|
||
Now, let’s connect it to flycheck:
|
||
#+begin_src emacs-lisp
|
||
(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 'write-good))
|
||
#+end_src
|
||
*** Proselint
|
||
With overlapping goals to =write-good=, the [[https://github.com/amperser/proselint/][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/.
|
||
#+begin_src sh
|
||
brew install proselint
|
||
#+end_src
|
||
And on Linux:
|
||
#+BEGIN_SRC sh
|
||
sudo apt install python3-proselint
|
||
#+END_SRC
|
||
|
||
Next, create a configuration file, =~/.config/proselint/config= file, to turn on/off checks:
|
||
#+begin_src js :tangle ~/.config/proselint/config.json :mkdirp yes
|
||
{
|
||
"checks": {
|
||
"typography.diacritical_marks": false,
|
||
"annotations.misc": false,
|
||
"consistency.spacing": false
|
||
}
|
||
}
|
||
#+end_src
|
||
|
||
And tell [[https://www.flycheck.org/][flycheck]] to use this:
|
||
#+begin_src emacs-lisp
|
||
(use-package flycheck
|
||
:config
|
||
(add-to-list 'flycheck-checkers 'proselint)
|
||
;; And create the chain of checkers so that both work:
|
||
(flycheck-add-next-checker 'write-good 'proselint))
|
||
#+end_src
|
||
*** Textlint
|
||
The [[https://textlint.github.io/][textlint]] project comes with =flycheck=, as long as there is an executable:
|
||
#+begin_src sh :results silent
|
||
sudo npm install -g textlint
|
||
# And all the rules
|
||
sudo npm install -g textlint-rule-alex
|
||
sudo npm install -g textlint-rule-diacritics
|
||
sudo npm install -g textlint-rule-en-max-word-count
|
||
sudo npm install -g textlint-rule-max-comma
|
||
sudo npm install -g textlint-rule-no-start-duplicated-conjunction
|
||
sudo npm install -g textlint-rule-period-in-list-item
|
||
sudo npm install -g textlint-rule-stop-words
|
||
sudo npm install -g textlint-rule-terminology
|
||
sudo npm install -g textlint-rule-unexpanded-acronym
|
||
#+end_src
|
||
I create a configuration file in my home directory:
|
||
#+begin_src js :tangle ~/.textlintrc
|
||
{
|
||
"filters": {},
|
||
"rules": {
|
||
"abbr-within-parentheses": false,
|
||
"alex": true,
|
||
"common-misspellings": false,
|
||
"diacritics": true,
|
||
"en-max-word-count": true,
|
||
"max-comma": true,
|
||
"no-start-duplicated-conjunction": true,
|
||
"period-in-list-item": true,
|
||
"stop-words": true,
|
||
"terminology": true,
|
||
"unexpanded-acronym": true,
|
||
"write-good": false
|
||
}
|
||
}
|
||
#+end_src
|
||
|
||
Add =textlint= to the /chain/ for Org files:
|
||
#+begin_src emacs-lisp
|
||
(use-package flycheck
|
||
:config
|
||
(setq flycheck-textlint-config (format "%s/.textlintrc" (getenv "HOME")))
|
||
(flycheck-add-next-checker 'proselint 'textlint t))
|
||
#+end_src
|
||
|
||
*** Language Tool
|
||
Another flycheck feature is to use [[http://languagetool.org][LanguageTool]] connection to [[https://github.com/emacs-languagetool/flycheck-languagetool][flycheck-languagetool]]:
|
||
|
||
#+BEGIN_SRC emacs-lisp :tangle no
|
||
(use-package flycheck-languagetool
|
||
:ensure t
|
||
:hook (text-mode . flycheck-languagetool-setup)
|
||
:init
|
||
(setq flycheck-languagetool-server-jar (expand-file-name "~/other/LanguageTool/languagetool-server.jar")
|
||
flycheck-languagetool-server-args (expand-file-name "~/.config/languagetool/config.properties")))
|
||
#+END_SRC
|
||
|
||
And connect it to the chain:
|
||
|
||
#+BEGIN_SRC emacs-lisp :tangle no
|
||
(use-package flycheck
|
||
:config (flycheck-add-next-checker 'textlint 'languagetool t)
|
||
|
||
;; May have to specify a Java on one of my Mac machines:
|
||
(when (file-exists-p "/opt/homebrew/opt/openjdk")
|
||
(add-to-list 'exec-path "/opt/homebrew/opt/openjdk/bin")))
|
||
#+END_SRC
|
||
|
||
This check complains about whitespace in Org files (duh), so let’s create a configuration file where we can disable that rule (and any other we can require):
|
||
|
||
#+BEGIN_SRC conf :tangle ~/.config/languagetool/config.properties :mkdirp ~/.config/languagetool
|
||
disabledRuleIds: WHITESPACE
|
||
#+END_SRC
|
||
|
||
Gotta admit that Language Tool doesn’t seem to help much. $ 100 ?
|
||
** Perfect Sentence
|
||
Chris Malorana’s [[https://www.youtube.com/watch?v=E-yk_V5TnNU][video tutorial]] demonstrates the ability to extrude a single sentence into another buffer, edit different versions of that sentence, and replace one version into the original buffer. For instance, how org-mode edits blocks.
|
||
|
||
Malorana based this idea on Jordan Peterson's writing app, [[https://essay.app/guide][Essay]]. Thought I might work on it, but I want my version more resilient and not as dependent on the context.
|
||
|
||
When we create a new buffer, we set the following /buffer-local/ variables, so we know where to return:
|
||
|
||
#+begin_src emacs-lisp
|
||
(defvar-local ha-sentence-buffer nil
|
||
"The name of the buffer to return when completed.")
|
||
(defvar-local ha-sentence-begin nil
|
||
"The beginning position in the original buffer to replace text.")
|
||
(defvar-local ha-sentence-end nil
|
||
"The ending position in the original buffer to replace text.")
|
||
#+end_src
|
||
|
||
My first thought is how to select the sentence. Sure, sometimes that should be the /region/, but we can also use the help:bounds-of-thing-at-point to define the start and the end of the current sentence:
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun ha-sentence--select-region (type-of-thing &optional start end)
|
||
"Return a tuple of the start and end of the selected sentence."
|
||
(cond
|
||
((region-active-p) (cons (region-beginning) (region-end)))
|
||
((and start end) (cons start end))
|
||
(t (bounds-of-thing-at-point type-of-thing))))
|
||
#+end_src
|
||
|
||
In the original buffer, we want to edit a /sentence/, but in the editing buffer, a single sentence may expand to more than one, so we need to change whether we select a ='sentence= or a ='defun= (for a paragraph).
|
||
|
||
With this function, we can call [[help:cl-destructuring-bind][destructuring-bind]] to define what section we want to edit by assigning the =start= and =end= values. Now we create another buffer window, set the local variables, and insert the region/sentence we requested:
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun ha-sentence-break (&optional start end)
|
||
"Break a sentence out and work it in a new buffer.
|
||
We base the chosen sentence chosen on the location of a point,
|
||
or the active region."
|
||
(interactive)
|
||
(cl-destructuring-bind (start . end)
|
||
(ha-sentence--select-region 'sentence start end)
|
||
(let ((orig-mode major-mode)
|
||
(orig-buffer (current-buffer))
|
||
(orig-sentence (buffer-substring-no-properties start end)))
|
||
|
||
(switch-to-buffer-other-window "**sentence-breakout**")
|
||
(funcall orig-mode)
|
||
(ha-sentence-buffer-mode)
|
||
|
||
;; Store some breadcrumbs so we can return where we left off:
|
||
(setq-local ha-sentence-buffer orig-buffer
|
||
ha-sentence-begin start
|
||
ha-sentence-end end)
|
||
(erase-buffer)
|
||
(insert orig-sentence)
|
||
|
||
;; Because we might want to duplicate the sentence in the
|
||
;; buffer, let's put it on the kill ring:
|
||
(kill-new orig-sentence))))
|
||
#+end_src
|
||
|
||
With the new buffer displayed, we show the sentence to edit, with the idea to write different versions of that sentence. When we have the version we like, we hit ~C-c C-c~ which calls [[help:ha-sentence-choose][ha-sentence-choose]] /to choose/ the version that replaces the old one. But what if, during the editing process, we create more than one sentence?
|
||
|
||
In that case, we need to select the text before hitting the ~C-c C-c~ sequence. The buffer-local variables tell us which buffer to return, and what text to replace.
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun ha-sentence-choose (&optional start end)
|
||
"Choose a sentence and go back to the other window."
|
||
(interactive)
|
||
|
||
;; By default, our "region" is a paragraph using 'defun symbol of `thing-at-point'
|
||
;; It doesn't work on the last sentence if it doesn't include a
|
||
;; newline, so hackily, we insert one.
|
||
(save-excursion
|
||
(goto-char (point-max))
|
||
(insert "\n"))
|
||
|
||
(cl-destructuring-bind (start . end) (ha-sentence--select-region 'defun start end)
|
||
(let ((chosen-sentence (buffer-substring-no-properties start end))
|
||
(orig-buffer ha-sentence-buffer)
|
||
(orig-start ha-sentence-begin)
|
||
(orig-end ha-sentence-end))
|
||
|
||
(kill-buffer-and-window)
|
||
(switch-to-buffer orig-buffer)
|
||
(delete-region orig-start orig-end) ; Or call `kill-region' to put on clipboard?
|
||
(insert chosen-sentence))))
|
||
#+end_src
|
||
The [[help:kill-region][kill-region]] function takes the original text and places it on the [[help:kill-ring][kill-ring]] (the clipboard). But since we already copied that when we created the buffer, we call [[help:delete-region][delete-region]] instead. Especially since if we felt like we made a mistake, we could just undo the changes.
|
||
|
||
With my limited experience, I seldom enter completely difference sentences. Instead, I want to /copy/ the sentence and work on that. Let’s make a function to duplicate it.
|
||
#+begin_src emacs-lisp
|
||
(defun ha-sentence-duplicate ()
|
||
(interactive)
|
||
(let ((current (thing-at-point 'defun)))
|
||
(goto-char (point-max))
|
||
(insert "\n\n")
|
||
(let ((starting-point (point)))
|
||
(insert current)
|
||
(goto-char starting-point))))
|
||
#+end_src
|
||
|
||
When creating this new editing buffer, we need keybindings that exist only for this buffer, in other words, a [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Defining-Minor-Modes.html][minor mode]]:
|
||
#+begin_src emacs-lisp
|
||
(defvar ha-sentence-buffer-mode-map (make-sparse-keymap) "Keymap for `my-mode'.")
|
||
(define-key ha-sentence-buffer-mode-map (kbd "C-c C-c") #'ha-sentence-choose)
|
||
(define-key ha-sentence-buffer-mode-map (kbd "C-c C-k") #'kill-buffer-and-window)
|
||
(define-key ha-sentence-buffer-mode-map (kbd "C-c C-d") #'ha-sentence-duplicate)
|
||
|
||
(define-minor-mode ha-sentence-buffer-mode
|
||
"Toggle the Perfect Sentence mode.
|
||
Interactively with no argument, this command toggles the mode.
|
||
A positive prefix argument enables the mode, any other prefix
|
||
argument disables it. From Lisp, argument omitted or nil enables
|
||
the mode, `toggle' toggles the state.
|
||
|
||
When this mode is enabled, `C-c C-c' calls `ha-sentence-choose',
|
||
and `C-c C-k' cancels and buries the buffer."
|
||
;; :interactive nil
|
||
:init-value nil
|
||
:lighter " PS"
|
||
:keymap ha-sentence-buffer-mode-map)
|
||
#+end_src
|
||
|
||
Let’s bind a couple key sequences for Emacs mode:
|
||
#+begin_src emacs-lisp
|
||
(global-set-key (kbd "M-s b") 'ha-sentence-break)
|
||
#+end_src
|
||
I am making this global, as it may be nice in both org-mode and programming modes.
|
||
|
||
And something else while in Evil mode:
|
||
#+begin_src emacs-lisp
|
||
(ha-leader "x b" '("edit sentence" . ha-sentence-break))
|
||
#+end_src
|
||
Perhaps he might get around to turning [[https://git.chrismaiorana.com/?p=sentinel.git;a=blob;f=sentin.el;h=2738eff6ac2b0877576bafe88878683a7eff3125;hb=refs/heads/master][his code]] into a package. Features needed include:
|
||
- Adding an overlay to the original text, help:org-src--make-source-overlay
|
||
|
||
** Distraction-Free Writing
|
||
[[https://christopherfin.com/writing/emacs-writing.html][Christopher Fin's essay]] inspired me to clean my writing room.
|
||
*** Write-room
|
||
For a complete focused, /distraction-free/ environment, for writing or 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)
|
||
:init
|
||
(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
|
||
*** Olivetti
|
||
The [[https://github.com/rnkn/olivetti][olivetti project]] sets wide margins and centers the text. It isn’t better than Writeroom, but, it works well with Logos (below).
|
||
#+begin_src emacs-lisp
|
||
(use-package olivetti
|
||
:init
|
||
(setq-default olivetti-body-width 100)
|
||
(ha-leader "t O" '("olivetti" . olivetti-mode))
|
||
:bind (:map olivetti-mode-map
|
||
("C-M-<" . olivetti-shrink)
|
||
("C-M->" . olivetti-expand)
|
||
("C-M-=" . olivetti-set-width)))
|
||
#+end_src
|
||
*** Logos
|
||
Trying out [[https://protesilaos.com/][Protesilaos Stavrou]]’s [[https://protesilaos.com/emacs/logos][logos project]] as a replacement for [[https://github.com/joostkremers/writeroom-mode][Writeroom-mode]]:
|
||
#+begin_src emacs-lisp
|
||
(use-package logos
|
||
:straight (:host gitlab :repo "protesilaos/logos")
|
||
:init
|
||
(setq logos-outlines-are-pages t
|
||
logos-outline-regexp-alist
|
||
`((emacs-lisp-mode . "^;;;+ ")
|
||
(org-mode . "^\\*+ +")
|
||
(t . ,(or outline-regexp logos--page-delimiter))))
|
||
|
||
;; These apply when enabling `logos-focus-mode' as buffer-local.
|
||
(setq-default logos-hide-mode-line t
|
||
logos-scroll-lock nil
|
||
logos-indicate-buffer-boundaries nil
|
||
logos-buffer-read-only nil
|
||
logos-olivetti t)
|
||
:config
|
||
(ha-leader "t L" '("logos" . logos-focus-mode))
|
||
(define-key global-map [remap narrow-to-region] #'logos-narrow-dwim)
|
||
|
||
:general
|
||
(:states 'normal
|
||
"g [" '("back page" . logos-backward-page-dwim)
|
||
"g ]" '("next page" . logos-forward-page-dwim)))
|
||
#+end_src
|
||
* Technical Artifacts :noexport:
|
||
Let's provide a name, to allow =require= to work:
|
||
#+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:t todo:nil tasks:nil tags:nil date:nil
|
||
#+options: skip:nil author:nil email:nil creator:nil timestamp:nil
|
||
#+infojs_opt: view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
|
||
|
||
# Local Variables:
|
||
# jinx-local-words: "Braganza Graphviz Malorana’s Proselint Somers Textlint Writegood flycheck flyspell fontlock"
|
||
# End:
|