blog · git · desktop · images · contact & privacy · gopher


E-Mails mit Vim und groff

Auf so ziemlich jedem GNU/Linux-System ist heute GNU troff installiert, ein „einfaches“ – oder besser: altes – Textsatzsystem. Bei der Leistung heutiger Rechner ist es so leichtgewichtig geworden, dass man seine Existenz kaum mehr bemerkt. Trotzdem wird es bei jedem Aufruf einer Manpage verwendet, denn diese werden allesamt mit troff gesetzt (weswegen es auch ein Leichtes ist, eine Manpage als HTML oder PDF zu rendern). Früher wurden damit auch ganze Büche gesetzt. Das ein oder andere Buch dieser Liste ist ganz sicher bekannt.

Die Ergebnisse von groff finde ich durchaus ansprechend, insbesondere die Plaintext-Ausgabe. Das richtige Makro-Paket vorausgesetzt ist es auch nicht allzu schwer zu benutzen. Und es ist eben eine sehr schnelle und schlanke Angelegenheit – im Gegensatz zu zum Beispiel LaTeX. Installiert ist es ohnehin überall. Ich habe daher mal das Experiment gewagt, groff derart in Vim einzubinden, dass ich damit komfortabel E-Mails schreiben kann.

„plain groff“ ist nicht sonderlich komfortabel, vermutlich ähnlich wie „plain TeX“ (damit habe ich leider noch keine Erfahrung). Man kommt also nicht um ein Makro-Paket herum. Das mm-Paket (siehe groff_mm(7)) erschien mir auf den ersten Blick ganz sinnvoll, insbesondere weil man damit sehr leicht sowohl „bulleted lists“ als auch automatisch nummerierte Listen oder Fußnoten setzen kann.

Das erste Problem stellt sich dadurch, dass ich in Mails natürlich keine Seitenaufteilung will, sondern fortlaufenden Text wie in Manpages. Beim mm-Paket ist das aber der Fall, weil es eben für „richtige“ Dokumente gedacht ist. Sprich, die Ausgabe erfolgt zwar als Plaintext, aber unterbrochen durch einige Leerzeilen und Seitennummern. Den unmittelbaren Gedanken, doch einfach das mandoc-Paket zu verwenden, habe ich dann verworfen, weil damit keine Fußnoten unterstützt werden – und zum Beispiel URLs würde ich schon gerne per Fußnote in der Mail erwähnen. Also bleibt’s vorerst beim mm-Paket.

Eine eher wackelige Lösung ist, die Größe der Seite auf „sehr groß“ zu setzen. Das hat natürlich den Seiteneffekt, dass zwischen Text und Fußnoten furchtbar viel Platz ist, den man hinterher wieder entfernen muss. Trotzdem werde ich erstmal diesen Workaround verwenden.

Eine ganz einfache Mail könnte also so aussehen:

.PGNH
.PGFORM 72 10000 0
Hallo,
.P
das hier ist der Text. Das .P eben hat einen neuen Absatz eingeleitet.
Und so geht das dann weiter. Eine ganze Weile lang. Ein bisschen Text
brauche ich jetzt noch, damit man von der Formatierung auch etwas
erkennen kann. So, das sollte genügen. Kommen wir zu einer Liste:
.AL
.LI
Hallo, Welt.
.LI
Auch hier wird alles schön sauber eingerückt, was man an diesem langen
Element erkennen kann. Zumindest, wenn ich noch ein bisschen Blindtext
dazuschreibe.
.LE
.P
Das war's. Tschüss!

Und übersetzen kann man es so:

groff -m mm -Kutf8 -Tutf8 foo | cat -s

.PGNH“ gibt an, dass auf der ersten Seite kein Seitenkopf erscheinen soll, und „.PGFORM“ bestimmt dann die Größe der Seite. Die anderen Makros kann man, wie bereits angedeutet, der Manpage „man groff_mm“ entnehmen. Das Ergebnis:

Hallo,

das  hier  ist der Text. Das .P eben hat einen neuen Absatz eingeleitet.
Und so geht das dann weiter. Eine ganze Weile lang.  Ein  bisschen  Text
brauche  ich  jetzt  noch,  damit  man  von  der Formatierung auch etwas
erkennen kann. So, das sollte genügen. Kommen wir zu einer Liste:

  1.  Hallo, Welt.

  2.  Auch hier wird alles schön sauber eingerückt, was  man  an  diesem
      langen  Element  erkennen  kann.  Zumindest,  wenn  ich  noch  ein
      bisschen Blindtext dazuschreibe.

Das war’s. Tschüss!

Damit habe ich jetzt gegen zwei „Regeln“ verstoßen:

So weit, so gut. Ich will natürlich weder die zwei Header-Zeilen immer manuell dort reinschreiben, noch kann ich die gesamte Mail mit groff setzen, denn es gibt ja auch Zitate und Mail-Header. Jetzt kommt also Vim ins Spiel. Los geht’s mit folgendem Mapping:

