Book Writing inside of Emacs

I've written two novels in Emacs now, as well as many, many shorter prose works. I will cover some basics about writing a novel, and why you can do it in Emacs comfortably.

This will not be a complete guide (there are many, try this one), however I discuss a few tricks that I found very useful, particularly those that help working on larger projects. Of course, the biggest thing I recommend really getting proficient with your basic tooling. Org-mode, Emacs both have sensible keybindings that, while possible to do without, will save you a lot of time.

All the Especially Useful Packages

  • abbrev-mode for common typos to be autocorrected.
  • auto-capitalize-mode for capitalizing the beginning of sentences.
  • company-mode for convenient auto-complete
  • flyspell-mode for spell-checking.
  • langtool for grammar-checking.
  • mw-thesaurus for synonym lookup inside Emacs.
  • olivetti-mode for a nice clean, centered buffer.
  • org-wc for counting per-word headlines

I also use Spacemacs for a whole lot of better defaults…

Make your own writing and editing modes

There's two distinct phases when writing: creation and refinement. While editing can make use of all sorts of error checkers, you don't want that cruft running in the background, especially on a giant buffer, such as a novel. Nor do you want all the error symbols worrying you.

Therefore, I use two distinct modes, writing mode (on most of the time in my writing buffers), and editing mode, which I only use during my cleanup phase. I find it useful to put my writing-related customizations here.

  (defvar writing-mode-map
      (let ((map (make-sparse-keymap)))
        (define-key map [remap evil-previous-line] 'evil-previous-visual-line)
        (define-key map [remap evil-next-line] 'evil-next-visual-line)
        (define-key map [?-] 'typopunct-insert-typographical-dashes)

  (define-minor-mode cf/writing-mode ()
        :keymap writing-mode-map
        :group 'writing
        :global nil

        (setq-local org-startup-folded nil)
        (setq-local org-level-color-stars-only nil)
        (setq-local org-hide-leading-stars t)

        ;; org number headlines
        (setq-local org-num-skip-unnumbered t)
        (setq-local org-num-skip-footnotes t)
        (setq-local org-num-max-level 1)
        (setq-local org-num-face nil)
        (setq-local line-spacing 2)

        ;; Have org number headlines
        (org-num-mode 1)

        ;; Org indent
        (org-indent-mode 1)

        ;; This interferes with olivetti.
        ;; Center the buffer
        (olivetti-mode 1)
        ;; Hide title / author ... keywords
        (setq-local org-hidden-keywords '(title author date startup))

        ;; Spelling stuff

        (git-gutter-mode -1) ;; too slow for large buffers.
        nil " Writing" '()
  ;; Auto-enable the mode for writing buffers.
  (add-hook 'org-mode-hook
            (lambda ()
              (if (and (stringp buffer-file-name)
                        (string-match "/org/" buffer-file-name)
                        (string-match "/writing/" buffer-file-name)))
                  (cf/writing-mode))) t)

(define-minor-mode cf/editing-mode ()
  :keymap editing-mode-map
  :group 'editing
  :global nil

Switching Outline Views

One of the single-best things that org-mode does is allow one to do is quickly navigate between high-level and specifics. This mimics the best of Scrivener's Features, which is their Corkboard and Outline mode, that allow you to do similar. My gripe with org-mode is that they hide it inside of this Shift-TAB, which always feels clumsy to me, because depending on what's collapsed or not, it can have unintended behavior. So, here:

;; quick cuts to show certain views
(evil-define-key '(insert normal) org-mode-map
  (kbd "M-1") (lambda () (interactive) (org-shifttab 1)))
(evil-define-key '(insert normal) org-mode-map
  (kbd "M-2") (lambda () (interactive) (org-shifttab 2)))
(evil-define-key '(insert normal) org-mode-map
  (kbd "M-3") (lambda () (interactive) (org-shifttab 3)))
;; show everything
(evil-define-key '(insert normal) org-mode-map
  (kbd "M-4") (lambda () (interactive) (org-show-all '(headings drawers blocks))))


Dealing with a huge buffer can be daunting, so when I'm working I usually narrow towards a specific chapter. This is great to focus on the day's work…

But, there's one better: indirect buffers. Basically, this lets you have one buffer be your "WIP" narrowed buffer, where you're zoomed in on today's work, and your other buffer still at normal scope, which you can use to reference other facts. Usually I just use one of these per day, and name it after the day.

(defun narrow-todays-indirect-buffer ()
  "Narrow to an indirect buffer for the day's writing."
  (let ((old-name  buffer-file-name)
        (new-name (format "*%s.%s*" (file-name-nondirectory buffer-file-name)
                          (format-time-string "%m_%d_%y"))))
    (rename-buffer new-name)))


From/To the Web

This is probably my most useful. You want org to understand the copying that you did from the web, or vice-versa. It should be good enough that I can copy into org, paste into Google Docs, with nothing lost in translation. Headings, emphasis, it's all there. Same going the other way.

You'll need pandoc and your OS's version of a clipboard manager.

(defun cf/html2org-paste ()
  "Convert clipboard contents from HTML to Org and then paste (yank)."
  (kill-new (shell-command-to-string "xclip -selection clipboard -o -t text/html | 
                                      pandoc -f html -t json | 
                                      pandoc -f json -t org --wrap none"))

(defun cf/org2html-copy ()
  "Convert the contents of the current buffer or region from Org
mode to HTML. Store the result in the clipboard."
  (cf/export-to-org) ;; Need this, otherwise it copies things that aren't intended.
   (lambda ()(shell-command-on-region (point-min)
                                      "pandoc --from=org --to=html
                                       | xclip -selection clipboard -target text/html 
                                        &> /dev/null"))))

To Manuscript-quality PDF

For full document sharing, you should have latex working well. I personally have everything beautifully formatted in a standard manuscript format.

#+title: Book
#+subtitle: Draft 1
#+author: John Doe

* Org Settings                                                     :noexport:
# This is a standalone formatting sheet for org that provides "Standard
# Manuscript Formatting. That is, 12pt font, (almost) Times New Roman, 1-inch
# margins, lower-right hand pagination, and indentation on all paragraphs
# except the first one.

# To get new-page chapters, use a book or report latex class.
#+LATEX_CLASS_OPTIONS: [oneside, 12pt]

# Correct Margins
#+latex_header: \usepackage{setspace}
#+latex_header: \usepackage[margin=1in]{geometry}

# Get Correct Font. (Closest thing LaTeX has to Times)
#+latex_header: \usepackage{mathptmx}

# Hyphenate letters correctly.
#+latex_header: \usepackage[T1]{fontenc}

# More Unicode characters
#+latex_header: \usepackage[utf8]{inputenc}
#+latex_header: \usepackage{newunicodechar}

# Get page numbers bottom right.
#+latex_header: \usepackage{lastpage}
#+latex_header: \usepackage{fancyhdr}
#+latex_header: \pagestyle{fancyplain}
# Clear any old style settings
#+latex_header: \fancyhead{}
#+latex_header: \fancyfoot{}

# Double spacing between lines
#+latex_header_extra: \doublespacing
# Give spaces between paragraphs.
#+latex_header_extra: \setlength{\parskip}{1em}

#+OPTIONS: arch:nil d:nil title:t toc:nil num:nil tags:nil ':t
#+OPTIONS: tex:t
#+STARTUP: fold

Word Counts (More than you ever wanted, perhaps…)

There are a few questions related to word count, all subtly different:

  1. How long is the exported document in words?
  2. How does the word count break down over the document (i.e. What sections are heavy versus light…are my chapters of similar length or not?)
  3. How many words have I written today?

    The default count-words doesn't do any of these.

;; This solves #1.
(defun cf/org-count-exported-words ()
  "This function exports the current buffer temporarily and runs word-count over the exported content. Gets the word count as a kill and prints it out."
    (switch-to-buffer "*Org ORG Export*")
  (message (format "Exported content has %s words." (car kill-ring)))
  (string-to-number (car kill-ring))
(defun cf/wc-helper ()
  "Counts the number of words in the region and adds it to the kill-ring."
    (goto-char (point-min))
    (kill-new (number-to-string (count-matches "\\sw+")))))
(defun cf/export-to-org ()
  "Export with minimum additions"
  (let ((org-export-with-toc nil)
        (org-export-with-title nil)
        (org-export-with-author nil)
        (org-export-time-stamp-file nil)
        (org-export-with-date nil)
        (org-export-show-temporary-export-buffer nil)

#2 is solved by org-wc package.

#3 isn't really solved in Emacs, it's solved using git. I use a local git repo, for version tracking (not backup) and have a cronjob script to auto-commit every day, with the day's word count and date. At the core is a script that stages everything, calculates the word differential, and posts that as my git commit.

The resulting git log looks something like this:

10686d Auto-Saved "2022-05-24", Word Count: 1200
eb21ff Auto-Saved "2022-05-23", Word Count: 1790
230053 Auto-Saved "2022-05-22", Word Count: 2722
04b981 Auto-Saved "2022-05-21", Word Count: 1202
b88245 Auto-Saved "2022-05-20", Word Count: 2434
80b336 Auto-Saved "2022-05-19", Word Count: 379
07ddf2 Auto-Saved "2022-05-18", Word Count: 697

I cribbed the git word-count from here:

Autocommit Script

# Run this file from the current directory.
pushd "$(dirname "$(readlink -f "$0")")"

COMMIT_DATE=$(date +\"%Y-%m-%d\")

source ${HOME}/bin/
WORD_DIFF=$(git_words_diff HEAD)
WORD_ADD=$(git_words_added HEAD)
WORD_DEL=$(git_words_removed HEAD)

COMMIT_MSG="Auto-Saved ${COMMIT_DATE}, Word Count: ${WORD_DIFF}

Words Written: ${WORD_ADD}, Deleted: ${WORD_DEL}"

# stage all
git add -u .

# commit with current date
git commit -m "${COMMIT_MSG}"

Join the newsletter to be kept up to date!