Novellas with groff

本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。

Novellas with groff

blog - git - desktop - contact Novellas with groff 2026-10-31 I recently finished my second “novella”. Don’t even think about it: I won’t publish this. Nor send it to you privately, which is mostly the same as publishing it. The first story is roughly 50 A5 pages long (~15k words), the second one is about 120 pages (~40k words). (Both barely fit the definition of a “novella”, apparently, with the first one being a bit too short and the second one a tiny bit too long.)

博客 - git - 桌面 - 联系方式:用 groff 写中篇小说 2026-10-31。我最近完成了我的第二部“中篇小说”。别想了:我不会发表它的。也不会私下发给你,因为那和发表没什么两样。第一个故事大约 50 页 A5 纸长(约 1.5 万字),第二个大约 120 页(约 4 万字)。(显然,两者都勉强符合“中篇小说”的定义,第一个稍微短了点,第二个又稍微长了点。)

It feels pretty absurd and surreal that I have done this at all. In school, anything related to “writing” was pure horror for me — unless it was related to maths, physics, or other “technical” stuff. But here we are. How long did it take? Well, the first one took over seven months. But the second one, the longer one? Ten days.

我竟然完成了这些,感觉非常荒谬且超现实。在学校里,任何与“写作”有关的事情对我来说都是纯粹的噩梦——除非它与数学、物理或其他“技术”内容有关。但现在我做到了。花了多久?嗯,第一部用了七个多月。但第二部,那部更长的呢?十天。

Some time last year, probably around July, I started reading some of the books that were lying around in my appartment for decades. And this was much more enjoyable than I thought. No idea why. It’s not like I never read a book before, but it was always a somewhat boring thing to do and it took me ages — unless it was something technical. I’m only referring to fiction and novels here. Like, one book per year, tops. Yet, I somehow got hooked last year and now I’ve read about 30 novels since July 2025. Some faster (a couple of days), some slower (three months). It wasn’t that much of a stretch to try to do this myself.

去年某个时候,大概在七月左右,我开始阅读一些在公寓里放了几十年的书。这比我想象的要有趣得多。不知道为什么。并不是说我以前从没读过书,但这对我来说一直是一件有点无聊的事情,而且要花很长时间——除非是技术类书籍。我这里指的只是小说。比如,一年最多读一本。然而,去年我不知怎么就迷上了,自 2025 年 7 月以来,我已经读了大约 30 本小说。有些读得快(几天),有些慢(三个月)。尝试自己写小说并不是什么遥不可及的事。

This blog is still technical in nature, so I’ll walk through the software that I used. Table of contents: groff, Basic formatting, Footers and page numbers, Header, A closer look at traps, Inter-sentence spacing, German hyphenation, Markdown and other pre-processing, Vim, Goyo, Reflowing text, Git, demo-book.tar.gz: Putting it all together, The writing process itself, Where to go from here?

这个博客本质上仍然是技术性的,所以我将介绍我使用的软件。目录:groff、基本格式、页脚和页码、页眉、深入了解陷阱、句间间距、德语连字符、Markdown 和其他预处理、Vim、Goyo、重排文本、Git、demo-book.tar.gz:整合所有内容、写作过程本身、接下来该做什么?

groff Both of my books (they’re not really “books”, but I’ll keep referring to them as such for the sake of simplicity) are typeset in groff. You know, the thing that usually renders manual pages in your terminal on Linux. groff/troff is a full typesetting system, though, and it can be used to produce real books, which I’ve already mentioned a couple of times in previous blog posts. In my case, the appeal of groff is that it’s relatively small and fast, but still able to produce high-quality output. Making that 50-page document takes about 700 ms on my 13.5 year old PC, and the 120-page one takes 1.6 seconds. This includes some pre-processing which we’ll cover later. Given that most modern software these days feels like walking in mud, this is refreshingly quick.

groff:我的两本书(它们其实算不上“书”,但为了简单起见,我还是这样称呼它们)都是用 groff 排版的。你知道的,就是那个通常在 Linux 终端中渲染手册页的工具。不过,groff/troff 是一个完整的排版系统,它可以用来制作真正的书籍,我在之前的博客文章中已经提到过几次了。对我来说,groff 的吸引力在于它相对小巧且快速,但仍能产生高质量的输出。在我那台 13.5 年前的电脑上,制作那份 50 页的文档大约需要 700 毫秒,而 120 页的文档需要 1.6 秒。这包括了一些我们稍后会介绍的预处理。考虑到现在大多数现代软件运行起来就像在泥潭里行走一样,这简直快得令人耳目一新。

Because it’s more fun that way, I made my own little macro package. There really isn’t a lot to it, though. It’s tiny. Not a lot is needed for a simple novel / novella — no table of contents, no cross-references, no diagrams, no code blocks, no lists, … Just nicely flowing text and sometimes a word gets emphasized. Disclaimer: You’ll note some TODO items in the text below. groff is a full-blown typesetting system and these things are complicated. I’m a long way from having mastered every aspect of it.

