Skip to content

Printing

Iain Smith edited this page Oct 16, 2026 · 6 revisions

Printing

veetee has the printer port a DEC terminal has, from 1.5.0. What the host prints on the terminal's printer, and what you print from the screen, go to a PDF or to a real printer.

Where it goes

Chosen in the window menu:

  • Print to Folder… — each print job becomes a PDF in the folder you choose. Until you choose, it is your Documents folder. A message says where each one went, with a button to open it.
  • Print to Printer… — the system print dialog, once: pick the printer, and a test page prints. From then on every job goes straight to that printer with no dialog, as to a printer on the terminal's port, even after veetee is restarted. On Linux the job goes through CUPS; on Windows, through GTK's own printing.

While something stands in for a printer, veetee tells the host a printer is ready (CSI ? 15 n answers CSI ? 10 n) and the indicator status line says Printer: Ready.

Each job is drawn in a monospaced font at six lines to the inch, on your locale's paper (A4 or US Letter), turned to landscape for lines longer than 80 columns. DEC line drawing prints as the lines it shows.

Printing the screen

F2, or Print Screen in the window menu, prints the whole page on a VT420 and the scrolling region on a VT500 model, their factory Print Extent; Printer Set-Up, or the host with DECPEX, changes it. Print Terminator in Printer Set-Up, or DECPFF, adds a form feed after it.

Ctrl+F2 (or Ctrl+Print) turns auto print on and off, as on the VT420, and Printer Set-Up's Print Mode shows and sets normal, auto print or printer controller mode; leaving auto print or controller mode prints what it had gathered.

What the host can print

The DEC printing functions, sent as escape sequences:

