diff options
Diffstat (limited to '')
-rw-r--r-- | src/devices/grotty/TODO | 3 | ||||
-rw-r--r-- | src/devices/grotty/grotty.1.man | 810 | ||||
-rw-r--r-- | src/devices/grotty/grotty.am | 39 | ||||
-rwxr-xr-x | src/devices/grotty/tests/basic_latin_glyphs_map_correctly.sh | 205 | ||||
-rwxr-xr-x | src/devices/grotty/tests/osc8_works.sh | 119 | ||||
-rw-r--r-- | src/devices/grotty/tty.cpp | 1043 |
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: |