Two spaces after a full stop are a typist’s convention, and Emacs believes
in them: sentence-end-double-space is what tells fill-paragraph and
forward-sentence where one sentence stops and the next begins. The
trouble is keeping the text and the variable in agreement, and keeping the
second space out of everything that is laid out rather than written –
code, tables, link targets, maths. That is the whole of this package.
How a document is spaced is a property of that document, so garamond keeps it in the document rather than in anybody’s configuration. You tell a buffer what it uses, garamond writes that into the file, and from then on the file says so itself – on your machine, on a colleague’s, in a checkout made five years from now.
There is no list of modes in this package. No buffer is altered because of what major mode it happens to be in.
Worth knowing, because it is the pivot the design turns on: Emacs ships
with sentence-end-double-space set to t. Every buffer in every Emacs
already “has” double spacing, whether or not anything double-spaced it. A
package that read the value would therefore follow that global into files
which never asked for it.
So garamond reads file-local-variables-alist instead, which holds only
what the file itself said – or what a .dir-locals.el above it said on its
behalf. A file that declares nothing is left entirely alone.
Garamond is one file with no dependencies beyond Emacs 29.1.
git clone https://github.com/yardquit/garamond ~/src/garamond
;; Where the clone lives. One place, read by both `:load-path' and
;; the `:init' block under it.
(defvar my-garamond-dir (expand-file-name "~/src/garamond"))
(use-package garamond
:load-path my-garamond-dir
:init
;; A package archive byte-compiles what it installs. A clone does
;; not, and nothing in garamond compiles itself, so compile it here
;; -- and again whenever garamond.el turns out to be newer than the
;; .elc beside it, which is what a `git pull' leaves behind.
(let* ((src (expand-file-name "garamond.el" my-garamond-dir))
(elc (expand-file-name "garamond.elc" my-garamond-dir))
(recompiled nil))
(if (not (file-exists-p src))
(message "garamond: no garamond.el under %s -- check my-garamond-dir"
my-garamond-dir)
(when (file-newer-than-file-p src elc)
(require 'bytecomp)
(let ((start (current-time)))
(setq recompiled
(and (ignore-errors (byte-compile-file src))
(float-time (time-since start))))))
(let ((compiled (and (file-exists-p elc)
(not (file-newer-than-file-p src elc)))))
(load (if compiled elc src) nil t)
(message "garamond %s loaded %s%s"
(garamond-version)
(if compiled "compiled" "from source (interpreted)")
(if recompiled
(format ", recompiled in %.2fs" recompiled)
"")))))
:config
;; The one line of setup: read the spacing that documents declare.
(garamond-follow-declarations-mode 1)
;; The knobs, should you want any; none is needed.
;; (setq garamond-extra-abbreviations '("Ph.D." "et al." "Inc.")
;; garamond-removed-abbreviations '("St." "No.")
;; garamond-double-space-on-typing t ; nil: declare, never type
;; garamond-lighter t) ; nil: place it yourself
)
:load-path on its own would be enough to load garamond: use-package
ends with (require 'garamond), and require would find garamond.el
there. It would also run interpreted, and garamond works from
post-self-insert-hook, so that cost is paid on every keystroke – though
honesty about its size is owed: interpreted, a space between words costs
0.9 µs against 0.3 µs compiled, and a space after a sentence end 5 µs
against 3 µs. Nobody can feel either. Compiling is worth doing for two
other reasons: the byte-compiler reads the whole file and says what it
finds wrong, and a .elc is what Emacs’s native compiler picks up in the
background when native-comp-jit-compilation is on – a .el loaded from
source never is.
So :init picks the file instead of leaving the choice to require.
That is what the load call is for. Emacs prefers garamond.elc to
garamond.el whenever both are on the path, which makes a stale .elc
the copy require takes – and a later (load "garamond.el") cannot
undo it. Choosing explicitly sidesteps that, and by the time
use-package runs its own require, garamond is already in
features and the require does nothing.
Both halves read my-garamond-dir, so there is one path to edit and no
way for the two to drift apart. Pointing :load-path at one directory
and the compile step at another is the mistake this arrangement exists
to prevent: garamond would still load, from the :load-path copy and
interpreted, behind a warning about the other.
The echo area says which copy you got – loaded compiled, or loaded
from source (interpreted) – with the version, and the time the
recompile took when there was one. M-x garamond-version says the
version again later.
If the compile fails, garamond loads from source and says so. That is
deliberate: a file that will not compile must not stop Emacs from
starting. It also means the reason is not shown, so run
M-x byte-compile-file on garamond.el when you want to see it.
(garamond-follow-declarations-mode 1) in :config above is the whole
of it, and it is the only global thing in the package – not a spacing,
but a willingness to read the ones documents state. It runs from
hack-local-variables-hook, so it sees a file just after the file’s own
variables have been applied, and from read-only-mode-hook, so a
declared file caught read-only is caught up with when made writable. A
file that declares nothing is left alone, and so is a buffer nobody
types in. Nothing runs itself on load; a library that switches itself
on is a library you cannot load in order to read it.
garamond-set-spacing | Declare what this buffer uses. No text is touched |
garamond-adjust-spacing | Declare it, and rewrite the text to match |
garamond-mode | Buffer-local: type one space, get two. Says 1S or 2S |
For a document that already has the convention you want, and just needs
Emacs to agree with it: a file that arrived single-spaced, say. It sets
sentence-end-double-space buffer-locally, switches garamond-mode on so
that the mode line reports the spacing, offers to record the choice in the
file – and changes not
one character of your text. C-u 2 M-x garamond-set-spacing declares
double spacing outright; with no prefix it asks.
This is also the toggle to reach for in a buffer that has nothing to do
with prose. If you want two spaces in a conf-mode buffer, say so and you
have them; garamond has no opinion about where you are allowed to want
things.
The same, plus the rewrite. It works on the region, or the whole buffer when no region is active. Every run of spaces after a sentence end becomes exactly one or two, so the result does not depend on what was there before, while line breaks, indentation, trailing whitespace and the period that numbers an ordered list item are all left alone.
Recording in the file is offered for a whole buffer only – a region says
nothing about the rest of the file – and garamond-persist set to nil
suppresses the question everywhere. A file that already declares its
spacing in its own text is kept accurate without asking, so its local
variable never contradicts its text; one spoken for by a .dir-locals.el
is asked like any other, since the directory’s word is not the file’s.
Both commands refuse a read-only buffer before changing anything when
asked to record, and say so rather than claim a record when file-local
variables are disabled.
The typing. One space after a sentence end becomes two; a longer run typed by hand is trimmed back, so the result does not depend on how many times the space bar was pressed.
The mode line says which spacing is in force, and the absence of it says that nothing has spoken for this buffer at all:
2S | Two spaces between sentences; typing one gives two |
1S | One space; the mode is on but dormant |
(2S) | Bracketed while garamond-double-space-on-typing is nil |
| nothing | The mode is off: nothing has declared this buffer |
Each carries a tooltip saying the same at more length. The mode stays on
for a buffer declared single spaced, where it does nothing but report –
that is what makes 1S distinguishable from a buffer nobody has spoken
for.
A typing mode has nothing to do in a buffer nobody types in.
garamond-mode refuses, saying why, in a read-only buffer, in the
minibuffer, and in the modes listed in garamond-unsuitable-modes –
special-mode and its family, Dired, the shells – whatever asked for it:
a .dir-locals.el entry for all modes, or you by hand. By hand the
refusal is an error; from Lisp, a message. The list is a guard against
the absurd, not a list of where prose is written: a conf-mode or
prog-mode buffer is not on it, and switching the mode on there is taken
at its word.
Read-only is followed, not just refused: with
garamond-follow-declarations-mode on, a declared file opened read-only
gets the typing the moment C-x C-q makes it writable, and loses it again
when it is made read-only.
Set garamond-lighter to nil and the mode contributes nothing of its own;
garamond-mode-line-string is then yours to put where you like. The
ready-made construct goes on the end of the mode line:
(setq garamond-lighter nil)
(add-to-list 'global-mode-string garamond-mode-line-format t)-UUU:**- F1 doc.txt All L2 (Text) 2S---
Or write it into a mode line of your own – which is what a configuration that hides minor mode lighters altogether will want:
(:eval (garamond-mode-line-string))What you place is the bare string, without the leading space a minor mode
lighter is expected to bring, so the spacing around it is yours to decide.
It is safe anywhere, in buffers garamond has nothing to do with included:
there it is the empty string. Leave garamond-lighter at t and add it
by hand as well and you will see it twice.
It requires a letter before the punctuation, which rules out list numbering
and decimals, and it consults garamond-abbreviations, whose trailing period
does not end a sentence, even inside a closing bracket. There are a hundred
and more of those by default: the Latin and editorial shorthand (e.g.,
i.e., cf., viz., etc., ibid.), the bibliographic marks (p.,
pp., ed., Fig., Vol., Sec.), forms of address and of rank (Dr.,
Prof., Messrs., Capt., Sgt.), organisations and places (Co.,
Dept., Mt., U.S.), the months and the days (Sept., Tues.), and the
clock (a.m., p.m.). Degrees, company suffixes and units are left out on
purpose: Ph.D., Inc. and lb. follow the thing they qualify, and so end
a sentence about as often as they sit inside one. Add your own with
garamond-extra-abbreviations and drop defaults with
garamond-removed-abbreviations, neither of which means retyping the list:
(setq garamond-extra-abbreviations '("Ph.D." "et al." "Inc.")
garamond-removed-abbreviations '("St." "No."))All three are read afresh, so a setq takes effect at the next keystroke.
Case matters, with one allowance: an entry that begins with a lower-case
letter also counts capitalised, as it is at the start of a sentence – e.g.
covers E.g. – while No. and Dr. are matched only as given, since no.
and dr. do end sentences.
- Initials.
J. R. R. Tolkienreads as three sentence ends, and the typing doubles after each; there is no tellingJ.from a sentence that ends inI. Add the initials you use togaramond-extra-abbreviationsif it bothers you. - An abbreviation that ends a sentence. After
etc.the typing holds back, as it must – so type the second space yourself. Two spaces typed by hand are left as two. Some defaults do this more often than others:U.S.,a.m.andAve.end sentences about as readily as they sit inside one, andgaramond-removed-abbreviationsis how you drop the ones that get in your way. - A very long Org paragraph. Org reads a paragraph from its start to say what surrounds point, so a space at the end of a paragraph hundreds of lines long with no blank line in it costs milliseconds. Blank lines between paragraphs, which is how prose is written, keep it at a fifth of a millisecond. The wholesale rewrite deliberately ignores that
list, so that its result is uniform. In overwrite-mode it stays out of
the way altogether: a space typed over a character is meant to replace it,
not to shift the rest of the line along. And should a mode’s parser signal
an error mid-keystroke, the error is shown and the space left alone; the
hook never breaks typing.
You rarely turn it on yourself: declaring a buffer does it, either spacing, and so does opening a file that declares itself. Turning it on by hand, in any buffer whatever, is taken at its word.
In its own local variables block, which is what the commands write:
# Local Variables: # sentence-end-double-space: t # End:
Or a whole prose repository at once, in .dir-locals.el, with no line in
anyone’s configuration:
((org-mode . ((sentence-end-double-space . t))))Or, for the typing alone, in a file’s first line:
-*- mode: markdown; mode: garamond -*-
A new document you want double-spaced has no declaration yet, so nothing
happens until you run garamond-set-spacing once – which writes the
declaration, after which the file carries the fact itself. One act per
document, at its birth. That is the price of keeping the setting out of
your configuration, and it buys a document that behaves the same way in
every Emacs that opens it.
One thing, and it is per position rather than per mode: inside a buffer that has declared itself double-spaced, a second space still has no business in a source block, a table column or a link target.
In Org that is org-element-context’s verdict – source and example
blocks, tables, links, inline code, LaTeX fragments, keywords, drawers,
timestamps, citations. Markdown contributes its code blocks, inline code
and links, LaTeX its maths and verbatim environments, and any buffer its
orgtbl-mode tables. The typing hook goes further and recognises
constructs that are only half typed – an Org link with no closing
bracket yet, a block with no #+end_, an unclosed backtick or fence –
because a predicate that parses complete syntax sees nothing at all in
those, and would otherwise put a second space inside a URL.
Measured in a one-megabyte buffer, per space typed, hook included:
| space typed after | text-mode | org-mode | markdown |
|---|---|---|---|
| a word | 0.4 µs | 0.4 µs | 0.4 µs |
| a sentence end | 3 µs | 160 µs | 5 µs |
Any key but the space bar costs one comparison. A space between words
– nearly all of them – costs a look at the two characters before it,
which is what decides that it is not after a sentence end; that look is
the same 0.4 µs on a 70-character line and on the 20,000-character
paragraph-lines of visual-line-mode. Only a space that is after a
sentence end goes on to ask what the text around it is.
That question has two parts. Blocks are settled by an incremental scan: the delimiters above point are counted once and the answer carried forward, so typing at the end of a file, which is how prose mostly grows, costs only the lines typed since the last look. An edit higher up throws the scan away and the next keystroke there rebuilds it from the top – a millisecond or two per megabyte, once. The scan sees a block whose end has not been typed yet, which the parsers cannot, and it answers for closed blocks too, before the parsers are asked.
Then Org is asked org-element-context about what surrounds point,
which is where the 160 µs go: it is Org’s own account of links, inline
code, tables, timestamps and the rest, and it comes with a sync of the
element cache after the edit just made. It is the correct oracle and
is kept for that reason; a fifth of a millisecond, once per sentence,
is not a latency anyone can feel. It does grow with the paragraph,
which Org reads from its start each time – a few milliseconds at the
end of a paragraph hundreds of lines long with no blank line in it.
garamond-adjust-spacing does not pay that per break. It reads the
whole stretch first, deciding every gap while the text is still as it
was, and replaces afterwards from the last gap backwards; in Org it
parses the buffer once instead of asking at each break. Twenty
thousand lines of Org take about two seconds, and a single
thousand-line paragraph, which used to take the square of its length,
a few hundredths.
None says anything about how any document is spaced, which is why they are allowed to be globals.
garamond-abbreviations | Words whose trailing period is not a full stop |
garamond-extra-abbreviations | Yours, added to the defaults |
garamond-removed-abbreviations | Defaults that should not count |
garamond-unsuitable-modes | Where the typing mode refuses to run |
garamond-double-space-on-typing | Master switch for the typing half |
garamond-lighter | Whether the mode shows its own indicator |
M-x garamond-doctor reports all of them, and what they are doing to the
current buffer.
(setq garamond-double-space-on-typing nil)With that, a declared file still sets sentence-end-double-space – so
filling and sentence motion behave – while no second space ever appears
under your hands. It is read afresh at each keystroke, so a keybinding can
flip it mid-sentence and the minor mode need not be cycled.
garamond-persist governs only whether the commands offer to write a
declaration.
M-x garamond-doctor
It reports what garamond is doing in the current buffer and why: whether the buffer has been declared, whether anything read that declaration, whether the indicator is being shown, hidden, or simply has nothing to say – and ends with the likeliest reason for what you are seeing.
The three usual answers:
- Nothing has declared this buffer. Which is the design, not a fault: a
file that says nothing gets nothing.
M-x garamond-set-spacingsays what it uses, writes that into the file, and the indicator appears at once. - The file declares its spacing, but the mode is off. Either
garamond-follow-declarations-modeis not on, or the file was opened before it was – reverting the buffer settles the second. - The mode is on but the mode line does not show it. A mode line that
hides minor mode lighters, as doom-modeline does unless
doom-modeline-minor-modesist. Either show minor modes, or place the indicator yourself as above. The doctor tells these apart by rendering the mode line and looking for the indicator in it.
make # compile, with warnings as errors, then run the tests
make test
make lint # checkdoc; fails on any findingCI runs make and make lint on Emacs 29.1 – the floor declared in the
header – 29.4, 30.1 and a snapshot, the last allowed to fail. Changes
are recorded in CHANGELOG.org.
GPL-3.0-or-later. See LICENSE.