Novellas with Groff

Hacker News by 21 min read 39x views
Novellas with Groff

Share Post

blog - git - desktop - contact


2026-10-31

I lately completed my second "novella".

 About 50 pages / 15k words for the archetypal document, 120 pages / 40k words for the second one.

Don't equal think concerning it: I won't publish this. Nor dispatch it to you privately, which is mostly the identical as publishing it.

The archetypal narrative is approximately 50 A5 pages long (~15k words), the second one is concerning 120 pages (~40k words). (Both barely fit the definition of a "novella", apparently, alongside the archetypal one being a bit too abbreviated and the second one a small bit too long.)

It feels beautiful ridiculous and surreal that I have done this at all. In school, anything connected to "writing" was clean fear for me -- unless it was connected to maths, physics, or another "technical" stuff. But here we are.

How lengthy did it take? Well, the archetypal one took complete seven months. But the second one, the longer one? Ten days.

Some period final year, likely about July, I started study several of the books that were lying about in my appartment for decades. And this was much additional fun than I thought. No idea why. It's not akin I never read a publish before, but it was continually a slightly boring item to do and it took me ages -- unless it was item technical. I'm lone referring to fabrication and novels here. Like, one publish per year, tops. Yet, I somehow got hooked final twelvemonth and now I've peruse concerning 30 novels since July 2025. Some faster (a brace of days), several slower (three months).

It wasn't that much of a extend to try to do this myself.

This blog is motionless specialized in nature, so I'll stroll through the software that I used.


Table of contents:


groff

Both of my books (they're not really "books", but I'll keep referring to them as specified for the purpose of simplicity) are typeset in groff. You know, the item that normally renders manual pages in your terminal on Linux. groff/troff is a complete typesetting system, though, and it can be used to produce genuine books, which I've already mentioned a brace of times in previous blog posts.

 Photo of my copies of 'The C Programming Language', 'The UNIX Programming Environment', and 'UNIX Network Programming'.  A leaf of 'The C Programming Language'.  A leaf of 'UNIX Network Programming', showing several diagrams.

In my case, the petition of groff is that it's comparatively small and fast, but motionless capable to create high-quality output. Making that 50-page document takes concerning 700 ms on my 13.5 twelvemonth old PC, and the 120-page one takes 1.6 seconds. This includes several pre-processing which we'll shield later. Given that most contemporary application these days feels akin strolling in mud, this is refreshingly quick.

Because it's additional fun that way, I made my own small macro package. There really isn't a lot to it, though. It's tiny. Not a lot is needed for a uncomplicated novel / novella -- no array of contents, no cross-references, no diagrams, no code blocks, no lists, ... Just nicely flowing content and sometimes a term gets emphasized.

Disclaimer: You'll note several TODO items in the content below. groff is a full-blown typesetting scheme and these things are complicated. I'm a long way from having mastered all aspect of it.

Basic formatting

There are lone three macros:

.\" Section header / division title .de SH . sp 1v . ps +5p . ce \f[HR]\\$1\fP . ps . sp 4v .. .\" Normal paragraph alongside hanging indent. .de PP . br . ti +2m .. .\" "Long" paragraph, longer bare space. .de LP . sp 2v .. 

