hamacs/ha-org-word-processor.org
2022-04-16 22:52:43 -07:00

25 KiB
Raw Blame History

Org As A Word Processor

A literate programming file for making Org file more readable.

Introduction

I like having org-mode files look more like a word processor than having it look like programming code. But that is just me.

General Org Settings

Since I use ellipsis in my writing… to change how org renders a collapsed heading.

  (setq org-pretty-entities t
        org-ellipsis "⤵"     ; …, ➡, ⚡, ▼, ↴, , ∞, ⬎, ⤷, ⤵
        org-agenda-breadcrumbs-separator " ❱ "
        org-catch-invisible-edits 'show-and-error
        org-special-ctrl-a/e t     ; Note: Need to get this working with Evil!
        org-src-fontify-natively t ; Pretty code blocks
        org-hide-emphasis-markers t)

Oh, and as I indent lists, they should change the bulleting in a particular sequence. If I begin with an * asterisk, I walk down the chain, but with the dashed bullets (my default choice), I just stay with dashed bullets. Numeric bullets should cycle:

  (setq org-list-demote-modify-bullet '(("*" . "+") ("+" . "-") ("-" . "-")
                                        ("1." . "a.") ("a." . "1.")))

The org-indent-indentation-per-level, which defaults to 2 doesnt really work well with variable-width fonts, so lets make the spaces at the beginning of the line fixed:

  (use-package org
    :custom-face (org-indent ((t (:inherit fixed-pitch)))))

Typographic Quotes

According to Artur Malabarba of Endless Parenthesis blog, I type either a straight single or double quote, ", Emacs actually inserts Unicode rounded quotes, like “this”. This idea isnt how a file is displayed, but actually how the file is made. Time will tell if this idea works with my auxiliary functions on my phone, like Orgzly and Orgro.