nmap <Leader>gh :silent a<CR>
            \ROFFSTART<CR>
            \.mso vain-plain.tmac<CR>
            \ROFFEND<CR>
            \.<CR>
            \:silent normal k<CR>

Mein Leader ist das Komma, also kann ich über „,gh“ zwei Marker einfügen und einen Verweis auf ein eigenes Makro-Paket. In diesem kann ich neben dem notwendigen Header von oben auch noch andere hilfreiche Dinge verstauen. Der Pfad, in dem diese Datei gesucht wird, kann übrigens über die Umgebungsvariable „$GROFF_TMAC_PATH“ beeinflusst werden. Der Cursor wird dann auf der Zeile mit „.mso ...“ platziert, sodass ich „o“ drücken kann, um mit dem Schreiben zu beginnen. Durch die Marker kann es mehrere getrennte Abschnitte geben, die später einzeln durch groff geschickt werden. Und das passiert mit folgender Funktion:

fun! DoGroff(outenc, ...) range
    " mm als Default-Paket.
    let l:package = a:0 >= 1 ? a:1 : "mm"

    " Nimmt an, dass ROFFSTART und ROFFEND schön ordentlich in Paaren
    " existieren -- sofern sie überhaupt an Start und Ende des Bereichs
    " auftauchen. Dann werden sie auch gelöscht. Bereich für groff über
    " l:rto entsprechend verringern.
    let l:rto = a:lastline
    if getline(a:firstline) == "ROFFSTART"
        exe ":" . a:lastline . "d"
        exe ":" . a:firstline . "d"
        let @/ = ""
        let l:rto = l:rto - 2
    endif

    " Ab nach groff. Input-Encoding aus Vim-Einstellung ablesen,
    " Output-Encoding gemäß Parameter. Grotty-Farben deaktivieren.
    " Leerzeilen am Ende (entstehen durch die lange Seite) löschen.
    exe ":" . a:firstline . "," . l:rto
                \ . "!groff -pet -m " . l:package
                \ . " -K" . &fenc . " -T" . a:outenc
                \ . " -P-c -P-u -P-o -P-b | cat -s"
endfun

Diese Funktion verwendet also als Eingabe-Encoding immer das aktuelle „fenc“ von Vim, das Ausgabe-Encoding wird als Parameter angegeben. Außerdem akzeptiert die Funktion eine Bereichsangabe, was gleich noch eine Rolle spielen wird. Die Marker entfernt sie automatisch, sofern es sie überhaupt findet. Zuguterletzt wird der Text im angegebenen Bereich durch groff als Filter geschickt. Die vielen Parameter „-P“ deaktivieren Formatierungen wie Farben oder Fettdruck und sind eigentlich nicht wirklich notwendig.

Folgende weitere Mappings:

nmap <Leader>gu :%call DoGroff("utf8")<CR>
nmap <Leader>ga :%call DoGroff("ascii")<CR>
nmap <Leader>gl :%call DoGroff("latin1")<CR>

vmap <Leader>gu :call DoGroff("utf8")<CR>
vmap <Leader>ga :call DoGroff("ascii")<CR>
vmap <Leader>gl :call DoGroff("latin1")<CR>

nmap <Leader>gm :sil %g/^ROFFSTART$/,/^ROFFEND$/ call DoGroff("utf8")<CR>

Ich kann also sehr leicht entweder die gesamte Datei, eine visuelle Markierung oder alle Bereiche zwischen den Markern auf die Funktion werfen und dabei das Ausgabe-Encoding angeben.

Das heißt, ich muss beim Schreiben einer Mail nur zwei Dinge tun:

  1. Einen neuen groff-Abschnitt beginne ich mit „,gh“.
  2. Ganz am Ende vor dem Versand drücke ich einmal „,gm“.

Das war’s.

Mal sehen, wie dieses Experiment weitergeht. Sollte das hier jemand lesen, der sich besser mit groff auskennt und mir sagen kann, wie/ob ich die Seitenaufteilung auch ohne eine übernatürlich lange Seite loswerden kann, wäre ich natürlich dankbar. Falls ich es noch selbst rausfinde, trage ich’s nach.

PS.: Auf den Screenshots ist „smart quoting“ zu sehen. Das heißt, mein "Hurz" wird automatisch durch „Hurz“ ersetzt. Die Ausgabe muss dann aber UTF-8 sein. Das geht so:

.\" Smart Quoting. Ersetzt "Foo" durch \(BqFoo\(lq.
.\" http://lists.gnu.org/archive/html/groff/2007-08/msg00091.html
.de smartq
.ds dblq0 \(Bq
.ds dblq1 \(lq
.nr dblqn 0
.char " \\\\*[dblq\\\\n[dblqn]]\\R'dblqn (1 - \\\\n[dblqn])'
..
.\" Smart Quoting wieder abschalten.
.de /smartq
.rchar "
..

Mit einem „.smartq“ wird dieses Verhalten also aktiviert und mit „./smartq“ wieder deaktiviert.