(The name SH is a spill-over from manual pages. I've written fairly a few of those and this name somehow stuck. CH for "chapter heading" or CT for "chapter title" power have been improved choices. Or, you know, .CHAPTER.)

You commencement defining a macro alongside .de -- item until .. is part of that macro.

  • .SH: Go downward one vee, addition category size, petition centering, imprint the macro's first disagreement alongside the HR font (Helvetica), come back to former type size, add another four vees of space.
  • .PP: Just interrupt the current line, afterward petition a impermanent indent for the next output line. This creates a hanging indent, which is customary in German books.
  • .LP: Just several bare area between paragraphs. Used for things akin intra-chapter period jumps.

You can appearance up the idiosyncratic commands in man 7 groff.

And that would already be adequate to create a document that somewhat resembles a book.

Put the snippet complete in a document called macros.groff. I made one file per chapter. For example: chapter01.groff. You can afterward create a PDF using a command akin this:

groff -Tpdf -dpaper=a5 -P-pa5 macros.groff chapter01.groff >book.pdf 

A5 is approximately the leaf size of a "typical book".

Footers and leaf numbers

You'll note that there are no leaf numbers by default. You could actually fair decide to not have them if all you desire is a digital PDF. They're helpful for printing, but you don't really need them in a file, if we're being honest.

Except: The item gets confused concerning leaf breaks. Create the PDF using the example complete and you'll see:

 Last row of content exterior of the page.

Looks akin the baseline of the final row of output is fair exterior the page. Not certain what's going on current -- it's connected to environment the page size to A5. In another words, I likely missed another place anywhere this has to be set. It'll be fixed implicitly by the following.

The entire leaf figure item consists of three parts: First, how ample is the footer (the area anywhere the leaf numbers live)? I defined this in a register (which is not really necessary but I discover the "code" easier to peruse this way) -- the name is arbitary:

.nr _FooterSize 5v 

We than factory a trap at the base of the page:

.wh -\n[_FooterSize]u _FooterTrap 

\n[_FooterSize] "gets" the value of the enroll called _FooterSize. There's a minus sign in forefront of it, it's negative and related to the bottom of the page. The records says:

A general-purpose enroll (one created or updated alongside the nr request) is implicitly dimensionless, or reckoned in essential units if interpreted in a measure context.

I'm not 100% certain why I need to add the u. I haven't peruse all page of the manual, yet. Needless to say, it doesn't activity without it.

Okay, so the trap gets installed at 5v before the end of the leaf and it'll call the _FooterTrap macro. It looks akin this:

.de _FooterTrap .\" Push current surroundings on stack and toggle to `_FooterEnv`. .\" Parent env volition keep partially collected lines. . ev _FooterEnv .\" Copy settings from default environment. . evc 0 .\" Don't do this for pages 1 and 2, they're heading pages in my actual .\" "book". . if \\n[%]>2 \{\ .\" Set vertical position, explained in the content below. . sp |\\n[.p]u-2v .\" Print % enroll centered. . ce \fR\\n[%]\fP \} .\" Go rear to former environment. . ev .\" Request leaf break. ' bp .. 

It's much easier to peruse without the inline comments:

.de _FooterTrap . ev _FooterEnv . evc 0 . if \\n[%]>2 \{\ . sp |\\n[.p]u-2v . ce \fR\\n[%]\fP \} . ev ' bp .. 

Why is it 'bp alternatively of .bp? We'll get to that.

What I'm unsure about: I set the vertical stance to |\\n[.p]u-2v. That's related to the top of the page, it's the leaf dimension minus two vee. I would have expected |-2v-1v (including the "minus one vee" mentioned in the docs) to additionally work, but that leads to an infinite loop (probably triggers the identical trap again and again). Why?

For symmetry, I additionally desire several bare area at the top of the page:

.nr _HeaderSize 5v .de _HeaderTrap ' sp |\\n[_HeaderSize]u .. .wh 0v _HeaderTrap 

And another TODO for me: This isn't 100% symmetrical. The dimension of the header is a bit larger than the footer. An matter alongside the leaf size again?

A nearer appearance at traps

Let's have a nearer appearance at traps. I'm not 100% certain on this -- this is to the finest of my understanding.

Actually, let's obtain a stage rear from the example above. Let's start with this:

This is a small excerpt from Mary Shelly's Frankenstein. These visions faded whenever I perused, for the archetypal time, those poets whose effusions entranced my psyche and lifted it to heaven. I additionally became a author and for one twelvemonth lived in a paradise of my own creation; I imagined that I additionally power get a niche in the temple anywhere the names of Homer and Shakespeare are consecrated. You are fine acquainted with my nonaccomplishment and how heavily I bore the disappointment. But fair at that time I inherited the fortune of my cousin, and my thoughts were turned into the conduit of their before bent. Six years have passed since I resolved on my current undertaking. I can, even now, recall the hr from which I dedicated myself to this great enterprise. I commenced by inuring my build to hardship. I accompanied the whale-fishers on multiple expeditions to the North Sea; I voluntarily endured cold, famine, thirst, and desire of sleep; I frequently worked harder than the average sailors during the day and devoted my nights to the study of mathematics, the theory of medicine, and those branches of physical discipline from which a naval adventurer power get the greatest practical advantage. Twice I really hired myself as an under-mate in a Greenland whaler, and acquitted myself to admiration. I must own I felt a small arrogant whenever my captain offered me the second dignity in the vessel and entreated me to remain alongside the top earnestness, so valuable did he regard my services. 

Save it as test.groff and create a PDF:

groff -Tpdf test.groff >book.pdf 

Looks akin this:

traps-1.webp

Install a trap at 10v. It's bare so far and does nothing -- insert this at the extremely top of the file:

.de MyTrap .. .wh 10v MyTrap .\" 

The PDF looks the same.

Now what happens whenever we alter the trap macro to this?

.de MyTrap FOO .. 

This is the result:

traps-2.webp

When the output reached the term "fishers", it sprang the trap. "FOO" comes following "fishers" in the PDF: "fishers" triggered the trap, then "FOO" got inserted. I think it's as uncomplicated as that, isn't it?

Suppose we desire to put "FOO" at the commencement of the row and we want nothing but that term on that line. "fishers" shall be put on the next line.

If I comprehend correctly, switching environments is key: The environment holds the currently collected row (among many other things). Switch to a new one and we get a new buffer/line -- one that's empty so far. We can afterward flush/break this row to have it included in the PDF, and afterward toggle rear to the former environment:

.de MyTrap . ev MyTrapEnv FOO . br . ev .. 

Looks akin this:

traps-3.webp

So far, so good. Now I desire there to be an bare row before and after "FOO":

.de MyTrap . ev MyTrapEnv . sp FOO . sp . ev .. 

In the new environment, we matter .sp: It breaks and inserts space. Nothing was collected so far, so the interrupt doesn't imprint item at this point. Then we have FOO -- this isn't printed directly, it's just collected. The next .sp breaks again (thus printing "FOO") and then inserts additional space.

traps-4.webp

Okay. Now. Suppose that printing "FOO" alongside those two lines about it is all we desire to do inner of the trap. We afterward desire the normal content to continue at several complete stance on the page, say at 17v.

So, how concerning the following? We go rear to the former surroundings and then matter an complete location request:

.de MyTrap . ev MyTrapEnv . sp FOO . sp . ev . sp |17v .. 

Oops:

traps-5.webp

.sp |17v bankrupt the already collected row (the one alongside fair "fishers" in it) and then inserted the space. We don't desire that break. (Also note that "fishers" is at the exact identical stance anywhere we remaining off inside the trap.)

This can be avoided by using ' alternatively of .:

' The neutral apostrophe is the no-break authority character, rec‐ ognized anywhere the authority character is. It suppresses the (first) interrupt implied by the bp, ce, cf, fi, fl, "in", nf, rj, sp, ti, and trf requests. The requested procedure takes ef‐ fect at the next break. 

So let's do this:

.de MyTrap . ev MyTrapEnv . sp FOO . sp . ev ' sp |17v .. 

Result:

traps-6.webp

The example in the documentation doesn't alter environments. But I discover it much easier to understand what's going whenever changing them. (And I anticipation I got it right.)

One final remark: Environment transport settings. .ev foo starts a completely new surroundings (unless an surroundings alongside that name already exists), it doesn't copy the former one. That's why my macro package also does .evc 0.

As a small exercise, see what happens whenever you use 'sp |12v, 'sp |11.5v, 'sp |11v, all the way downward to 'sp |9v.

Inter-sentence spacing

Only whenever penning this blog post, I established the following declaration in the docs (Basics):

If you don't commencement a declaration on a new line, put two spaces following the previous sentence. GNU troff afterward recognizes symbols that ends a sentence, and inserts inter-sentence area accordingly.

I didn't do that, since this isn't customary in German. But now I see it. Sometimes, it looks akin this in the PDF:

Foo. Bar. 

And sometimes there's a small bit of additional space:

Foo. Bar. 

As far as I know, this is normally not done in German books. I disabled it alongside this request, which keeps "minimal inter-word spacing" at the default value of 12 but sets "supplemental inter-sentence spacing" to 0:

.ss 12 0 

This isn't part of my macro package, but it comes archetypal in the "index" file (book.groff, see demo bundle below).

German hyphenation

Apparently, it's as uncomplicated as this:

.hla den .hpf hyphen.den 

den is "de(utsch) new": This includes the "new" German rules from 2006.

Markdown and another pre-processing

I really wrote the two of my stories in plain groff, using the macros I explained above.

Markdown has the big advantage of being "visually pleasing": It mostly looks like text that is intended to be peruse by a individual being. groff isn't, really. I mean, it wasn't horrible, I wouldn't have written complete 170 pages in groff if this was pain. It's fine. I use groff in multiple another places as well. But Markdown is a small tiny bit better, and I assume that it allows me to concentration additional on the imaginative process.

So, afterwards, I converted the second narrative to "Markdown". Didn't bother to do that for the archetypal one. I put that in quotation marks because there are lone extremely few elements, it's not complete Markdown:

  • # Chapter heading at the top of the file.
  • Empty lines between paragraphs, --- for "long bare paragraphs".
  • *foo* for emphasized text.

And that's it. Makes the conversion really easy.

I could have fair use plain Markdown for this entire item and created an HTML file. I already have the tooling for that -- that's what drives my blog. Maybe this could equal create a PDF file. But what would be missing there, among another things, would be an motor to properly justify text. It hurts my eyes to see justified content in HTML, it just looks akin a complete disaster. The results that groff or TeX create are much, much better.

While we're at it, we can additionally do several essential character replacements: I have the German typographic quotation marks U+201E and U+201C directly on my keyboard, but Guillemets are preferred in books.

I archetypal wanted to do all this in Python but afterward switched to a little Perl snippet, since it makes it uncomplicated to peruse the complete document and then do multiline-regex material on it. It goes item akin this:

perl -0pe ' # Guillemets are preferred in books. s/... German typographic quotes, see demo bundle .../... Guillemets .../g; # Markdown "h1" into ".SH". s/^# ([^\n]+)\n/.SH "\1"\n/smg; # Markdown "hr" into ".LP". s/\n---\n/.LP/smg; # Markdown paragraphs into ".PP". s/\n\n/\n.PP\n/smg; # Markdown emphasis into groff font selection: s/\*([^*]+)\*/\\fI\1\\fP/smg; ' 

The output is motionless groff for my small macro package. But I can now write it akin this:

# Kapitel 42: Die Antwort Da war sie also. Die Antwort auf *alle* Fragen. Und dann ging es weiter: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum hendrerit blandit suscipit. Maecenas sed nisi nec ante mattis vulputate. Pellentesque maximus leo id purus vestibulum dictum. Donec tincidunt mollis viverra. Curabitur at ligula in turpis pellentesque vehicula. Duis hendrerit commodo mauris, non posuere leo vulputate ut. Nulla finibus urna eget vehicula auctor. Mauris in posuere nibh. Vivamus ut metus vel nisi gravida molestie vitae et tellus. Integer mi odio, vestibulum eget laoreet vel, auctor nec ligula. Quisque eu placerat quam, eu tincidunt mauris. Nam vestibulum lectus non leo dignissim, non sagittis dolor venenatis. 

This says much nicer and there is syntax highlighting in Vim for it. Well, the *emphasized* parts, that is -- the remainder doesn't matter:

 Emphasized content uses the terminal's 'standout' attribute.

Speaking of Vim ...

Vim

Goyo

There is one excellent important Vim plugin for this:

https://github.com/junegunn/goyo.vim

It hides nearly all UI elements.

On top of that, I put the terminal opening in fullscreen manner to conceal my desktop.

And there you go. "Distraction-free writing" indeed. Maybe put on some music, but there's nothing else. No browser, no clock, no nothing. Just a typewriter alongside an uncomplicated undo function.

 Photo of Vim alongside Markdown highlighting and energetic Goyo, terminal in fullscreen mode.

The font is the 11x22 type of UW ttyp0.

Reflowing text

There's a famous way to format content in plain-text files:

This is one sentence. This is another one. And so on and so on. One declaration per line, that's it. Sentences can be short. Or they can be beautiful long, sometimes equal exceeding the accessible width on your screen, thus requiring you to either scroll or have your publishing company soft-wrap it for you whenever displaying it. 

As we've learned complete from the groff docs, this really carries definition for groff (properly detect the ends of sentences). And it makes the diffs in VCS's small noisy.

Another famous way would be to put all paragraph on its own line. Like, there's lone one "physical" newline character following the whole thing and it's your editor's job to correctly display this:

This is one sentence. This is another one. And so on and so on. One declaration per line, that's it. Sentences can be short. Or they can be beautiful long, sometimes equal exceeding the accessible width on your screen, thus requiring you to either scroll or have your publishing company soft-wrap it for you whenever displaying it. 

I used the second type for a brace of years at the end of the 1990ies and the first 2000s, but eventually switched to hard-wrapped text. Like this:

This is one sentence. This is another one. And so on and so on. One sentence per line, that's it. Sentences can be short. Or they can be pretty long, sometimes equal exceeding the accessible width on your screen, thus requiring you to either scroll or have your editor soft-wrap it for you whenever displaying it. 

I fair discover this easier to peruse and easier to navigate in. And it reminds me of the "good" old DOS days, anywhere I used the T editor a lot and all my records looked akin that (manually formatted, of course). It does have drawbacks, yes. It's not awesome for collaboration alongside other people, but that doesn't matter in my case.

Another disadvantage is that I have to reformat these paragraphs while editing. Luckily, Vim makes that easy:

" In ~/.vimrc: set nojs map Q gq} 

I can afterward fair put my cursor at the commencement of a paragraph:

[T]his is one sentence. This is another one. And so on and so on. One declaration per line, that's it. Sentences can be short. 

And hit Q to rotate it into this:

This is one sentence. This is another one. And so on and so on. One sentence per line, that's it. Sentences can be short. [_] 

The cursor volition be positioned following that paragraph. To reflow the entire document, I can fair keep Q pressed downward until it hits the end.

-- edit: Or do :set formatoptions+=a to have paragraphs reformatted automatically during editing it. I normally don't desire that, I usually want it to be an definitive operation. But for penning prose, this might be beautiful useful.

The nojs choice (funny name, huh?) was new to me, lone established out about it fair now. The complete name is nojoinspaces and it prevents the very thing that I should be doing (but which is not customary in German): It does not insert two spaces following a period. Without it, i.e., with set js, the outcome would appearance akin this:

This is one sentence. This is another one. And so on and so on. One declaration per line, that's it. Sentences can be short. 

Some group akin that, I don't.

Git

As alongside practically everything I do, this is in Git.

But that's the end. I regularly commit the current state, but I hardly care concerning item else. No appropriate commit messages, nothing. It's just a tool for making "backups" easier and to sync the data between my laptop and desktop PC.

demo-book.tar.gz: Putting it all together

As I said, I won't publish the genuine "books". But here's a demo of the "scaffold":

demo-book.tar.gz

(I won't update this. This is fair a snapshot of the current status.)

The penning procedure itself

I won't say much concerning this. I'm a total noob.

I changed way a brace of times. For the two stories, I tried to draft the entire item before I began penning the genuine text, but that didn't help much. The archetypal narrative isn't that awesome anyway. It has an interesting premise, but I wasn't really "in the zone". That's additionally why it took so long. The second narrative has a much additional mundane essential idea to it and afterward I eventually realized how engaging it really was. There are fairly a few twists and turns in it, and I affection that.

It was way harder than I idea to keep the facts straight, especially alongside the way changes. Which character knows what, when, why? What are the motives? Is everything accordant and plausible? Constantly inquiry everything -- does she really need to do that now or is there several other, much additional apt way out? Does all sentence she stated 100 pages ago motionless create awareness following the big disclose at the end? Stuff akin that. I'll have to get improved at that. Probably use a little "character inventory" next time, like, a overview for all one.

Building a earth alongside its own set of rules and former and what not turns out to be extremely fascinating. As is construction the characters.

The dimension of the stories create proof-reading ridiculously hard. Like, I'm not going to peruse 120 pages in a brace of minutes. I re-read the second narrative one final period yesterday and it took ... *checks notes* ... three hours. Ouch.

Where to go from here?

Now that I've re-read that 120-page item again yesterday ... I actually like it. I'm blessed alongside the result. There's evidently a lot to learn: This complete site is entirely new to me. I have identified fairly a few places anywhere the storytelling isn't awesome -- the gait isn't right, things progress too fast. (At the moment, I lone notice this whenever study the whole item in one go. Oof.) Sometimes the wording isn't great. Not enough detail. And so on. But the narrative itself is item that I'd enjoy study myself.

Digging deeper into groff is on my TODO list, additionally for apparent reasons.

What I really loved concerning this entire item is that I ended up completely in the tunnel. On one of those ten days that it took me to compose the 120-page thing, I wasn't equal really typing on a keyboard: We were travelling throughout the country, many of idle time, I didn't have a laptop, but I basically "wrote" at smallest one division and afterward memorized (most of) it. Went rear and forth, new ideas. Completely absorbed.

I desire additional of that. :-)

Maybe material volition eventually get published (as in: I power put it on this website), but not now. I do this for myself. I'm not equal remotely interested in my activity getting torn distinct by rude critics. Leave me alone. :-)

And if you think concerning suggesting the use of LLMs: Congratulations, you have completely missed the point. "Setzen, 6."

Other Article Hacker News
↑
Close Right Ads
Close Left Ads