Sequence VT52 Does
ESC [ 5 i … ESC [ 4 i ESC W … ESC X Printer controller: everything between goes to the printer and none of it to the screen
ESC [ i, ESC [ 0 i ESC ] Print the screen
ESC [ ? 1 i ESC V Print the line the cursor is on
ESC [ ? 5 i / ESC [ ? 4 i ESC ^ / ESC _ Auto print on / off: each line printed as the cursor leaves it

What comes in printer controller mode is often meant for a particular printer, escape sequences for bold or a pitch among it. veetee prints it as a plain printer would: line feeds, form feeds, tabs, and a carriage return that overprints, with the escape sequences left out.

From OpenVMS

OpenVMS knows the terminal has a printer port — SHOW TERMINAL lists Printer port. From DCL, this prints a file on the printer at your desk, the usual way of printing locally:

$! LPRINT.COM - print a file on the terminal's own printer
$ esc[0,8]=27
$ set terminal/nowrap/nobroadcast
$ write sys$output esc+"[5i"
$ type/nopage 'p1'
$ write sys$output esc+"[4i"
$ set terminal/wrap/broadcast

@LPRINT REPORT.TXT then prints it; confirmed on OpenVMS IA64, September 2026. /NOWRAP keeps the terminal driver from breaking long lines at the screen width, and /NOBROADCAST keeps a broadcast message out of the middle of the job.

An OpenVMS print queue is a different thing: its symbiont sends jobs to a printer on the network or a terminal server, not through a terminal session, so it cannot print through veetee. To print from a queue to the printer on your desk, point an LPD queue at CUPS on the machine veetee runs on, as below.

A print queue to a printer on Linux

The route is OpenVMS's LPD client, to cups-lpd on the Linux machine, to a CUPS queue, to the printer. It needs no part of veetee, and it was confirmed with TCP/IP Services V6.0 on OpenVMS IA64 printing to a driverless (IPP Everywhere) inkjet, September 2026.

On Linux, make a CUPS queue for the printer, with A4 plain paper as its default. Put your own printer's IPP address in place of the one shown, and use Letter for US paper:

sudo lpadmin -p canon -E -v 'ipp://PRINTER.local:631/ipp/print' \
    -m 'driverless:ipp://PRINTER.local:631/ipp/print' \
    -o PageSize=A4 -o MediaType=Stationery

Then have systemd run cups-lpd on the LPD port, only for the OpenVMS host. Create /etc/systemd/system/cups-lpd.socket, with your OpenVMS host's address:

[Unit]
Description=CUPS LPD server socket

[Socket]
ListenStream=515
Accept=true
IPAddressAllow=192.0.2.10 127.0.0.1
IPAddressDeny=any

[Install]
WantedBy=sockets.target

and /etc/systemd/system/cups-lpd@.service:

[Unit]
Description=CUPS LPD server

[Service]
ExecStart=-/usr/lib/cups/daemon/cups-lpd -o job-sheets=none -o media=A4 -o media-type=stationery
StandardInput=socket
User=lp

then sudo systemctl daemon-reload and sudo systemctl enable --now cups-lpd.socket. Open port 515 in the firewall to the OpenVMS host, if there is one.

Every OpenVMS host that prints must be in IPAddressAllow, separated by spaces; after a change, sudo systemctl daemon-reload and sudo systemctl restart cups-lpd.socket. A host left out is dropped without a word in any log, and its jobs wait on the queue as if the printer were off. journalctl -u 'cups-lpd@*' shows each connection that does get through, and the job it made.

On OpenVMS, enable the LPD service in TCPIP$CONFIG if it is not already, then run TCPIP$LPRSETUP and add a remote printer: rm is the Linux machine's address, rp the CUPS queue's name. Then add :sh: to its entry in TCPIP$PRINTCAP.DAT (in SYS$SPECIFIC:[TCPIP$LPD]), before the last line — every line but the last ends in a backslash:

CANON|canon:\
        :lf=/TCPIP$LPD_ROOT/000000/CANON.LOG:\
        :lp=CANON:\
        :rm=192.0.2.20:\
        :rp=canon:\
        :sd=/TCPIP$LPD_ROOT/CANON:\
        :sh:\
        :rt#515:

and restart the queue, STOP/QUEUE/RESET CANON and START/QUEUE CANON. PRINT/QUEUE=CANON LOGIN.COM then prints on the printer by your desk.

Or with one command procedure

LPD_QUEUE.COM does the OpenVMS side in one step, in place of TCPIP$LPRSETUP and the edit: it writes the printcap entry, :sh: and all, makes the spool directory, and creates and starts the queue on the LPD symbiont with no flag, burst or trailer pages. Run it again to change a queue; its jobs are kept, and the printcap is backed up first. From SYSTEM, with the LPD client already configured in TCPIP$CONFIG:

$ @LPD_QUEUE CANON 192.0.2.20 canon          ! queue, Linux machine, CUPS queue
$ @LPD_QUEUE CANON 192.0.2.20 canon DEFAULT  ! and point SYS$PRINT at it

Confirmed on OpenVMS x86-64, October 2026.

$! LPD_QUEUE.COM
$!
$! Creates (or re-creates) an OpenVMS print queue that prints through
$! TCP/IP Services' LPD client to cups-lpd and CUPS on another machine,
$! and writes its entry in the LPD printcap.  Safe to run again: an
$! existing queue is stopped and re-initialised, keeping its jobs, and an
$! existing printcap entry of the same name is replaced.
$!
$!   $ @LPD_QUEUE queue remote-host remote-queue [DEFAULT]
$!
$!   P1  OpenVMS queue name
$!   P2  host running cups-lpd
$!   P3  CUPS queue on that host (CUPS ignores case)
$!   P4  DEFAULT to point SYS$PRINT at the queue
$!
$! Needs SYSPRV and OPER; run it from SYSTEM.  The LPD client must be
$! configured and started (TCPIP$CONFIG, Client components, LPR/LPD).
$!
$! On the CUPS side, cups-lpd should run with
$!   -o job-sheets=none -o media=A4 -o media-type=stationery
$! or the printer feeds a blank banner sheet, or tries photo paper.
$! If its systemd socket has IPAddressAllow, this node's address must be
$! in it: denied connections are dropped without a word in any log, and
$! jobs sit on the queue as if the printer were off.
$!
$ SET NOON
$ say = "WRITE SYS$OUTPUT"
$ tab = F$FAO("!_")
$ old_privs = F$SETPRV("SYSPRV,OPER")
$ IF .NOT. F$PRIVILEGE("SYSPRV,OPER")
$ THEN
$   say "LPD_QUEUE: SYSPRV and OPER are needed"
$   GOTO DONE
$ ENDIF
$!
$ queue = F$EDIT(P1,"TRIM,UPCASE")
$ rhost = F$EDIT(P2,"TRIM")
$ rqueue = F$EDIT(P3,"TRIM")
$ IF queue .EQS. "" .OR. rhost .EQS. "" .OR. rqueue .EQS. ""
$ THEN
$   say "Usage: @LPD_QUEUE queue remote-host remote-queue [DEFAULT]"
$   GOTO DONE
$ ENDIF
$ make_default = F$EDIT(P4,"TRIM,UPCASE") .EQS. "DEFAULT"
$ node = F$GETSYI("NODENAME")
$!
$! The LPD client: its root, its symbiont and its printcap.
$!
$ IF F$TRNLNM("TCPIP$LPD_ROOT") .EQS. ""
$ THEN
$   say "LPD_QUEUE: TCPIP$LPD_ROOT is not defined; configure LPR/LPD with"
$   say "           @SYS$MANAGER:TCPIP$CONFIG, or run @SYS$STARTUP:TCPIP$LPD_STARTUP"
$   GOTO DONE
$ ENDIF
$ IF F$SEARCH("SYS$SYSTEM:TCPIP$LPD_SMB.EXE") .EQS. ""
$ THEN
$   say "LPD_QUEUE: SYS$SYSTEM:TCPIP$LPD_SMB.EXE is missing; is LPR/LPD installed?"
$   GOTO DONE
$ ENDIF
$ pcap = F$TRNLNM("TCPIP$PRINTCAP")
$ IF pcap .EQS. "" THEN pcap = "TCPIP$LPD_ROOT:[000000]TCPIP$PRINTCAP.DAT"
$!
$ say "Queue ''queue' on ''node':: to ''rqueue' at ''rhost'"
$!
$! The printcap: copy it to a new version, leaving out any entry for this
$! queue (an entry runs on while its lines end in a backslash), and add
$! the entry at the end.
$!
$ in_file = F$SEARCH(pcap)
$ IF in_file .NES. "" THEN COPY 'in_file' *.BAK
$ OPEN/WRITE out 'pcap'
$ IF in_file .EQS. "" THEN GOTO PCAP_APPEND
$ OPEN/READ in 'in_file'
$ skipping = 0
$PCAP_READ:
$ READ/END_OF_FILE=PCAP_EOF in line
$ last = ""
$ IF F$LENGTH(line) .GT. 0 THEN last = F$EXTRACT(F$LENGTH(line)-1,1,line)
$ IF skipping
$ THEN
$   IF last .NES. "\" THEN skipping = 0
$   GOTO PCAP_READ
$ ENDIF
$ first = F$EXTRACT(0,1,line)
$ IF line .NES. "" .AND. first .NES. "#" .AND. first .NES. " " -
     .AND. first .NES. tab .AND. first .NES. ":"
$ THEN
$   names = F$EDIT(F$ELEMENT(0,":",line),"TRIM,UPCASE") - "\"
$   names = "|" + names + "|"
$   IF F$LOCATE("|''queue'|",names) .LT. F$LENGTH(names)
$   THEN
$     say "Replacing the printcap entry for ''queue'"
$     IF last .EQS. "\" THEN skipping = 1
$     GOTO PCAP_READ
$   ENDIF
$ ENDIF
$ WRITE out line
$ GOTO PCAP_READ
$PCAP_EOF:
$ CLOSE in
$PCAP_APPEND:
$ WRITE out queue, "|", F$EDIT(queue,"LOWERCASE"), ":\"
$ WRITE out tab, ":lf=/TCPIP$LPD_ROOT/000000/", queue, ".LOG:\"
$ WRITE out tab, ":rm=", rhost, ":\"
$ WRITE out tab, ":rp=", rqueue, ":\"
$ WRITE out tab, ":sd=/TCPIP$LPD_ROOT/", queue, ":\"
$ WRITE out tab, ":sh:"
$ CLOSE out
$ say "Printcap: ''F$SEARCH(pcap)'"
$!
$! The spool directory the printcap names.
$!
$ IF F$SEARCH("TCPIP$LPD_ROOT:[000000]''queue'.DIR") .EQS. ""
$ THEN
$   CREATE/DIRECTORY/OWNER=PARENT TCPIP$LPD_ROOT:['queue']
$ ENDIF
$!
$! The queue: stopped first if it is there, so INITIALIZE can change it
$! without losing its jobs.  No flag, burst or trailer pages and no form
$! feed added, so a job prints as it was sent.
$!
$ dummy = F$GETQUI("CANCEL_OPERATION")
$ IF F$GETQUI("DISPLAY_QUEUE","QUEUE_NAME",queue) .NES. ""
$ THEN
$   say "Stopping the existing queue ''queue'"
$   STOP/QUEUE/RESET 'queue'
$ ENDIF
$ INITIALIZE/QUEUE/START 'queue' -
    /ON='node':: -
    /PROCESSOR=TCPIP$LPD_SMB -
    /DEFAULT=(NOBURST,NOFLAG,NOTRAILER,NOFEED) -
    /RETAIN=ERROR -
    /NOENABLE_GENERIC
$ IF .NOT. $STATUS
$ THEN
$   say "LPD_QUEUE: the queue was not initialised"
$   GOTO DONE
$ ENDIF
$!
$ IF make_default
$ THEN
$   DEFINE/SYSTEM/EXECUTIVE_MODE SYS$PRINT 'queue'
$   say "SYS$PRINT is ''queue' until the next boot; for good, add to"
$   say "SYS$MANAGER:SYLOGICALS.COM:  $ DEFINE/SYSTEM/EXECUTIVE_MODE SYS$PRINT ''queue'"
$ ENDIF
$!
$ SHOW QUEUE/FULL 'queue'
$ say ""
$ say "To test:   $ PRINT/QUEUE=''queue' SYS$LOGIN:LOGIN.COM"
$ say "Errors go to TCPIP$LPD_ROOT:[000000]''queue'.LOG"
$ say "To start the queue at boot, add to SYS$MANAGER:SYSTARTUP_VMS.COM,"
$ say "after TCP/IP Services starts:   $ START/QUEUE ''queue'"
$!
$DONE:
$ IF F$TRNLNM("IN","LNM$PROCESS") .NES. "" THEN CLOSE in
$ IF F$TRNLNM("OUT","LNM$PROCESS") .NES. "" THEN CLOSE out
$ old_privs = F$SETPRV(old_privs)
$ EXIT

Two things each gave a blank sheet on the way, and are why the settings above are there:

  • A banner page. The OpenVMS LPD client asks for a banner, and CUPS makes one of its own, which came out blank, the job stalling after it. :sh: in the printcap and -o job-sheets=none on cups-lpd turn it off.
  • The printer's own paper. A job by LPD carries no paper size or type, and a driverless printer then uses its own default, which on many inkjets is photo paper; with plain A4 in the tray it reports a paper error and feeds a blank sheet. The queue's default is not enough: -o media=A4 -o media-type=stationery on cups-lpd puts them on every job.

lpstat -W completed -o canon shows the jobs CUPS has finished; one that sits in processing with Waiting for job to complete is usually the printer reporting a paper error.

If it goes wrong

See Troubleshooting. The plan, and what printing on real paper found, is in docs/printing.md.

Clone this wiki locally