因为这样更有趣,我制作了自己的小型宏包。不过它真的没什么复杂的,非常小巧。对于简单的小说/中篇小说来说,不需要太多东西——没有目录、没有交叉引用、没有图表、没有代码块、没有列表……只需要流畅的文本,偶尔强调一下某个词即可。免责声明:你会注意到下面文本中有一些 TODO 项目。groff 是一个成熟的排版系统,这些东西很复杂。我离掌握它的方方面面还有很长的路要走。

Basic formatting There are only three macros: .” Section header / chapter title .de SH . sp 1v . ps +5p . ce \f[HR]\$1\fP . ps . sp 4v .. .” Normal paragraph with hanging indent. .de PP . br . ti +2m .. .” “Long” paragraph, longer empty space. .de LP . sp 2v .. (The name SH is a spill-over from manual pages. I’ve written quite a few of those and this name somehow stuck. CH for “chapter heading” or CT for “chapter title” might have been better choices. Or, you know, .CHAPTER.) You start defining a macro with .de — anything until .. is part of that macro. .SH: Go down one vee, increase type size, request centering, print the macro’s first argument with the HR font (Helvetica), return to previous type size, add another four vees of space. .PP: Just break the current line, then request a temporary indent for the next output line. This creates a hanging indent, which is customary in German books. .LP: Just some empty space between paragraphs. Used for things like intra-chapter time jumps. You can look up the individual commands in man 7 groff. And that would already be enough to produce a document that somewhat resembles a book. Put the snippet above in a file called macros.groff. I made one file per chapter. For example: chapter01.groff. You can then make a PDF using a command like this: groff -Tpdf -dpaper=a5 -P-pa5 macros.groff chapter01.groff >book.pdf A5 is roughly the page size of a “typical book”.

基本格式:只有三个宏:.SH(章节标题)、.PP(带悬挂缩进的普通段落)、.LP(“长”段落,更大的空白)。(SH 这个名字是从手册页沿用下来的。我写过不少手册页,这个名字不知怎么就固定下来了。CH 代表“章节标题”或 CT 代表“章节标题”可能是更好的选择。或者,你知道的,.CHAPTER。)你用 .de 开始定义一个宏——直到 .. 之前的所有内容都是该宏的一部分。.SH:向下移动一个 vee,增加字号,请求居中,用 HR 字体(Helvetica)打印宏的第一个参数,返回之前的字号,再增加四个 vee 的空间。.PP:只需中断当前行,然后请求下一输出行的临时缩进。这会创建一个悬挂缩进,这是德语书籍中的惯例。.LP:段落之间的一些空白。用于章节内的时间跳跃等情况。你可以在 man 7 groff 中查找各个命令。这就足以制作出一份看起来有点像书的文档了。将上面的代码片段放入一个名为 macros.groff 的文件中。我为每一章制作了一个文件。例如:chapter01.groff。然后你可以使用如下命令制作 PDF:groff -Tpdf -dpaper=a5 -P-pa5 macros.groff chapter01.groff >book.pdf。A5 大致是“典型书籍”的页面尺寸。

Footers and page numbers You’ll note that there are no page numbers by default. You could actually just decide to not have them if all you want is a digital PDF. They’re useful for printing, but you don’t really need them in a file, if we’re being honest. Except: The thing gets confused about page breaks. Create the PDF using the example above and you’ll see: Looks like the baseline of the last line of output is just outside the page. Not sure what’s going on here — it’s related to setting the page size to A5. In other words, I probably missed another spot where this has to be set. It’ll be fixed implicitly by the following. The whole page number thing consists of three parts: First, how large is the footer (the area where the page numbers live)? I defined this in a register (which is not really necessary but I find the “code” easier to read this way) — the name is arbitary: .nr _FooterSize 5v We than plant a trap at the bottom of the page: .wh -\n[_FooterSize]u _FooterTrap \n[_FooterSize] “gets” the value of the register called _FooterSize. There’s a minus sign in front of it, it’s negative and relative to the bottom of the page. The documentation says: A general-purpose register (one created or updated with the nr request) is implicitly dimensionless, or reckoned in basic units if interpreted in a measurement context. I’m not 100% sure why I need to add the u. I haven’t read every page of the manual, yet. Needless to say, it doesn’t work without it.

页脚和页码:你会注意到默认情况下没有页码。如果你只需要一个数字 PDF,其实可以决定不要它们。老实说,它们对打印很有用,但在文件中并不真正需要。除了:它会对分页感到困惑。使用上面的示例创建 PDF,你会看到:输出的最后一行基线似乎刚好在页面之外。不确定这里发生了什么——这与将页面大小设置为 A5 有关。换句话说,我可能漏掉了另一个需要设置的地方。下面的内容会隐式修复它。整个页码部分由三部分组成:首先,页脚(页码所在区域)有多大?我在一个寄存器中定义了它(这其实没必要,但我发现这样“代码”更容易阅读)——名字是任意的:.nr _FooterSize 5v。然后我们在页面底部设置一个陷阱:.wh -\n[_FooterSize]u _FooterTrap。\n[_FooterSize] “获取”名为 _FooterSize 的寄存器的值。前面有一个减号,它是负的,相对于页面底部。文档说:通用寄存器(用 nr 请求创建或更新的寄存器)是隐式无量纲的,或者如果在测量上下文中解释,则按基本单位计算。我不 100% 确定为什么需要添加 u。我还没读完手册的每一页。不用说,没有它就无法工作。