Stealing his function, and updating it a bit, so that “quotes” just work to insert rounded quotation marks:

  (defun ha--insert-round-quotes (opening closing)
    "Insert rounded quotes in prose but not inside code.
  The OPENING and CLOSING variables are either or .

  Rules:

   • At beginning of line or after a space (in other words,
     single-quoting a word or phrase), insert OPENING and CLOSING
     string, and leave point between them.

   • At beginning of existing word, insert OPENING, as the assumption
     is to go somewhere else to insert the CLOSING character.

   • If looking at the CLOSING character, move past it.

   • Otherwise, insert an CLOSING character, as this is probably
     finishing the quotation.

  Inside a code-block, just call `self-insert-command'."
    (cl-flet ((insert-pair ()
                (insert opening) (insert closing) (forward-char -1)))

      ;; Don't do anything special in code blocks:
      (if (and (derived-mode-p 'org-mode)
               (org-in-block-p '("src" "latex" "html" "example")))
          (call-interactively #'self-insert-command)

        ;; Define some regular expressions to make the `cond' clearer:
        (let ((existing-word (rx word-start))
              (starting-anew (rx (or bol space)))
              (existing-endq
               (rx-to-string `(seq (or "'" "\"" ,opening ,closing)
                                   (optional (any "=_/*"))))))
          (cond
           ((looking-at   existing-word) (insert opening))
           ((looking-back starting-anew) (insert-pair))
           ((looking-at   existing-endq) (goto-char (match-end 0)))
           (t                            (insert closing)))))))

Now we can take advantage of the abstraction for “double quotes”:

  (defun ha-round-quotes ()
    "Insert “” and leave point in the middle.
  Inside a code-block, just call `self-insert-command'.
  See `ha--insert-round-quotes' for rule details."
    (interactive)
    (ha--insert-round-quotes "“" "”"))

  (define-key org-mode-map "\"" #'ha-round-quotes)

And something similar for single quotes:

  (defun ha-apostrophe ()
    "Insert  and leave point in the middle.
  Inside a code-block, just call `self-insert-command'.
  See `ha--insert-round-quotes' for rule details."
    (interactive)
    (ha--insert-round-quotes "" ""))

  (define-key org-mode-map "'" #'ha-apostrophe)

Note: I still need to worry about how quotes affect spell checking.

What would be nice, is that if I end quotes using the functions above, that if I immediately delete, I delete both pairs.

  (defun ha-delete-quote-pairs (&optional N)
    "If positioned between two quote symbols, delete the last.
  Used as advice to `org-delete-backward-char' function."
    (when (and (looking-at (rx (any "\"" "'" "`" "”" "")))
             (looking-back (rx (any "\"" "'" "`" "“" ""))))
      (org-delete-char N)))

  (advice-add #'org-delete-backward-char :before #'ha-delete-quote-pairs)

Can we do the same with ellipses?

  (defun ha-insert-dot-or-ellipsis ()
    "Insert a `.' unless two have already be inserted.
  In this case, insert an ellipsis instead."
    (interactive)
    (if (and (derived-mode-p 'org-mode)
             (org-in-block-p '("src" "latex" "html" "example")))
        (call-interactively #'self-insert-command)
      (cond
       ((looking-back (rx "…"))   (delete-backward-char 1)
                                  (insert "⋯"))
       ((looking-back (rx ".."))  (delete-backward-char 2)
                                  (insert "…"))
       (t                         (insert ".")))))

  (define-key org-mode-map "." #'ha-insert-dot-or-ellipsis)

Now Im getting obsessive with elongating dashes:

  (defun ha-insert-long-dash ()
    "Insert a `.' unless two have already be inserted.
  In this case, insert an ellipsis instead."
    (interactive)
    (if (and (derived-mode-p 'org-mode)
             (org-in-block-p '("src" "latex" "html" "example")))
        (call-interactively #'self-insert-command)
      (cond
       ((looking-back (rx "-"))  (delete-backward-char 1)
        (insert "—"))
       ((looking-back (rx "—"))  (delete-backward-char 1)
        (insert "⸺"))
       ((looking-back (rx "⸺"))  (delete-backward-char 1)
        (insert "⸻"))
       ((looking-back (rx "⸻"))  (delete-backward-char 1)
        (insert "------------------------------------------------------------"))
       (t                        (insert "-")))))

  (define-key org-mode-map "-" #'ha-insert-long-dash)

Org Beautify

I really want to use the Org Beautify package, but it overrides my darker themes (and all I really want is headlines to behave).

First step is to make all Org header levels to use the variable font, and be the same color as the default text:

  (when window-system
    (let ((default-color (face-attribute 'default :foreground)))
      (dolist (face '(org-level-1 org-level-2 org-level-3 org-level-4 org-level-5 org-level-6 org-level-7 org-level-8))
        (set-face-attribute face nil :height 1.1
                            :foreground default-color :weight 'bold :font ha-variable-font))))

Next, we just need to change the header sizes:

  (when window-system
    (set-face-attribute 'org-level-1 nil :height 2.2)
    (set-face-attribute 'org-level-2 nil :height 1.8)
    (set-face-attribute 'org-level-3 nil :height 1.4)
    (set-face-attribute 'org-level-4 nil :height 1.2))

While we are at it, lets make sure the code blocks are using my fixed with font:

  (when window-system
    (dolist (face '(org-block org-code org-verbatim org-table org-drawer org-table org-formula
                    org-special-keyword org-property-value org-document-info-keyword))
      (set-face-attribute face nil :inherit 'fixed-pitch :height 0.9)))

  (set-face-attribute 'org-table nil :height 1.0)
  (set-face-attribute 'org-formula nil :height 1.0)

Not sure why the above code removes the color of org-verbatim, so lets make it stand out slightly:

  (set-face-attribute 'org-verbatim nil :foreground "#aaaacc")

And some slight adjustments to the way blocks are displayed:

  (set-face-attribute 'org-block-begin-line nil :background "#282828")
  (set-face-attribute 'org-block nil :height 0.95)
  (set-face-attribute 'org-block-end-line nil :background "#262626")

And decrease the prominence of the property drawers:

  (set-face-attribute 'org-drawer nil :height 0.8)
  (set-face-attribute 'org-property-value nil :height 0.85)
  (set-face-attribute 'org-special-keyword nil :height 0.85)

This process allows us to use variable-pitch mode for all org documents.

  (use-package org
    :hook (org-mode . variable-pitch-mode))

Superstar

Now that headers are noticeable, I have no reason to see a number of asterisks. I would think that this would work:

(setq org-hide-leading-stars t)

But since it doesnt, I need to roll my own. I add a hook to standard Org, and since this is a Lisp-2, I can get away with using the same name as the variable:

(defun org-hide-leading-stars ()
  (let* ((keyword
          `(("^\\(\\*+ \\)\\s-*\\S-" ; Do not hide empty headings!
             (1 (put-text-property (match-beginning 1) (match-end 1) 'invisible t)
                nil)))))
    (font-lock-add-keywords nil keyword)))

  (add-hook 'org-mode-hook 'org-hide-leading-stars)

Once I used the org-bullets package, but we've replaced it with org-superstar-mode, so the following is an improvement, especially with the sub-bullets:

  (use-package org-superstar
    :after org
    :straight (:type git :protocol ssh :host github :repo "integral-dw/org-superstar-mode")
    :hook (org-mode . org-superstar-mode)
    :init
    (setq ; org-superstar-headline-bullets-list '("▶")
          org-superstar-special-todo-items nil
          org-superstar-todo-bullet-alist t
          org-superstar-prettify-item-bullets t
          org-superstar-item-bullet-alist '((42 . "●")   ; *
                                            (43 . "○")   ; +
                                            (45 . "•"))))

If this works, then we get the prettier bulleted list:

  • Top bullets
  • Plus bullets:

    • Apples
    • Oranges
    • Bananas
  • Minus bullets:

    • Dogs
    • Cats
    • Birds

Checkboxes

According to an idea by Huy Trần, (and expanded by the org-modern project), we can prettify the list checkboxes as well:

(defun ha-org-prettify-checkboxes ()
  "Beautify Org Checkbox Symbol"
  (push '("[ ]" . "☐") prettify-symbols-alist)
  (push '("[X]" . "☐✓") prettify-symbols-alist)
  (push '("[-]" . "☐-") prettify-symbols-alist)
  (prettify-symbols-mode))

And now we can attach it to a newly loaded org files:

(add-hook 'org-mode-hook 'ha-org-prettify-checkboxes)

To make it more distinguishable, he also changed the colors:

(defface org-checkbox-done-text
  '((t (:foreground "#71696A" :strike-through t)))
  "Face for the text part of a checked org-mode checkbox.")

(font-lock-add-keywords
 'org-mode
 `(("^[ \t]*\\(?:[-+*]\\|[0-9]+[).]\\)[ \t]+\\(\\(?:\\[@\\(?:start:\\)?[0-9]+\\][ \t]*\\)?\\[\\(?:X\\|\\([0-9]+\\)/\\2\\)\\][^\n]*\n\\)"
    1 'org-checkbox-done-text prepend))
 'append)

SVG-Based Prettiness

While I'm intrigued with Nicolas P. Rougier's notebook project, I really just want to steal their svg-lib project that allows me to create and display various SVG objects, namely tags, progress bars, progress pies and icons. Each object fits nicely in a text buffer ensuring width is an integer multiple of character width.

  (when (image-type-available-p 'svg)
    (use-package svg-lib
      :straight (:type git :protocol ssh :host github :repo "rougier/svg-lib"))

    (use-package svg-tag-mode
      ;; :straight (:type git :protocol ssh :host github :repo "rougier/svg-tag-mode")
      :straight (:type git :protocol ssh :host github :repo "howardabrams/svg-tag-mode")
      :hook ((org-mode . svg-tag-mode)
             (emacs-lisp-mode . svg-tag-mode)
             (python-mode . svg-tag-mode))
      :config
      (setq svg-tag-tags
            `(("NOTE:" .
               ((lambda (tag)
                  (svg-tag-make "NOTE" :face 'org-done
                                :inverse nil :margin 0 :radius 2))))

              ;; ------------------------------------------------------------
              ;; Two definitions of TODO That include the single line message
              ;; ------------------------------------------------------------
              (,(rx word-start "TODO" word-end) .
               ((lambda (tag)
                  (svg-tag-make "TODO" :face 'org-todo
                                :radius 2 :inverse t :margin 0 :padding 0 :crop-right t))))
              (,(rx word-start "TODO"
                    (group (one-or-more space) (zero-or-more not-newline)) line-end) .
                    ((lambda (tag)
                       (svg-tag-make tag :face 'org-todo
                                     :radius 2 :margin 0 :padding 0 :crop-left t))))

              ;; ------------------------------------------------------------
              ;; Two definitions of tag combo, like :TAG:foobar:
              ;; ------------------------------------------------------------
              ;; Where the first is inversed:
              ;; (,(rx (group ":" (one-or-more (any alpha "-")) ":")
              ;;       (one-or-more (any alphanumeric "-" "_")) ":") .
              ;;  ((lambda (tag)
              ;;     (svg-tag-make tag :beg 1 :end -1 :inverse t :margin 0 :crop-right t))))
              ;; And the second is not:
              ;; (,(rx ":" (one-or-more (any alpha "-"))
              ;;       (group ":" (one-or-more (any alphanumeric "-" "_")) ":")) .
              ;;  ((lambda (tag)
              ;;     (svg-tag-make tag :beg 1 :end -1 :margin 0 :crop-left t))))

              ;; ------------------------------------------------------------
              ;; Org-mode :TAGS: shown as a box:
              ;; ------------------------------------------------------------
              (,(rx (or line-start space)
                    (group ":" (one-or-more (any alpha "-")) ":")
                    (or space line-end)) .
                    ((lambda (tag)
                       (svg-tag-make tag :face 'org-drawer :beg 1 :end -1 :margin 0))))

              ;; ------------------------------------------------------------
              ;; Org-mode blocks #+BEGIN_SRC language ... #+END_SRC
              ;; ------------------------------------------------------------
              (,(rx line-start (zero-or-more space) "#+BEGIN_SRC") .
               ((lambda (tag)
                  (svg-tag-make "RUN" :face 'org-block-begin-line
                                :inverse t :margin 0 :crop-right t))
                (lambda () (interactive) (org-ctrl-c-ctrl-c))
                "Run this block of code"))
              (,(rx line-start (zero-or-more space) "#+BEGIN_SRC"
                    (1+ space) (group (one-or-more (any alpha "-"))) word-end) .
                    ((lambda (tag)
                       (svg-tag-make tag :face 'org-block-begin-line
                                     :margin 0 :crop-left t))
                     (lambda () (interactive) (org-ctrl-c-ctrl-c))
                     "Run this block of code"))
              (,(rx line-start (zero-or-more space) "#+END_SRC" word-end) .
               ((lambda (tag)
                  (svg-tag-make "END" :face 'org-block-end-line :inverse t))))
              ("^#\\+RESULTS:" .
               ((lambda (tag)
                  (svg-tag-make "RESULTS" :face 'org-block-end-line :inverse t))))

              (,(rx line-start (zero-or-more space) "#+ATTR" (zero-or-more (any alpha "_")) ":") .
               ((lambda (tag)
                  (svg-tag-make "ATTR" :face 'org-block-begin-line :inverse t))))

              ("^#\\+NAME:"         . ((lambda (tag) (svg-tag-make "NAME" :face 'org-meta-line))))
              ("^#\\+HEADER:"       . ((lambda (tag) (svg-tag-make "HEADER" :face 'org-meta-line))))
              ("^#\\+BEGIN_EXAMPLE" . ((lambda (tag) (svg-tag-make "CODE" :face 'org-block-begin-line))))
              ("^#\\+END_EXAMPLE"   . ((lambda (tag) (svg-tag-make "END" :face 'org-block-end-line))))
              ("^#\\+begin_quote"   . ((lambda (tag) (svg-tag-make "“" :face 'org-block-begin-line))))
              ("^#\\+end_quote"     . ((lambda (tag) (svg-tag-make "”" :face 'org-block-end-line))))

              ;; ------------------------------------------------------------
              ;; Things like #+OPTIONS: and property definitions
              ;; ------------------------------------------------------------
              ("#\\+PROPERTY:" . ((lambda (tag) (svg-tag-make "PROPERTY" :face 'org-meta-line))))
              ("#\\+OPTIONS:"  . ((lambda (tag) (svg-tag-make "OPTIONS" :face 'org-meta-line))))))))

What does do? Here are some examples: TODO Marks comments for tasks (this can be in source files too). NOTE: Highlights comments and other notes. :PROP:tag: are highlighted as two parts of the same tag And :TAG: with colons are highlighted, which include :PROPERTY: drawers. Org-specific #+PROPERTY: entries are highlighted.

  (use-package notebook
    :straight (:type git :protocol ssh :host github :repo "rougier/notebook-mode")
    :after org
    :hook (org-mode . notebook-mode))

Padding

The org-padding project looks places extra space before and after headers and blocks (essentially leading), to create a more word-processor-y experience. Great idea, however, I have spent a lot of extra time entering blank lines before and after my headers and blocks:

(use-package org-padding
  :straight (:type git :protocol ssh :host github :repo "TonCherAmi/org-padding")
  :hook
  (org-mode . org-padding-mode)
  :config
  (setq org-padding-block-begin-line-padding '(0.5 . 0.3)
        org-padding-block-end-line-padding '(0.1 . 0.5)
        org-padding-heading-padding-alist
        '((4.0 . 1.5) (3.0 . 0.5) (3.0 . 0.5) (3.0 . 0.5) (2.5 . 0.5) (2.0 . 0.5) (1.5 . 0.5) (0.5 . 0.5))))

However, I'm just going to have to write a function to clean this.

  (defun ha-remove-superfluous-org-padding ()
    (interactive)
    (goto-char (point-min))
    (ha-remove-org-header-padding)
    (goto-char (point-min))
    (ha-remove-org-block-padding))

  (defun ha-remove-org-header-padding ()
    ;; (goto-char (point-min))
    (while (re-search-forward (rx (optional bol (zero-or-more space) eol "\n")
                                  (group bol (one-or-more "*") (one-or-more space) (one-or-more any) "\n")
                                  (optional bol (zero-or-more space) eol "\n")) nil t)
      (replace-match (match-string 1) nil :no-error)))

  (defun ha-remove-org-block-padding ()
    ;; (goto-char (point-min))
    (while (re-search-forward (rx (optional bol (zero-or-more space) eol "\n")
                                  (group bol (zero-or-more space) "#+BEGIN" (one-or-more any) eol "\n"
                                         (zero-or-more (group bol (zero-or-more any) eol "\n"))
                                         bol (zero-or-more space) "#+END" (zero-or-more any) eol "\n")
                                  (optional bol (zero-or-more space) eol "\n")) nil t)
      (replace-match (match-string 1) nil :no-error)))

Now that is some complicated regular expressions.

Pasting

I like the idea that I will paste HTML text from the clipboard and have it converted to org-formatted text:

(defun ha-org-paste ()
  (interactive)
  (if (eq system-type 'gnu/linux)
      (shell-command "xclip -t text/html -o | pandoc -r html -w org" t)))

Presentations

Used to use org-tree-slide for showing org files as presentations. Converted to use org-present. I love the hooks as that makes it easier to pull out much of my demo-it configuration. My concern with org-present is that it only jumps from one top-level to another top-level header.

  (use-package org-present
    :init
    (defvar ha-org-present-mode-line mode-line-format "Cache previous mode-line format state")

    :config
    (defun org-blocks-hide-headers ()
      "Make the headers and other block metadata invisible. See `org-blocks-show-headers'."
      (add-to-invisibility-spec 'ha-org-block-headers)

      (defun hide-this (regexp)
        (goto-char (point-min))
        (while (re-search-forward regexp nil t)
          (let ((start (match-beginning 0)) (end (1+ (match-end 0))))
            (overlay-put (make-overlay start end) 'invisible 'ha-org-block-headers))))

      (defun hide-these (patterns)
        (when patterns
            (hide-this (car patterns))
            (hide-these (cdr patterns))))

      (save-excursion
        (hide-these (list (rx bol (zero-or-more space)
                              "#+" (or "begin" "end") "_"
                              (one-or-more any) eol)
                          (rx bol (zero-or-more space)
                              "#+" (or "name" "header" "results" "property" "options"
                                       "filetags") ":"
                              (zero-or-more any) eol)
                          (rx bol (zero-or-more space)
                              ":" (or "properties" "header-args" "end") ":"
                              (zero-or-more any) eol)))))

    (defun org-blocks-show-headers ()
      "Un-invisibilize the headers and other block metadata invisible.
    In other words, this undoes what `org-blocks-hide-headers' did."
      (remove-from-invisibility-spec 'ha-org-block-headers))

    (defun org-present-start ()
      (goto-char (point-min)) (re-search-forward (rx bol "*"))
      (org-blocks-hide-headers)
      (org-present-big)
      (setq mode-line-format nil)
      (org-display-inline-images)
      (blink-cursor-mode -1)
      (setq cursor-type nil))

    (defun org-present-end ()
      (org-present-small)
      (org-blocks-show-headers)
      (setq mode-line-format ha-org-present-mode-line)
      (setq cursor-type t)
      (blink-cursor-mode 1)
      (org-present-read-write))

    :hook
    (org-present-mode . org-present-start)
    (org-present-mode-quit . org-present-end))