summaryrefslogtreecommitdiffstats
path: root/src/devices/grotty
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/devices/grotty/TODO3
-rw-r--r--src/devices/grotty/grotty.1.man810
-rw-r--r--src/devices/grotty/grotty.am39
-rwxr-xr-xsrc/devices/grotty/tests/basic_latin_glyphs_map_correctly.sh205
-rwxr-xr-xsrc/devices/grotty/tests/osc8_works.sh119
-rw-r--r--src/devices/grotty/tty.cpp1043
6 files changed, 2219 insertions, 0 deletions
diff --git a/src/devices/grotty/TODO b/src/devices/grotty/TODO
new file mode 100644
index 0000000..3f23dc3
--- /dev/null
+++ b/src/devices/grotty/TODO
@@ -0,0 +1,3 @@
+Document font and device description file usage of grotty.
+
+With -h avoid using a tab when a single space will do.
diff --git a/src/devices/grotty/grotty.1.man b/src/devices/grotty/grotty.1.man
new file mode 100644
index 0000000..3dcafae
--- /dev/null
+++ b/src/devices/grotty/grotty.1.man
@@ -0,0 +1,810 @@
+.TH grotty @MAN1EXT@ "@MDATE@" "groff @VERSION@"
+.SH Name
+grotty \-
+.I groff
+output driver for typewriter-like (terminal) devices
+.
+.
+.\" ====================================================================
+.\" Legal Terms
+.\" ====================================================================
+.\"
+.\" Copyright (C) 1989-2021 Free Software Foundation, Inc.
+.\"
+.\" Permission is granted to make and distribute verbatim copies of this
+.\" manual provided the copyright notice and this permission notice are
+.\" preserved on all copies.
+.\"
+.\" Permission is granted to copy and distribute modified versions of
+.\" this manual under the conditions for verbatim copying, provided that
+.\" the entire resulting derived work is distributed under the terms of
+.\" a permission notice identical to this one.
+.\"
+.\" Permission is granted to copy and distribute translations of this
+.\" manual into another language, under the above conditions for
+.\" modified versions, except that this permission notice may be
+.\" included in translations approved by the Free Software Foundation
+.\" instead of in the original English.
+.
+.
+.\" Save and disable compatibility mode (for, e.g., Solaris 10/11).
+.do nr *groff_grotty_1_man_C \n[.cp]
+.cp 0
+.
+.\" Define fallback for groff 1.23's MR macro if the system lacks it.
+.nr do-fallback 0
+.if !\n(.f .nr do-fallback 1 \" mandoc
+.if \n(.g .if !d MR .nr do-fallback 1 \" older groff
+.if !\n(.g .nr do-fallback 1 \" non-groff *roff
+.if \n[do-fallback] \{\
+. de MR
+. ie \\n(.$=1 \
+. I \%\\$1
+. el \
+. IR \%\\$1 (\\$2)\\$3
+. .
+.\}
+.rr do-fallback
+.
+.
+.\" ====================================================================
+.SH Synopsis
+.\" ====================================================================
+.
+.SY grotty
+.RB [ \-dfho ]
+.RB [ \-i | \-r ]
+.RB [ \-F\~\c
+.IR dir ]
+.RI [ file\~ .\|.\|.]
+.YS
+.
+.
+.SY "grotty \-c"
+.RB [ \-bBdfhouU ]
+.RB [ \-F\~\c
+.IR dir ]
+.RI [ file\~ .\|.\|.]
+.YS
+.
+.
+.SY grotty
+.B \-\-help
+.YS
+.
+.
+.SY grotty
+.B \-v
+.
+.SY grotty
+.B \-\-version
+.YS
+.
+.
+.\" ====================================================================
+.SH Description
+.\" ====================================================================
+.
+The GNU
+.I roff
+TTY
+(\[lq]Teletype\[rq])
+output driver translates the output of
+.MR @g@troff @MAN1EXT@
+into a form suitable for typewriter-like devices,
+including terminal emulators.
+.
+Normally,
+.I grotty
+is invoked by
+.MR groff @MAN1EXT@
+when the latter is given one of the
+.RB \[lq] \-T\~ascii \[rq],
+.RB \[lq] \-T\~latin1 \[rq],
+.BR \-Tlatin1 ,
+or
+.RB \[lq] \-T\~utf8 \[rq]
+options on systems using ISO character encoding standards,
+or with
+.RB \[lq] \-T\~cp1047 \[rq]
+or
+.RB \[lq] \-T\~utf8 \[rq]
+on EBCDIC-based hosts.
+.
+(In this installation,
+.B @DEVICE@
+is the default output device.)
+.
+Use
+.IR groff 's
+.B \-P
+option to pass any options shown above to
+.IR grotty .
+.
+If no
+.I file
+arguments are given,
+or if
+.I file
+is \[lq]\-\[rq],
+.I grotty
+reads the standard input stream.
+.
+Output is written to the standard output stream.
+.
+.
+.P
+By default,
+.I grotty
+emits SGR escape sequences
+(from ISO\~6429,
+popularly called \[lq]ANSI escapes\[rq])
+to change text attributes
+(bold,
+italic,
+underline,
+reverse video
+.\" ECMA-48, 2nd edition (1979) calls it "negative image".
+[\[lq]negative image\[rq]]
+and colors).
+.
+Devices supporting the appropriate sequences can view
+.I roff
+documents using eight different background and foreground colors.
+.
+Following ISO\~6429,
+the following colors are defined in
+.IR tty.tmac :
+black,
+white,
+red,
+green,
+blue,
+yellow,
+magenta,
+and cyan.
+.
+Unrecognized colors are mapped to the default color,
+which is dependent on the settings of the terminal.
+.
+OSC\~8 hyperlinks are produced for these devices.
+.
+.
+.P
+In keeping with long-standing practice and the rarity of terminals
+(and emulators)
+that support oblique or italic fonts,
+italicized text is represented with underlining by default\[em]but see
+the
+.B \-i
+option below.
+.
+.
+.\" ====================================================================
+.SS "SGR and OSC support in pagers"
+.\" ====================================================================
+.
+When paging
+.IR grotty 's
+output with
+.MR less 1 ,
+the latter program must be instructed to pass SGR and OSC sequences
+through to the device;
+its
+.B \-R
+option is one way to achieve this
+.RI ( less
+version 566 or later is required for OSC\~8 support).
+.
+Consequently,
+programs like
+.MR man 1
+that page
+.I roff
+documents with
+.I less
+must call it with an appropriate option.
+.
+.
+.\" ====================================================================
+.SS "Legacy output format"
+.\" ====================================================================
+.
+The
+.B \-c
+option tells
+.I grotty
+to use an output format compatible with paper terminals,
+like the Teletype machines for which
+.I roff
+and
+.I nroff
+were first developed but which are no longer in wide use.
+.
+SGR escape sequences are not emitted;
+bold,
+italic,
+and underlining character attributes are thus not manipulated.
+.
+Instead,
+.I grotty
+overstrikes,
+representing a bold character
+.I c
+with the sequence
+.RI \[lq] c\~\c
+BACKSPACE\~\c
+.IR c \[rq],
+an italic character
+.I c
+with the sequence
+.RB \[lq] _\~\c
+BACKSPACE\~\c
+.IR c \[rq],
+and bold italics with
+.RB \[lq] _\~\c
+BACKSPACE\~\c
+.I c
+BACKSPACE\~\c
+.IR c \[rq].
+.
+This rendering is inherently ambiguous when the character
+.I c
+is itself the underscore.
+.
+.
+.P
+The legacy output format can be rendered on a video terminal
+(or emulator)
+by piping
+.IR grotty 's
+output through
+.MR ul 1 ,
+.\" from bsdmainutils 11.1.2+b1 (on Debian Buster)
+which may render bold italics as reverse video.
+.
+.\" 'more' from util-linux 2.33.1 (on Debian Buster) neither renders
+.\" double-struck characters as bold nor supports -b, but does render
+.\" SGR sequences (including color) with no flags required.
+Some implementations of
+.MR more 1
+are also able to display these sequences;
+you may wish to experiment with that command's
+.B \-b
+option.
+.
+.\" Version 487 of...
+.I less
+renders legacy bold and italics without requiring options.
+.
+In contrast to the terminal output drivers of some other
+.I roff
+implementations,
+.I grotty
+never outputs reverse line feeds.
+.
+There is therefore no need to filter its output through
+.MR col 1 .
+.
+.
+.\" ====================================================================
+.SS "Device control commands"
+.\" ====================================================================
+.
+.I grotty
+understands one device control function produced by the
+.I roff
+.B \[rs]X
+escape sequence in a document.
+.
+.
+.TP
+.BR "\[rs]X\[aq]tty: link " [\c
+.IR uri \~[ key\c
+.BI = value\c
+] \|.\|.\|.\|]\c
+.B \[aq]
+.
+Embed a hyperlink using the OSC 8 terminal escape sequence.
+.
+Specifying
+.I uri
+starts hyperlinked text,
+and omitting it ends the hyperlink.
+.
+When
+.I uri
+is present,
+any number of additional key/value pairs can be specified;
+their interpretation is the responsibility of the pager or terminal.
+.
+Spaces or tabs cannot appear literally in
+.IR uri ,
+.IR key ,
+or
+.IR value ;
+they must be represented in an alternate form.
+.
+.
+.\" ====================================================================
+.SS "Device description files"
+.\" ====================================================================
+.
+If the
+.I DESC
+file for the character encoding contains the
+.RB \[lq] unicode \[rq]
+directive,
+.I grotty
+emits Unicode characters in UTF-8 encoding.
+.
+Otherwise,
+it emits characters in a single-byte encoding depending on the data in
+the font description files.
+.
+See
+.MR groff_font @MAN5EXT@ .
+.
+.
+.P
+A font description file may contain a directive
+.RB \[lq] internalname\~\c
+.IR n \[rq]
+where
+.I n
+is a decimal integer.
+.
+If the 01 bit in
+.I n
+is set,
+then the font is treated as an italic font;
+if the 02 bit is set,
+then it is treated as a bold font.
+.
+.\" The following seems to say nothing that is not true of font
+.\" description files in general; if so, it belongs in groff_font(5).
+.\"The code field in the font description field gives the code which is
+.\"used to output the character.
+.\".
+.\"This code can also be used in the
+.\".I groff
+.\".B \[rs]N
+.\"escape sequence in a document.
+.
+.
+.\" ====================================================================
+.SS Typefaces
+.\" ====================================================================
+.
+.I grotty
+supports the standard four styles:
+.B R
+(roman),
+.B I
+.RI ( italic ),
+.B B
+.RB ( bold ),
+and
+.B BI
+(\f[BI]bold-italic\f[]).
+.
+Because the output driver operates in
+.I nroff
+mode,
+attempts to set or change the font family or type size are ignored.
+.
+.
+.
+.\" ====================================================================
+.SH Options
+.\" ====================================================================
+.
+.B \-\-help
+displays a usage message,
+while
+.B \-v
+and
+.B \-\-version
+show version information;
+all exit afterward.
+.
+.
+.TP
+.B \-b
+Suppress the use of overstriking for bold characters in legacy output
+format.
+.
+.
+.TP
+.B \-B
+Use only overstriking for bold-italic characters in legacy output
+format.
+.
+.
+.TP
+.B \-c
+Use
+.IR grotty 's
+legacy output format
+(see subsection \[lq]Legacy output format\[rq] above).
+.
+SGR and OSC escape sequences are not emitted.
+.
+.
+.TP
+.B \-d
+Ignore all
+.B \[rs]D
+drawing escape sequences in the input.
+.
+By default,
+.I grotty
+renders
+.BR \[rs]D\[aq]l \|.\|.\|.\& \[aq]
+escape sequences that have at least one zero argument
+(and so are either horizontal or vertical)
+using Unicode box drawing characters
+(for the
+.B utf8
+device)
+or the
+.BR \- ,
+.BR | ,
+and
+.B +
+characters
+(for all other devices).
+.
+.I grotty
+handles
+.BR \[rs]D\[aq]p \|.\|.\|.\& \[aq]
+escape sequences that consist entirely of horizontal and vertical
+lines similarly.
+.
+.
+.TP
+.B \-f
+Emit a form feed at the end of each page having no output on its last
+line.
+.
+.
+.TP
+.BI \-F\~ dir
+Prepend directory
+.RI dir /dev name
+to the search path for font and device description files;
+.I name
+describes the output device's character encoding,
+one of
+.BR ascii ,
+.BR latin1 ,
+.BR utf8 ,
+or
+.BR cp1047 .
+.
+.
+.TP
+.B \-h
+Use literal horizontal tab characters in the output.
+.
+Tabs are assumed to be set every 8 columns.
+.
+.
+.TP
+.B \-i
+Render oblique-styled fonts
+.RB ( I
+and
+.BR BI )
+with the SGR attribute for italic text
+rather than underlined text.
+.
+Many terminals don't support this attribute;
+however,
+.MR xterm 1 ,
+since patch\~#314 (2014-12-28),
+does.
+.
+Ignored if
+.B \-c
+is also specified.
+.
+.
+.TP
+.B \-o
+Suppress overstriking
+(other than for bold and/or underlined characters when the legacy output
+format is in use).
+.
+.
+.TP
+.B \-r
+Render oblique-styled fonts
+.RB ( I
+and
+.BR BI )
+with the SGR attribute for reverse video text
+rather than underlined text.
+.
+Ignored if
+.B \-c
+or
+.B \-i
+is also specified.
+.
+.
+.TP
+.B \-u
+Suppress the use of underlining for italic characters in legacy output
+format.
+.
+.
+.TP
+.B \-U
+Use only underlining for bold-italic characters in legacy output format.
+.
+.
+.\" ====================================================================
+.SH Environment
+.\" ====================================================================
+.
+.TP
+.I GROFF_FONT_PATH
+A list of directories in which to seek the selected output device's
+directory of device and font description files.
+.
+See
+.MR @g@troff @MAN1EXT@
+and
+.MR groff_font @MAN5EXT@ .
+.
+.
+.TP
+.I GROFF_NO_SGR
+If set,
+.IR grotty 's
+legacy output format is used just as if the
+.B \-c
+option were specified;
+see subsection \[lq]Legacy output format\[rq] above.
+.
+.
+.br
+.ne 3v \" Keep section heading and paragraph tag together.
+.\" ====================================================================
+.SH Files
+.\" ====================================================================
+.
+.TP
+.I @FONTDIR@/\:\%devascii/\:DESC
+describes the
+.B ascii
+output device.
+.
+.
+.TP
+.IR @FONTDIR@/\:\%devascii/ F
+describes the font known
+.RI as\~ F
+on device
+.BR ascii .
+.
+.
+.TP
+.I @FONTDIR@/\:\%devcp1047/\:DESC
+describes the
+.B cp1047
+output device.
+.
+.
+.TP
+.IR @FONTDIR@/\:\%devcp1047/ F
+describes the font known
+.RI as\~ F
+on device
+.BR cp1047 .
+.
+.
+.TP
+.I @FONTDIR@/\:\%devlatin1/\:DESC
+describes the
+.B latin1
+output device.
+.
+.
+.TP
+.IR @FONTDIR@/\:\%devlatin1/ F
+describes the font known
+.RI as\~ F
+on device
+.BR latin1 .
+.
+.
+.TP
+.I @FONTDIR@/\:\%devutf8/\:DESC
+describes the
+.B utf8
+output device.
+.
+.
+.TP
+.IR @FONTDIR@/\:\%devutf8/ F
+describes the font known
+.RI as\~ F
+on device
+.BR utf8 .
+.
+.
+.TP
+.I @MACRODIR@/\:tty\:.tmac
+defines macros for use with the
+.BR ascii ,
+.BR cp1047 ,
+.BR latin1 ,
+and
+.B utf8
+output devices.
+.
+It is automatically loaded by
+.I troffrc
+when any of those output devices is selected.
+.
+.
+.TP
+.I @MACRODIR@/\:tty\-char\:.tmac
+defines fallback characters for use with
+.I grotty.
+.
+See
+.MR nroff @MAN1EXT@ .
+.
+.
+.\" XXX: The following no longer seems to be true; an inspection of the
+.\" font/*/dev*.am files suggests no evidence of it, at any rate.
+.\".P
+.\"Note that on EBCDIC hosts,
+.\"only files for the
+.\".B cp1047
+.\"device are installed.
+.
+.
+.\" ====================================================================
+.SH Limitations
+.\" ====================================================================
+.
+.I grotty
+is intended only for simple documents.
+.
+.
+.IP \[bu] 2n
+There is no support for fractional horizontal or vertical motions.
+.
+.
+.IP \[bu]
+.I roff
+.B \[rs]D
+escape sequences producing anything other than horizontal and vertical
+lines are not supported.
+.
+.
+.IP \[bu]
+Characters above the first line
+(that is,
+with a vertical drawing position of\~0)
+cannot be rendered.
+.
+.
+.IP \[bu]
+Color handling differs from other output drivers.
+.
+The
+.I groff
+requests and escape sequences that set the stroke and fill colors
+instead set the foreground and background character cell colors,
+respectively.
+.
+.
+.\" ====================================================================
+.SH Examples
+.\" ====================================================================
+.
+The following
+.I groff
+document exercises several features for which output device support
+varies:
+(1)\~bold style;
+(2)\~italic (underline) style;
+(3)\~bold-italic style;
+(4)\~character composition by overstriking (\[lq]co\[:o]perate\[rq]);
+(5)\~foreground color;
+(6)\~background color;
+and
+(7)\~horizontal and vertical line-drawing.
+.
+.
+.P
+.RS
+.EX
+You might see \ef[B]bold\ef[] and \ef[I]italic\ef[].
+Some people see \ef[BI]both\ef[].
+If the output device does (not) co\ez\e[ad]operate,
+you might see \em[red]red\em[].
+Black on cyan can have a \eM[cyan]\em[black]prominent\em[]\eM[]
+\eD\[aq]l 1i 0\[aq]\eD\[aq]l 0 2i\[aq]\eD\[aq]l 1i 0\[aq] look.
+\&.\e" If in nroff mode, end page now.
+\&.if n .pl \en[nl]u
+.EE
+.RE
+.
+.
+.P
+Given the foregoing input,
+compare and contrast the output of the following.
+.
+.
+.P
+.RS
+.EX
+$ \c
+.B groff \-T ascii \c
+.I file
+$ \c
+.B groff \-T utf8 \-P \-i \c
+.I file
+$ \c
+.B groff \-T utf8 \-P \-c \c
+.I file \c
+.B | ul
+.EE
+.RE
+.
+.
+.\" ====================================================================
+.SH "See also"
+.\" ====================================================================
+.
+\[lq]Control Functions for Coded Character Sets\[rq]
+(ECMA-48)
+5th\~edition,
+Ecma International,
+June\~1991.
+.
+A gratis version of ISO\~6429,
+this document includes a normative description of SGR escape sequences.
+.
+Available at
+.UR http://\:www\:.ecma\-international\:.org/\:publications/\:files/\:\
+ECMA\-ST/\:Ecma\-048\:.pdf
+.UE .
+.
+.
+.P
+.UR https://\:gist\:.github\:.com/\:egmontkob/\:\
+eb114294efbcd5ad\:b1944c9f3cb5feda
+\[lq]Hyperlinks in Terminal Emulators\[rq]
+.UE ,
+Egmont Koblinger.
+.
+.
+.P
+.MR groff @MAN1EXT@ ,
+.MR @g@troff @MAN1EXT@ ,
+.MR groff_out @MAN5EXT@ ,
+.MR groff_font @MAN5EXT@ ,
+.MR groff_char @MAN7EXT@ ,
+.MR ul 1 ,
+.MR more 1 ,
+.MR less 1 ,
+.MR man 1
+.
+.
+.\" Restore compatibility mode (for, e.g., Solaris 10/11).
+.cp \n[*groff_grotty_1_man_C]
+.do rr *groff_grotty_1_man_C
+.
+.
+.\" Local Variables:
+.\" fill-column: 72
+.\" mode: nroff
+.\" End:
+.\" vim: set filetype=groff textwidth=72:
diff --git a/src/devices/grotty/grotty.am b/src/devices/grotty/grotty.am
new file mode 100644
index 0000000..14921c5
--- /dev/null
+++ b/src/devices/grotty/grotty.am
@@ -0,0 +1,39 @@
+# Copyright (C) 2014-2021 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+bin_PROGRAMS += grotty
+grotty_SOURCES = src/devices/grotty/tty.cpp
+grotty_LDADD = $(LIBM) \
+ libdriver.a \
+ libgroff.a \
+ lib/libgnu.a
+man1_MANS += src/devices/grotty/grotty.1
+EXTRA_DIST += \
+ src/devices/grotty/grotty.1.man \
+ src/devices/grotty/TODO
+
+grotty_TESTS = \
+ src/devices/grotty/tests/basic_latin_glyphs_map_correctly.sh \
+ src/devices/grotty/tests/osc8_works.sh
+TESTS += $(grotty_TESTS)
+EXTRA_DIST += $(grotty_TESTS)
+
+# Local Variables:
+# fill-column: 72
+# mode: makefile-automake
+# End:
+# vim: set autoindent filetype=automake textwidth=72:
diff --git a/src/devices/grotty/tests/basic_latin_glyphs_map_correctly.sh b/src/devices/grotty/tests/basic_latin_glyphs_map_correctly.sh
new file mode 100755
index 0000000..92552d2
--- /dev/null
+++ b/src/devices/grotty/tests/basic_latin_glyphs_map_correctly.sh
@@ -0,0 +1,205 @@
+#!/bin/sh
+#
+# Copyright (C) 2022 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+grotty="${abs_top_builddir:-.}/grotty"
+
+fail=
+
+wail () {
+ printf "FAILED " >&2
+ fail=YES
+}
+
+# Ensure that characters are mapped to glyphs normatively.
+
+input='x T @DEVICE@
+x res 240 24 40
+x init
+p1
+x font 1 R
+f1
+s10
+V40
+H0
+md
+DFd
+t!#$%&()*+,./0123456789:;<=>?@
+n40 0
+V80
+H0
+tABCDEFGHIJKLMNOPQRSTUVWXYZ[]_
+n40 0
+V120
+H0
+tabcdefghijklmnopqrstuvwxyz{|}
+n40 0
+V160
+H0
+tneutral
+wh24
+tdouble
+wh24
+tquote:
+wh24
+Cdq
+h24
+n40 0
+V200
+H0
+tclosing
+wh24
+tsingle
+wh24
+tquote:
+wh24
+t'"'"'
+n40 0
+V240
+H0
+thyphen:
+wh24
+t-
+n40 0
+V280
+H0
+tbackslash:
+wh24
+Crs
+h24
+n40 0
+V320
+H0
+tmodifier
+wh24
+tcircumflex:
+wh24
+t^
+n40 0
+V360
+H0
+topening
+wh24
+tsingle
+wh24
+tquote:
+wh24
+t`
+n40 0
+V400
+H0
+tmodifier
+wh24
+ttilde:
+wh24
+t~
+n40 0
+x trailer
+V2640
+x stop'
+
+# TODO: Test cp1047 when we have access to a host environment using it.
+
+for D in ascii latin1 utf8
+do
+ if [ "$D" = "utf8" ]
+ then
+ # We can't test UTF-8 if the environment doesn't support it.
+ if [ "$(locale charmap)" != UTF-8 ]
+ then
+ # If we've already seen a failure case, report it.
+ if [ -n "$fail" ]
+ then
+ exit 1 # fail
+ else
+ exit 77 # skip
+ fi
+ fi
+ fi
+
+ printf 'checking "%s" output device...' $D >&2
+ output=$(echo "$input" | sed s/@DEVICE@/$D/ \
+ | "$grotty" -F font -F build/font)
+ printf 'group1 ' >&2
+ echo "$output" | grep -Fqx '!#$%&()*+,./0123456789:;<=>?@' || wail
+ printf 'group2 ' >&2
+ echo "$output" | grep -Fqx 'ABCDEFGHIJKLMNOPQRSTUVWXYZ[]_' || wail
+ printf 'group3 ' >&2
+ echo "$output" | grep -Fqx 'abcdefghijklmnopqrstuvwxyz{|}' || wail
+ printf '" ' >&2
+ echo "$output" | grep -Fqx 'neutral double quote: "' || wail
+ printf '\\ ' >&2
+ echo "$output" | grep -Fqx 'backslash: \' || wail
+ case $D in
+ (utf8)
+# Expected:
+#0000000 ! # $ % & ( ) * + , . / 0 1 2 3
+#0000020 4 5 6 7 8 9 : ; < = > ? @ \n A B
+#0000040 C D E F G H I J K L M N O P Q R
+#0000060 S T U V W X Y Z [ ] _ \n a b c d
+#0000100 e f g h i j k l m n o p q r s t
+#0000120 u v w x y z { | } \n n e u t r a
+#0000140 l d o u b l e q u o t e :
+#0000160 " \n c l o s i n g s i n g l e
+#0000200 q u o t e : 342 200 231 \n h y p h
+#0000220 e n : 342 200 220 \n b a c k s l a s
+#0000240 h : \ \n m o d i f i e r c i
+#0000260 r c u m f l e x : 313 206 \n o p e
+#0000300 n i n g s i n g l e q u o t
+#0000320 e : 342 200 230 \n m o d i f i e r
+#0000340 t i l d e : 313 234 \n
+#0000352
+ output_od=$(echo "$output" | LC_ALL=C od -t c)
+ printf "' " >&2
+ printf '%s\n' "$output_od" \
+ | grep -Eq '0000200 +q +u +o +t +e +: +342 +200 +231' \
+ || wail
+ printf '` ' >&2
+ printf '%s\n' "$output_od" \
+ | grep -Eq '0000320 +e +: +342 +200 +230' || wail
+ printf "%s " '-' >&2
+ printf '%s\n' "$output_od" \
+ | grep -Eq '0000220 +e +n +: +342 +200 +220' || wail
+ printf '^ ' >&2
+ printf '%s\n' "$output_od" \
+ | grep -Eq '0000260 +r +c +u +m +f +l +e +x +: +313 +206' \
+ || wail
+ printf "~ " >&2
+ printf '%s\n' "$output_od" \
+ | grep -Eq '0000340 +t +i +l +d +e +: +313 +234' || wail
+ ;;
+ (*)
+ printf '` ' >&2
+ echo "$output" | grep -Fqx 'opening single quote: `' || wail
+ printf "' " >&2
+ echo "$output" | grep -Fqx "closing single quote: '" || wail
+ printf "%s " '-' >&2
+ echo "$output" | grep -Fqx "hyphen: -" || wail
+ printf '^ ' >&2
+ echo "$output" | grep -Fqx "modifier circumflex: ^" || wail
+ printf "~ " >&2
+ echo "$output" | grep -Fqx "modifier tilde: ~" || wail
+ ;;
+ esac
+ echo >&2
+done
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/devices/grotty/tests/osc8_works.sh b/src/devices/grotty/tests/osc8_works.sh
new file mode 100755
index 0000000..db6480d
--- /dev/null
+++ b/src/devices/grotty/tests/osc8_works.sh
@@ -0,0 +1,119 @@
+#!/bin/sh
+#
+# Copyright (C) 2021 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+set -e
+
+grotty="${abs_top_builddir:-.}/grotty"
+
+input="x T utf8
+x res 240 24 40
+x init
+p1
+x font 1 R
+f1
+s10
+V40
+H0
+md
+DFd
+tA
+n40 0
+x X tty: link
+x X tty: link h
+x X tty: link http://example.com/1
+x X tty: link
+x X tty: link http://example.com/2
+tB
+x X tty: link
+x X tty: link mailto:g.branden.robinson@gmail.com
+tBranden
+x X tty: link
+x trailer
+V2640
+x stop"
+
+# We expect diagnostics from the first few "x X tty: link" lines. The
+# first should complain about a link ending without having been started.
+# The second is bogus ("h") but it's not grotty's job to validate the
+# structure of a URI. The third should draw complaint because we didn't
+# end the (bogus) URI that we started with the second.
+
+# The remaining input is well-formed. The URI ending in "1" is
+# effectively hidden because no character cells are drawn while it is
+# active.
+echo "expect two diagnostic messages regarding ill-formed links" >&2
+output=$(echo "$input" | "$grotty" -F font -F build/font | od -t c)
+
+# Expected:
+#0000000 A 033 ] 8 ; ; 033 \ 033 ] 8 ; ; h 033 \
+#0000020 033 ] 8 ; ; 033 \ 033 ] 8 ; ; h t t p
+#0000040 : / / e x a m p l e . c o m / 1
+#0000060 033 \ 033 ] 8 ; ; 033 \ 033 ] 8 ; ; h t
+#0000100 t p : / / e x a m p l e . c o m
+#0000120 / 2 033 \ B 033 ] 8 ; ; 033 \ 033 ] 8 ;
+#0000140 ; m a i l t o : g . b r a n d e
+#0000160 n . r o b i n s o n @ g m a i l
+#0000200 . c o m 033 \ B r a n d e n 033 ] 8
+#0000220 ; ; 033 \ \n \n \n \n \n \n \n \n \n \n \n \n
+#0000240 \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
+#*
+#0000320 \n \n \n \n \n \n
+#0000326
+
+echo "testing for URI that corresponds to no character cells" >&2
+echo "$output" | grep -Eq 'A 033 +] +8 +; +; +033 +\\'
+
+echo "testing http URI (1)" >&2
+echo "$output" | grep -Eq '0000020 +.*033 +] +8 +; +; +h + t +t +p'
+
+echo "testing http URI (2)" >&2
+echo "$output" | grep -Eq '0000040 +: +/ +/ +e +x +a +m +p +l +e +\. +c'
+
+echo "testing http URI (3)" >&2
+echo "$output" | grep -Eq '0000040.* +o +m +/ +1'
+
+echo "testing http URI (4)" >&2
+echo "$output" | grep -Eq '0000060 +033 +\\'
+
+echo "testing mailto URI (1)" >&2
+echo "$output" | grep -Eq '0000120 +.* +033 +] +8 +;$'
+
+echo "testing mailto URI (2)" >&2
+echo "$output" | grep -Eq '0000140 +; +m +a +i +l +t +o +: +g +\. +b'
+
+echo "testing mailto URI (3)" >&2
+echo "$output" | grep -Eq '0000140.* +r +a +n +d +e$'
+
+echo "testing mailto URI (4)" >&2
+echo "$output" | grep -Eq '0000160 +n +\. +r +o +b +i +n +s +o +n +@'
+
+echo "testing mailto URI (5)" >&2
+echo "$output" | grep -Eq '0000160.* +g +m +a +i +l$'
+
+echo "testing mailto URI (6)" >&2
+echo "$output" | grep -Eq '0000200 +\. +c +o +m +033 +\\ +B +r +a +n +d'
+
+echo "testing mailto URI (7)" >&2
+echo "$output" | grep -Eq '0000200.* +e +n +033 +] +8$'
+
+echo "testing mailto URI (8)" >&2
+echo "$output" | grep -Eq '0000220 +; +; +033 +\\'
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/devices/grotty/tty.cpp b/src/devices/grotty/tty.cpp
new file mode 100644
index 0000000..45926f6
--- /dev/null
+++ b/src/devices/grotty/tty.cpp
@@ -0,0 +1,1043 @@
+/* Copyright (C) 1989-2021 Free Software Foundation, Inc.
+ Written by James Clark (jjc@jclark.com)
+ OSC 8 support by G. Branden Robinson
+
+This file is part of groff.
+
+groff is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+groff is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "driver.h"
+#include "device.h"
+#include "ptable.h"
+
+typedef signed char schar;
+
+declare_ptable(schar)
+implement_ptable(schar)
+
+extern "C" const char *Version_string;
+
+#define putstring(s) fputs(s, stdout)
+
+#ifndef SHRT_MIN
+#define SHRT_MIN (-32768)
+#endif
+
+#ifndef SHRT_MAX
+#define SHRT_MAX 32767
+#endif
+
+#define TAB_WIDTH 8
+
+// A character of the output device fits in a 32-bit word.
+typedef unsigned int output_character;
+
+static bool want_horizontal_tabs = false;
+static bool want_form_feeds = false;
+static bool want_emboldening_by_overstriking = true;
+static bool do_bold;
+static bool want_italics_by_underlining = true;
+static bool do_underline;
+static bool want_glyph_composition_by_overstriking = true;
+static bool allow_drawing_commands = true;
+static bool want_sgr_italics = false;
+static bool do_sgr_italics;
+static bool want_reverse_video_for_italics = false;
+static bool do_reverse_video;
+static bool use_overstriking_drawing_scheme = false;
+
+static void update_options();
+static void usage(FILE *stream);
+
+static int hline_char = '-';
+static int vline_char = '|';
+
+enum {
+ UNDERLINE_MODE = 0x01,
+ BOLD_MODE = 0x02,
+ VDRAW_MODE = 0x04,
+ HDRAW_MODE = 0x08,
+ CU_MODE = 0x10,
+ COLOR_CHANGE = 0x20,
+ START_LINE = 0x40,
+ END_LINE = 0x80
+};
+
+// Mode to use for bold-underlining.
+static unsigned char bold_underline_mode_option = BOLD_MODE|UNDERLINE_MODE;
+static unsigned char bold_underline_mode;
+
+#ifndef IS_EBCDIC_HOST
+#define CSI "\033["
+#define OSC8 "\033]8"
+#define ST "\033\\"
+#else
+#define CSI "\047["
+#define OSC8 "\047]8"
+#define ST "\047\\"
+#endif
+
+// SGR handling (ISO 6429)
+#define SGR_BOLD CSI "1m"
+#define SGR_NO_BOLD CSI "22m"
+#define SGR_ITALIC CSI "3m"
+#define SGR_NO_ITALIC CSI "23m"
+#define SGR_UNDERLINE CSI "4m"
+#define SGR_NO_UNDERLINE CSI "24m"
+#define SGR_REVERSE CSI "7m"
+#define SGR_NO_REVERSE CSI "27m"
+// many terminals can't handle 'CSI 39 m' and 'CSI 49 m' to reset
+// the foreground and background color, respectively; we thus use
+// 'CSI 0 m' exclusively
+#define SGR_DEFAULT CSI "0m"
+
+#define DEFAULT_COLOR_IDX -1
+
+class tty_font : public font {
+ tty_font(const char *);
+ unsigned char mode;
+public:
+ ~tty_font();
+ unsigned char get_mode() { return mode; }
+#if 0
+ void handle_x_command(int argc, const char **argv);
+#endif
+ static tty_font *load_tty_font(const char *);
+};
+
+tty_font *tty_font::load_tty_font(const char *s)
+{
+ tty_font *f = new tty_font(s);
+ if (!f->load()) {
+ delete f;
+ return 0;
+ }
+ const char *num = f->get_internal_name();
+ long n;
+ if (num != 0 && (n = strtol(num, 0, 0)) != 0)
+ f->mode = (unsigned char)(n & (BOLD_MODE|UNDERLINE_MODE));
+ if (!do_underline)
+ f->mode &= ~UNDERLINE_MODE;
+ if (!do_bold)
+ f->mode &= ~BOLD_MODE;
+ if ((f->mode & (BOLD_MODE|UNDERLINE_MODE)) == (BOLD_MODE|UNDERLINE_MODE))
+ f->mode = (unsigned char)((f->mode & ~(BOLD_MODE|UNDERLINE_MODE))
+ | bold_underline_mode);
+ return f;
+}
+
+tty_font::tty_font(const char *nm)
+: font(nm), mode(0)
+{
+}
+
+tty_font::~tty_font()
+{
+}
+
+#if 0
+void tty_font::handle_x_command(int argc, const char **argv)
+{
+ if (argc >= 1 && strcmp(argv[0], "bold") == 0)
+ mode |= BOLD_MODE;
+ else if (argc >= 1 && strcmp(argv[0], "underline") == 0)
+ mode |= UNDERLINE_MODE;
+}
+#endif
+
+class tty_glyph {
+public:
+ tty_glyph *next;
+ int w;
+ int hpos;
+ unsigned int code;
+ unsigned char mode;
+ schar back_color_idx;
+ schar fore_color_idx;
+ inline int draw_mode() { return mode & (VDRAW_MODE|HDRAW_MODE); }
+ inline int order() {
+ return mode & (VDRAW_MODE|HDRAW_MODE|CU_MODE|COLOR_CHANGE); }
+};
+
+
+class tty_printer : public printer {
+ tty_glyph **lines;
+ int nlines;
+ int cached_v;
+ int cached_vpos;
+ schar curr_fore_idx;
+ schar curr_back_idx;
+ bool is_underlining;
+ bool is_boldfacing;
+ bool is_continuously_underlining;
+ PTABLE(schar) tty_colors;
+ void make_underline(int);
+ void make_bold(output_character, int);
+ schar color_to_idx(color *);
+ void add_char(output_character, int, int, int, color *, color *,
+ unsigned char);
+ void simple_add_char(const output_character, const environment *);
+ char *make_rgb_string(unsigned int, unsigned int, unsigned int);
+ bool tty_color(unsigned int, unsigned int, unsigned int, schar *,
+ schar = DEFAULT_COLOR_IDX);
+ void line(int, int, int, int, color *, color *);
+ void draw_line(int *, int, const environment *);
+ void draw_polygon(int *, int, const environment *);
+ void special_link(const char *, const environment *);
+public:
+ tty_printer();
+ ~tty_printer();
+ void set_char(glyph *, font *, const environment *, int, const char *);
+ void draw(int, int *, int, const environment *);
+ void special(char *, const environment *, char);
+ void change_color(const environment * const);
+ void change_fill_color(const environment * const);
+ void put_char(output_character);
+ void put_color(schar, int);
+ void begin_page(int) { }
+ void end_page(int);
+ font *make_font(const char *);
+};
+
+char *tty_printer::make_rgb_string(unsigned int r,
+ unsigned int g,
+ unsigned int b)
+{
+ char *s = new char[8];
+ s[0] = char(r >> 8);
+ s[1] = char(r & 0xff);
+ s[2] = char(g >> 8);
+ s[3] = char(g & 0xff);
+ s[4] = char(b >> 8);
+ s[5] = char(b & 0xff);
+ s[6] = char(0x80);
+ s[7] = 0;
+ // avoid null-bytes in string
+ for (int i = 0; i < 6; i++)
+ if (!s[i]) {
+ s[i] = 1;
+ s[6] |= 1 << i;
+ }
+ return s;
+}
+
+bool tty_printer::tty_color(unsigned int r,
+ unsigned int g,
+ unsigned int b, schar *idx, schar value)
+{
+ bool is_known_color = true;
+ char *s = make_rgb_string(r, g, b);
+ schar *i = tty_colors.lookup(s);
+ if (!i) {
+ is_known_color = false;
+ i = new schar[1];
+ *i = value;
+ tty_colors.define(s, i);
+ }
+ *idx = *i;
+ delete[] s;
+ return is_known_color;
+}
+
+tty_printer::tty_printer() : cached_v(0)
+{
+ if (font::is_unicode) {
+ hline_char = 0x2500;
+ vline_char = 0x2502;
+ }
+ schar dummy;
+ // black, white
+ (void)tty_color(0, 0, 0, &dummy, 0);
+ (void)tty_color(color::MAX_COLOR_VAL,
+ color::MAX_COLOR_VAL,
+ color::MAX_COLOR_VAL, &dummy, 7);
+ // red, green, blue
+ (void)tty_color(color::MAX_COLOR_VAL, 0, 0, &dummy, 1);
+ (void)tty_color(0, color::MAX_COLOR_VAL, 0, &dummy, 2);
+ (void)tty_color(0, 0, color::MAX_COLOR_VAL, &dummy, 4);
+ // yellow, magenta, cyan
+ (void)tty_color(color::MAX_COLOR_VAL, color::MAX_COLOR_VAL, 0, &dummy, 3);
+ (void)tty_color(color::MAX_COLOR_VAL, 0, color::MAX_COLOR_VAL, &dummy, 5);
+ (void)tty_color(0, color::MAX_COLOR_VAL, color::MAX_COLOR_VAL, &dummy, 6);
+ nlines = 66;
+ lines = new tty_glyph *[nlines];
+ for (int i = 0; i < nlines; i++)
+ lines[i] = 0;
+ is_continuously_underlining = false;
+}
+
+tty_printer::~tty_printer()
+{
+ delete[] lines;
+}
+
+void tty_printer::make_underline(int w)
+{
+ if (use_overstriking_drawing_scheme) {
+ if (!w)
+ warning("can't underline zero-width character");
+ else {
+ putchar('_');
+ putchar('\b');
+ }
+ }
+ else {
+ if (!is_underlining) {
+ if (do_sgr_italics)
+ putstring(SGR_ITALIC);
+ else if (do_reverse_video)
+ putstring(SGR_REVERSE);
+ else
+ putstring(SGR_UNDERLINE);
+ }
+ is_underlining = true;
+ }
+}
+
+void tty_printer::make_bold(output_character c, int w)
+{
+ if (use_overstriking_drawing_scheme) {
+ if (!w)
+ warning("can't print zero-width character in bold");
+ else {
+ put_char(c);
+ putchar('\b');
+ }
+ }
+ else {
+ if (!is_boldfacing)
+ putstring(SGR_BOLD);
+ is_boldfacing = true;
+ }
+}
+
+schar tty_printer::color_to_idx(color *col)
+{
+ if (col->is_default())
+ return DEFAULT_COLOR_IDX;
+ unsigned int r, g, b;
+ col->get_rgb(&r, &g, &b);
+ schar idx;
+ if (!tty_color(r, g, b, &idx)) {
+ char *s = col->print_color();
+ error("unrecognized color '%1' mapped to default", s);
+ delete[] s;
+ }
+ return idx;
+}
+
+void tty_printer::set_char(glyph *g, font *f, const environment *env,
+ int w, const char *)
+{
+ if (w % font::hor != 0)
+ fatal("glyph width is not a multiple of horizontal motion quantum");
+ add_char(f->get_code(g), w,
+ env->hpos, env->vpos,
+ env->col, env->fill,
+ ((tty_font *)f)->get_mode());
+}
+
+void tty_printer::add_char(output_character c, int w,
+ int h, int v,
+ color *fore, color *back,
+ unsigned char mode)
+{
+#if 0
+ // This is too expensive.
+ if (h % font::hor != 0)
+ fatal("horizontal position not a multiple of horizontal motion quantum");
+#endif
+ int hpos = h / font::hor;
+ if (hpos < SHRT_MIN || hpos > SHRT_MAX) {
+ error("character with ridiculous horizontal position discarded");
+ return;
+ }
+ int vpos;
+ if (v == cached_v && cached_v != 0)
+ vpos = cached_vpos;
+ else {
+ if (v % font::vert != 0)
+ fatal("vertical position not a multiple of vertical motion"
+ " quantum");
+ vpos = v / font::vert;
+ if (vpos > nlines) {
+ tty_glyph **old_lines = lines;
+ lines = new tty_glyph *[vpos + 1];
+ memcpy(lines, old_lines, nlines * sizeof(tty_glyph *));
+ for (int i = nlines; i <= vpos; i++)
+ lines[i] = 0;
+ delete[] old_lines;
+ nlines = vpos + 1;
+ }
+ // Note that the first output line corresponds to groff
+ // position font::vert.
+ if (vpos <= 0) {
+ error("output above first line discarded");
+ return;
+ }
+ cached_v = v;
+ cached_vpos = vpos;
+ }
+ tty_glyph *g = new tty_glyph;
+ g->w = w;
+ g->hpos = hpos;
+ g->code = c;
+ g->fore_color_idx = color_to_idx(fore);
+ g->back_color_idx = color_to_idx(back);
+ g->mode = mode;
+
+ // The list will be reversed later. After reversal, it must be in
+ // increasing order of hpos, with COLOR_CHANGE and CU specials before
+ // HDRAW characters before VDRAW characters before normal characters
+ // at each hpos, and otherwise in order of occurrence.
+
+ tty_glyph **pp;
+ for (pp = lines + (vpos - 1); *pp; pp = &(*pp)->next)
+ if ((*pp)->hpos < hpos
+ || ((*pp)->hpos == hpos && (*pp)->order() >= g->order()))
+ break;
+ g->next = *pp;
+ *pp = g;
+}
+
+void tty_printer::simple_add_char(const output_character c,
+ const environment *env)
+{
+ add_char(c, 0, env->hpos, env->vpos, env->col, env->fill, 0);
+}
+
+void tty_printer::special(char *arg, const environment *env, char type)
+{
+ if (type == 'u') {
+ add_char(*arg - '0', 0, env->hpos, env->vpos, env->col, env->fill,
+ CU_MODE);
+ return;
+ }
+ if (type != 'p')
+ return;
+ char *p;
+ for (p = arg; *p == ' ' || *p == '\n'; p++)
+ ;
+ char *tag = p;
+ for (; *p != '\0' && *p != ':' && *p != ' ' && *p != '\n'; p++)
+ ;
+ if (*p == '\0' || strncmp(tag, "tty", p - tag) != 0) {
+ error("X command without 'tty:' tag ignored");
+ return;
+ }
+ p++;
+ for (; *p == ' ' || *p == '\n'; p++)
+ ;
+ char *command = p;
+ for (; *p != '\0' && *p != ' ' && *p != '\n'; p++)
+ ;
+ if (*command == '\0') {
+ error("empty X command ignored");
+ return;
+ }
+ if (strncmp(command, "link", p - command) == 0)
+ special_link(p, env);
+ else
+ warning("unrecognized X command '%1' ignored", command);
+}
+
+// Produce an OSC 8 hyperlink. Given ditroff input of the form:
+// x X tty: link [URI[ KEY=VALUE] ...]
+// produce "OSC 8 [;KEY=VALUE];[URI] ST". KEY/VALUE pairs can be
+// repeated arbitrarily and are separated by colons. Omission of the
+// URI ends the hyperlink that was begun by specifying it. See
+// <https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda>.
+void tty_printer::special_link(const char *arg, const environment *env)
+{
+ static bool is_link_active = false;
+ if (use_overstriking_drawing_scheme)
+ return;
+ for (const char *s = OSC8; *s != '\0'; s++)
+ simple_add_char(*s, env);
+ simple_add_char(';', env);
+ char c = *arg;
+ if ('\0' == c || '\n' == c) {
+ simple_add_char(';', env);
+ if (!is_link_active)
+ warning("ending hyperlink when none was started");
+ is_link_active = false;
+ }
+ else {
+ // Our caller ensures that we see white space after 'link'.
+ assert(c == ' ' || c == '\t');
+ if (is_link_active) {
+ warning("new hyperlink started without ending previous one;"
+ " recovering");
+ simple_add_char(';', env);
+ for (const char *s = ST OSC8; *s != '\0'; s++)
+ simple_add_char(*s, env);
+ simple_add_char(';', env);
+ }
+ is_link_active = true;
+ do
+ c = *arg++;
+ while (c == ' ' || c == '\t');
+ arg--;
+ // The first argument is the URI.
+ const char *uri = arg;
+ do
+ c = *arg++;
+ while (c != '\0' && c != ' ' && c != '\t');
+ arg--;
+ ptrdiff_t uri_len = arg - uri;
+ // Any remaining arguments are "key=value" pairs.
+ const char *pair = 0;
+ bool done = false;
+ do {
+ if (pair != 0)
+ simple_add_char(':', env);
+ pair = arg;
+ bool in_pair = true;
+ do {
+ c = *arg++;
+ if ('\0' == c)
+ done = true;
+ else if (' ' == c || '\t' == c)
+ in_pair = false;
+ else
+ simple_add_char(c, env);
+ } while (!done && in_pair);
+ } while (!done);
+ simple_add_char(';', env);
+ for (size_t i = uri_len; i > 0; i--)
+ simple_add_char(*uri++, env);
+ }
+ for (const char *s = ST; *s != '\0'; s++)
+ simple_add_char(*s, env);
+}
+
+void tty_printer::change_color(const environment * const env)
+{
+ add_char(0, 0, env->hpos, env->vpos, env->col, env->fill, COLOR_CHANGE);
+}
+
+void tty_printer::change_fill_color(const environment * const env)
+{
+ add_char(0, 0, env->hpos, env->vpos, env->col, env->fill, COLOR_CHANGE);
+}
+
+void tty_printer::draw(int code, int *p, int np, const environment *env)
+{
+ if (!allow_drawing_commands)
+ return;
+ if (code == 'l')
+ draw_line(p, np, env);
+ else if (code == 'p')
+ draw_polygon(p, np, env);
+ else
+ warning("ignoring unsupported drawing command '%1'", char(code));
+}
+
+void tty_printer::draw_polygon(int *p, int np, const environment *env)
+{
+ if (np & 1) {
+ error("even number of arguments required for polygon");
+ return;
+ }
+ if (np == 0) {
+ error("no arguments for polygon");
+ return;
+ }
+ // We only draw polygons which consist entirely of horizontal and
+ // vertical lines.
+ int hpos = 0;
+ int vpos = 0;
+ for (int i = 0; i < np; i += 2) {
+ if (!(p[i] == 0 || p[i + 1] == 0))
+ return;
+ hpos += p[i];
+ vpos += p[i + 1];
+ }
+ if (!(hpos == 0 || vpos == 0))
+ return;
+ int start_hpos = env->hpos;
+ int start_vpos = env->vpos;
+ hpos = start_hpos;
+ vpos = start_vpos;
+ for (int i = 0; i < np; i += 2) {
+ line(hpos, vpos, p[i], p[i + 1], env->col, env->fill);
+ hpos += p[i];
+ vpos += p[i + 1];
+ }
+ line(hpos, vpos, start_hpos - hpos, start_vpos - vpos,
+ env->col, env->fill);
+}
+
+void tty_printer::draw_line(int *p, int np, const environment *env)
+{
+ if (np != 2) {
+ error("2 arguments required for line");
+ return;
+ }
+ line(env->hpos, env->vpos, p[0], p[1], env->col, env->fill);
+}
+
+void tty_printer::line(int hpos, int vpos, int dx, int dy,
+ color *col, color *fill)
+{
+ // XXX: zero-length lines get drawn as '+' crossings in nroff, even
+ // when there is no crossing, but they nevertheless occur frequently
+ // in input. Does tbl produce them?
+#if 0
+ if (0 == dx)
+ fatal("cannot draw zero-length horizontal line");
+ if (0 == dy)
+ fatal("cannot draw zero-length vertical line");
+#endif
+ if ((dx != 0) && (dy != 0))
+ warning("cannot draw diagonal line");
+ if (dx % font::hor != 0)
+ fatal("length of horizontal line %1 is not a multiple of horizontal"
+ " motion quantum %2", dx, font::hor);
+ if (dy % font::vert != 0)
+ fatal("length of vertical line %1 is not a multiple of vertical"
+ " motion quantum %2", dy, font::vert);
+ if (dx == 0) {
+ // vertical line
+ int v = vpos;
+ int len = dy;
+ if (len < 0) {
+ v += len;
+ len = -len;
+ }
+ if (len == 0)
+ add_char(vline_char, font::hor, hpos, v, col, fill,
+ VDRAW_MODE|START_LINE|END_LINE);
+ else {
+ add_char(vline_char, font::hor, hpos, v, col, fill,
+ VDRAW_MODE|START_LINE);
+ len -= font::vert;
+ v += font::vert;
+ while (len > 0) {
+ add_char(vline_char, font::hor, hpos, v, col, fill,
+ VDRAW_MODE|START_LINE|END_LINE);
+ len -= font::vert;
+ v += font::vert;
+ }
+ add_char(vline_char, font::hor, hpos, v, col, fill,
+ VDRAW_MODE|END_LINE);
+ }
+ }
+ if (dy == 0) {
+ // horizontal line
+ int h = hpos;
+ int len = dx;
+ if (len < 0) {
+ h += len;
+ len = -len;
+ }
+ if (len == 0)
+ add_char(hline_char, font::hor, h, vpos, col, fill,
+ HDRAW_MODE|START_LINE|END_LINE);
+ else {
+ add_char(hline_char, font::hor, h, vpos, col, fill,
+ HDRAW_MODE|START_LINE);
+ len -= font::hor;
+ h += font::hor;
+ while (len > 0) {
+ add_char(hline_char, font::hor, h, vpos, col, fill,
+ HDRAW_MODE|START_LINE|END_LINE);
+ len -= font::hor;
+ h += font::hor;
+ }
+ add_char(hline_char, font::hor, h, vpos, col, fill,
+ HDRAW_MODE|END_LINE);
+ }
+ }
+}
+
+void tty_printer::put_char(output_character wc)
+{
+ if (font::is_unicode && wc >= 0x80) {
+ char buf[6 + 1];
+ int count;
+ char *p = buf;
+ if (wc < 0x800)
+ count = 1, *p = (unsigned char)((wc >> 6) | 0xc0);
+ else if (wc < 0x10000)
+ count = 2, *p = (unsigned char)((wc >> 12) | 0xe0);
+ else if (wc < 0x200000)
+ count = 3, *p = (unsigned char)((wc >> 18) | 0xf0);
+ else if (wc < 0x4000000)
+ count = 4, *p = (unsigned char)((wc >> 24) | 0xf8);
+ else if (wc <= 0x7fffffff)
+ count = 5, *p = (unsigned char)((wc >> 30) | 0xfC);
+ else
+ return;
+ do *++p = (unsigned char)(((wc >> (6 * --count)) & 0x3f) | 0x80);
+ while (count > 0);
+ *++p = '\0';
+ putstring(buf);
+ }
+ else
+ putchar(wc);
+}
+
+void tty_printer::put_color(schar color_index, int back)
+{
+ if (color_index == DEFAULT_COLOR_IDX) {
+ putstring(SGR_DEFAULT);
+ // set bold and underline again
+ if (is_boldfacing)
+ putstring(SGR_BOLD);
+ if (is_underlining) {
+ if (do_sgr_italics)
+ putstring(SGR_ITALIC);
+ else if (do_reverse_video)
+ putstring(SGR_REVERSE);
+ else
+ putstring(SGR_UNDERLINE);
+ }
+ // set other color again
+ back = !back;
+ color_index = back ? curr_back_idx : curr_fore_idx;
+ }
+ if (color_index != DEFAULT_COLOR_IDX) {
+ putstring(CSI);
+ if (back)
+ putchar('4');
+ else
+ putchar('3');
+ putchar(color_index + '0');
+ putchar('m');
+ }
+}
+
+// The possible Unicode combinations for crossing characters.
+//
+// ' ' = 0, ' -' = 4, '- ' = 8, '--' = 12,
+//
+// ' ' = 0, ' ' = 1, '|' = 2, '|' = 3
+// | |
+
+static output_character crossings[4*4] = {
+ 0x0000, 0x2577, 0x2575, 0x2502,
+ 0x2576, 0x250C, 0x2514, 0x251C,
+ 0x2574, 0x2510, 0x2518, 0x2524,
+ 0x2500, 0x252C, 0x2534, 0x253C
+};
+
+void tty_printer::end_page(int page_length)
+{
+ if (page_length % font::vert != 0)
+ error("vertical position at end of page not multiple of vertical"
+ " motion quantum");
+ int lines_per_page = page_length / font::vert;
+ int last_line;
+ for (last_line = nlines; last_line > 0; last_line--)
+ if (lines[last_line - 1])
+ break;
+#if 0
+ if (last_line > lines_per_page) {
+ error("characters past last line discarded");
+ do {
+ --last_line;
+ while (lines[last_line]) {
+ tty_glyph *tem = lines[last_line];
+ lines[last_line] = tem->next;
+ delete tem;
+ }
+ } while (last_line > lines_per_page);
+ }
+#endif
+ for (int i = 0; i < last_line; i++) {
+ tty_glyph *p = lines[i];
+ lines[i] = 0;
+ tty_glyph *g = 0;
+ while (p) {
+ tty_glyph *tem = p->next;
+ p->next = g;
+ g = p;
+ p = tem;
+ }
+ int hpos = 0;
+ tty_glyph *nextp;
+ curr_fore_idx = DEFAULT_COLOR_IDX;
+ curr_back_idx = DEFAULT_COLOR_IDX;
+ is_underlining = false;
+ is_boldfacing = false;
+ for (p = g; p; delete p, p = nextp) {
+ nextp = p->next;
+ if (p->mode & CU_MODE) {
+ is_continuously_underlining = (p->code != 0);
+ continue;
+ }
+ if (nextp && p->hpos == nextp->hpos) {
+ if (p->draw_mode() == HDRAW_MODE &&
+ nextp->draw_mode() == VDRAW_MODE) {
+ if (font::is_unicode)
+ nextp->code =
+ crossings[((p->mode & (START_LINE|END_LINE)) >> 4)
+ + ((nextp->mode & (START_LINE|END_LINE)) >> 6)];
+ else
+ nextp->code = '+';
+ continue;
+ }
+ if (p->draw_mode() != 0 && p->draw_mode() == nextp->draw_mode()) {
+ nextp->code = p->code;
+ continue;
+ }
+ if (!want_glyph_composition_by_overstriking)
+ continue;
+ }
+ if (hpos > p->hpos) {
+ do {
+ putchar('\b');
+ hpos--;
+ } while (hpos > p->hpos);
+ }
+ else {
+ if (want_horizontal_tabs) {
+ for (;;) {
+ int next_tab_pos = ((hpos + TAB_WIDTH) / TAB_WIDTH) * TAB_WIDTH;
+ if (next_tab_pos > p->hpos)
+ break;
+ if (is_continuously_underlining)
+ make_underline(p->w);
+ else if (!use_overstriking_drawing_scheme
+ && is_underlining) {
+ if (do_sgr_italics)
+ putstring(SGR_NO_ITALIC);
+ else if (do_reverse_video)
+ putstring(SGR_NO_REVERSE);
+ else
+ putstring(SGR_NO_UNDERLINE);
+ is_underlining = false;
+ }
+ putchar('\t');
+ hpos = next_tab_pos;
+ }
+ }
+ for (; hpos < p->hpos; hpos++) {
+ if (is_continuously_underlining)
+ make_underline(p->w);
+ else if (!use_overstriking_drawing_scheme && is_underlining) {
+ if (do_sgr_italics)
+ putstring(SGR_NO_ITALIC);
+ else if (do_reverse_video)
+ putstring(SGR_NO_REVERSE);
+ else
+ putstring(SGR_NO_UNDERLINE);
+ is_underlining = false;
+ }
+ putchar(' ');
+ }
+ }
+ assert(hpos == p->hpos);
+ if (p->mode & COLOR_CHANGE) {
+ if (!use_overstriking_drawing_scheme) {
+ if (p->fore_color_idx != curr_fore_idx) {
+ put_color(p->fore_color_idx, 0);
+ curr_fore_idx = p->fore_color_idx;
+ }
+ if (p->back_color_idx != curr_back_idx) {
+ put_color(p->back_color_idx, 1);
+ curr_back_idx = p->back_color_idx;
+ }
+ }
+ continue;
+ }
+ if (p->mode & UNDERLINE_MODE)
+ make_underline(p->w);
+ else if (!use_overstriking_drawing_scheme && is_underlining) {
+ if (do_sgr_italics)
+ putstring(SGR_NO_ITALIC);
+ else if (do_reverse_video)
+ putstring(SGR_NO_REVERSE);
+ else
+ putstring(SGR_NO_UNDERLINE);
+ is_underlining = false;
+ }
+ if (p->mode & BOLD_MODE)
+ make_bold(p->code, p->w);
+ else if (!use_overstriking_drawing_scheme && is_boldfacing) {
+ putstring(SGR_NO_BOLD);
+ is_boldfacing = false;
+ }
+ if (!use_overstriking_drawing_scheme) {
+ if (p->fore_color_idx != curr_fore_idx) {
+ put_color(p->fore_color_idx, 0);
+ curr_fore_idx = p->fore_color_idx;
+ }
+ if (p->back_color_idx != curr_back_idx) {
+ put_color(p->back_color_idx, 1);
+ curr_back_idx = p->back_color_idx;
+ }
+ }
+ put_char(p->code);
+ hpos += p->w / font::hor;
+ }
+ if (!use_overstriking_drawing_scheme
+ && (is_boldfacing || is_underlining
+ || curr_fore_idx != DEFAULT_COLOR_IDX
+ || curr_back_idx != DEFAULT_COLOR_IDX))
+ putstring(SGR_DEFAULT);
+ putchar('\n');
+ }
+ if (want_form_feeds) {
+ if (last_line < lines_per_page)
+ putchar('\f');
+ }
+ else {
+ for (; last_line < lines_per_page; last_line++)
+ putchar('\n');
+ }
+}
+
+font *tty_printer::make_font(const char *nm)
+{
+ return tty_font::load_tty_font(nm);
+}
+
+printer *make_printer()
+{
+ return new tty_printer();
+}
+
+static void update_options()
+{
+ if (use_overstriking_drawing_scheme) {
+ do_sgr_italics = false;
+ do_reverse_video = false;
+ bold_underline_mode = bold_underline_mode_option;
+ do_bold = want_emboldening_by_overstriking;
+ do_underline = want_italics_by_underlining;
+ }
+ else {
+ do_sgr_italics = want_sgr_italics;
+ do_reverse_video = want_reverse_video_for_italics;
+ bold_underline_mode = BOLD_MODE|UNDERLINE_MODE;
+ do_bold = true;
+ do_underline = true;
+ }
+}
+
+int main(int argc, char **argv)
+{
+ program_name = argv[0];
+ static char stderr_buf[BUFSIZ];
+ if (getenv("GROFF_NO_SGR"))
+ use_overstriking_drawing_scheme = true;
+ setbuf(stderr, stderr_buf);
+ setlocale(LC_CTYPE, "");
+ int c;
+ static const struct option long_options[] = {
+ { "help", no_argument, 0, CHAR_MAX + 1 },
+ { "version", no_argument, 0, 'v' },
+ { NULL, 0, 0, 0 }
+ };
+ while ((c = getopt_long(argc, argv, "bBcdfF:hiI:oruUv", long_options, NULL))
+ != EOF)
+ switch(c) {
+ case 'v':
+ printf("GNU grotty (groff) version %s\n", Version_string);
+ exit(EXIT_SUCCESS);
+ break;
+ case 'i':
+ // Use italic font instead of underlining.
+ want_sgr_italics = true;
+ break;
+ case 'I':
+ // ignore include search path
+ break;
+ case 'b':
+ // Do not embolden by overstriking.
+ want_emboldening_by_overstriking = false;
+ break;
+ case 'c':
+ // Use old scheme for emboldening and underline.
+ use_overstriking_drawing_scheme = true;
+ break;
+ case 'u':
+ // Do not underline.
+ want_italics_by_underlining = false;
+ break;
+ case 'o':
+ // Do not overstrike (other than emboldening and underlining).
+ want_glyph_composition_by_overstriking = false;
+ break;
+ case 'r':
+ // Use reverse mode instead of underlining.
+ want_reverse_video_for_italics = true;
+ break;
+ case 'B':
+ // Do bold-underlining as bold.
+ bold_underline_mode_option = BOLD_MODE;
+ break;
+ case 'U':
+ // Do bold-underlining as underlining.
+ bold_underline_mode_option = UNDERLINE_MODE;
+ break;
+ case 'h':
+ // Use horizontal tabs.
+ want_horizontal_tabs = true;
+ break;
+ case 'f':
+ want_form_feeds = true;
+ break;
+ case 'F':
+ font::command_line_font_dir(optarg);
+ break;
+ case 'd':
+ // Ignore \D commands.
+ allow_drawing_commands = false;
+ break;
+ case CHAR_MAX + 1: // --help
+ usage(stdout);
+ break;
+ case '?':
+ usage(stderr);
+ exit(EXIT_FAILURE);
+ break;
+ default:
+ assert(0 == "unhandled getopt_long return value");
+ }
+ update_options();
+ if (optind >= argc)
+ do_file("-");
+ else {
+ for (int i = optind; i < argc; i++)
+ do_file(argv[i]);
+ }
+ return 0;
+}
+
+static void usage(FILE *stream)
+{
+ fprintf(stream,
+"usage: %s [-bBcdfhioruU] [-F font-directory] [file ...]\n"
+"usage: %s {-v | --version}\n"
+"usage: %s --help\n",
+ program_name, program_name, program_name);
+ if (stdout == stream) {
+ fputs(
+"\n"
+"Translate the output of troff(1) into a form suitable for\n"
+"typewriter‐like devices, including terminal emulators. See the\n"
+"grotty(1) manual page.\n",
+ stream);
+ exit(EXIT_SUCCESS);
+ }
+}
+
+// Local Variables:
+// fill-column: 72
+// mode: C++
+// End:
+// vim: set cindent noexpandtab shiftwidth=2 textwidth=72: