DIRECTORY
Commander USING [CommandProc, Handle, Register],
CommandTool USING [ArgumentVector, CurrentWorkingDirectory, Failed, Parse],
Convert USING [AppendInt, Error, IntFromRope],
FS USING [ComponentPositions, Delete, EnumerateForNames, ExpandName, Error, ErrorDesc, ErrorFromStream, FileInfo, NameProc, StreamOpen],
IO USING [Backup, CR, Close, FF, EndOfStream, Error, GetChar, GetLength, GetIndex, int, PutChar, PutF, PutFR, PutRope, rope, SetLength, SP, STREAM, TAB, time],
PressPrinter USING [Abort, CurrentState, CurrentStateMessage, Handle, IsAPressFile, SendPressFile, State],
Process USING [CheckForAbort],
PutGet USING [FromFile, FromFileError],
Real USING [RoundLI],
RefText USING [AppendRope, New, TrustTextAsRope],
Rope USING [Cat, Concat, Fetch, Find, IsEmpty, Length, ROPE, Size, Substr],
RopeEdit USING [BlankChar],
SirPress USING [ClosePress, Create, PressHandle, PutText, SetFont, SetPageSize, WritePage],
TEditInput USING [FreeTree],
TEditProfile USING [sourceExtensions],
TextNode USING [Forward, NarrowToTextNode, Ref, RefTextNode],
UserCredentials USING [Get],
UserProfile USING [Token];
 
Print: 
CEDAR 
PROGRAM
IMPORTS Commander, CommandTool, Convert, FS, IO, PressPrinter, Process, PutGet, Real, RefText, Rope, RopeEdit, SirPress, TEditInput, TEditProfile, TextNode, UserCredentials, UserProfile = BEGIN
ROPE: TYPE = Rope.ROPE;
STREAM: TYPE = IO.STREAM;
Micas: TYPE = INT;
pressHandle: SirPress.PressHandle ← NIL;
PressFromText: 
PROC [
text: --input-- STREAM, press: --output-- STREAM,
headingFontFamily: ROPE,
textFontFamily: ROPE,
columnsPerPage: [1..100],
pointSize: NAT,
portrait: BOOL,
shortFileName: ROPE,
fileNameForLeaderPage: --only first 49 chars will show-- ROPE,
fileNameForHeading: ROPE,
pressFileName: ROPE,
doNotClose: BOOL ← FALSE]
RETURNS [pressPages: 
INT] = {
-- ! IO.Error [$Failure] (FS errors)
-- Does not close text or press streams.
pageHeightTenths: INT ~ IF portrait THEN 110 ELSE 85; -- tenth inches
pageWidthTenths: INT ~ IF portrait THEN 85 ELSE 110; -- tenth inches
pageWidth: Micas ~ pageWidthTenths*254;
pageHeight: Micas ~ pageHeightTenths*254;
topMargin: Micas ~ IF portrait THEN 2581 ELSE 1905;
bottomMargin: Micas ~ IF portrait THEN 1270 ELSE 1905;
leftMargin: Micas ~ IF portrait THEN 1905 ELSE 1270;
rightMargin: Micas ~ 1270;
interColumnSpace: Micas ~ 2540/columnsPerPage;
columnOffset: Micas ~ (pageWidth-leftMargin-rightMargin+interColumnSpace)/columnsPerPage;
columnWidthForText: Micas ~ columnOffset - interColumnSpace;
xCoordinateOfLeftEdge: Micas ~ leftMargin;
yCoordinateOfFirstBaseline: Micas ~ pageHeight - topMargin;
yCoordinateOfPageNumberBaseline: Micas ~ bottomMargin/2;
yCoordinateOfHeadingBaseline: Micas ~ pageHeight - topMargin/2;
micasPerPoint: REAL ~ 2540.0 / 72.0; -- About 35.277777
charWidth: Micas ~ Real.RoundLI[pointSize*micasPerPoint*0.612];
0.612 is roughly the width of the characters in Gacha and Snail.  To allow the user to set the font but also not depend upon the TSetter, we have to hard-wire the number in here.  It would be a lot easier if the Imager could read TFM files, but it can't...
deltaYCoordinateBetweenLines: Micas ~ Real.RoundLI[1.2*pointSize*micasPerPoint];
-- 1.2 times the design-size of the font
maxCharsPerLine: NAT ~ columnWidthForText / charWidth;
maxLinesPerColumn: NAT ~
(yCoordinateOfFirstBaseline - bottomMargin) / deltaYCoordinateBetweenLines;
IF pressHandle = 
NIL 
THEN
pressHandle ← SirPress.Create[press, fileNameForLeaderPage];
 
pressHandle.SetPageSize[height: pageHeightTenths, width: pageWidthTenths];
pressHandle.SetFont[family: textFontFamily, size: pointSize];
BEGIN
column: INT;
xCoordinateOfColumnLeftEdge: Micas;
headingBuffer: REF TEXT ← RefText.New[maxCharsPerLine*columnsPerPage];
PrintHeading: 
PROC [] = {
pressHandle.SetFont[family: headingFontFamily, size: pointSize];
headingBuffer.length ← 0;
headingBuffer ← RefText.AppendRope[to: headingBuffer, from: "Page "];
headingBuffer ← Convert.AppendInt[to: headingBuffer, from: column];
pressHandle.PutText[textString: RefText.TrustTextAsRope[headingBuffer],
xCoordinateOfLeftEdge: xCoordinateOfColumnLeftEdge+columnWidthForText-Real.RoundLI[pointSize*35.277777*0.612*headingBuffer.length],
yCoordinateOfBaseline: yCoordinateOfPageNumberBaseline];
headingBuffer.length ← 0;
IF (column-1) 
MOD columnsPerPage = 0 
THEN 
BEGIN 
-- First column of page
start: INT ← fileNameForHeading.Length[] - (maxCharsPerLine*columnsPerPage-shortFileName.Length-3);
IF start > 0 
THEN 
BEGIN
headingBuffer ← RefText.AppendRope[to: headingBuffer, from: "..."];
start ← start + 3;
END
 
ELSE start ← 0;
headingBuffer ← RefText.AppendRope[to: headingBuffer, from: fileNameForHeading, start: start];
pressHandle.PutText[textString: RefText.TrustTextAsRope[headingBuffer],
xCoordinateOfLeftEdge: xCoordinateOfLeftEdge,
yCoordinateOfBaseline: yCoordinateOfHeadingBaseline];
pressHandle.PutText[textString: shortFileName,
xCoordinateOfLeftEdge: 
xCoordinateOfLeftEdge + columnsPerPage*columnOffset - interColumnSpace - Real.RoundLI[pointSize*35.277777*0.612*shortFileName.Length],
yCoordinateOfBaseline: yCoordinateOfHeadingBaseline];
END;
 
pressHandle.SetFont[family: textFontFamily, size: pointSize];
};
 
indent: BOOL ← FALSE;
indentChars: NAT ← 0;
lineBuffer: REF TEXT = RefText.New[maxCharsPerLine+1];
PrintColumn: 
PROC [] 
RETURNS [eof: 
BOOL] = {
ff: BOOL;
linesPrinted: INT ← 0;
UNTIL linesPrinted = maxLinesPerColumn 
DO
[eof, ff, indent, indentChars] ←
FillLineBuffer[lineBuffer, maxCharsPerLine, indent, indentChars, text];
IF linesPrinted = 0 
THEN 
BEGIN
IF eof THEN RETURN [TRUE];
IF ff THEN LOOP;
PrintHeading[];
END;
 
IF lineBuffer.length > 0 
THEN pressHandle.PutText[
RefText.TrustTextAsRope[lineBuffer], xCoordinateOfColumnLeftEdge,
yCoordinateOfFirstBaseline - linesPrinted*deltaYCoordinateBetweenLines];
 
IF eof OR ff THEN RETURN [FALSE];
linesPrinted ← linesPrinted.SUCC;
ENDLOOP;
 
RETURN [FALSE];
};
 
FOR column ← 1, column ← column.
SUCC 
DO
xCoordinateOfColumnLeftEdge ← xCoordinateOfLeftEdge +
((column-1) MOD columnsPerPage)*columnOffset;
IF PrintColumn[].eof THEN EXIT;
IF column MOD columnsPerPage = 0 THEN pressHandle.WritePage[];
ENDLOOP;
 
IF doNotClose
THEN 
BEGIN
pressHandle.WritePage[];
pressHandle.SetFont[family: textFontFamily, size: pointSize];
END
 
ELSE pressHandle.ClosePress[];
 
RETURN [(column-1+columnsPerPage-1)/columnsPerPage]
END;
 
 
};--PressFromText
 
FillLineBuffer: 
PROC [lineBuffer: 
REF 
TEXT, maxCharsPerLine: 
NAT,
indent: BOOL, indentChars: NAT, text: --input-- STREAM]
RETURNS [eof, ff: BOOL ← FALSE, nextIndent: BOOL ← FALSE, nextIndentChars: NAT ← 0] = BEGIN
-- lineBuffer is garbage on entry.  lineBuffer.maxLength >= maxCharsPerLine.
-- Returns eof = TRUE iff end of stream from text and lineBuffer is empty.
-- Returns ff = TRUE iff form-feed char read from text and lineBuffer is empty.
spacesPerTab: NAT = 4;
extraIndentForFollowingLines: NAT = 2;
lineLength, maxLineLength: NAT← 0;
OpenLine: 
PROC [] = 
BEGIN
lineLength ← 0;
maxLineLength ← IF indent THEN maxCharsPerLine-indentChars ELSE maxCharsPerLine;
END;
 
Append: 
PROC [c: 
CHAR] = 
BEGIN
lineBuffer[lineLength] ← c;  lineLength ← lineLength + 1
END;
 
UnAppend: 
PROC [n: 
NAT] = 
BEGIN
FOR i: 
NAT 
DECREASING 
IN [lineLength-n .. lineLength-1] 
DO
text.Backup[lineBuffer[i]];
ENDLOOP;
 
lineLength ← lineLength - n;
END;
 
IsTabStop: 
PROC [] 
RETURNS [
BOOL] = 
BEGIN
RETURN [
(IF indent THEN lineLength+indentChars ELSE lineLength) MOD spacesPerTab = 0];
 
END;
 
IndexOfLastWhiteSpace: 
PROC [] 
RETURNS [n: 
NAT] = 
BEGIN
FOR i: 
NAT 
DECREASING 
IN [1 .. lineLength) 
DO 
-- don't look at first char in line
IF lineBuffer[i].ORD <= (IO.SP).ORD THEN RETURN [i];
ENDLOOP;
 
-- here only if breaking an all-nonwhite line (unlikely)
RETURN [NAT.LAST]
END;
 
IndexOfFirstNonwhiteSpace: 
PROC [] 
RETURNS [n: 
NAT] = 
BEGIN
FOR i: 
NAT 
IN [0 .. lineLength) 
DO
IF lineBuffer[i].ORD > (IO.SP).ORD THEN RETURN [i];
ENDLOOP;
 
-- here only if breaking an all-white line (very unlikely)
RETURN [0];
END;
 
CloseLine: 
PROC [] 
RETURNS [] = 
BEGIN
IF indent 
THEN 
BEGIN
FOR i: 
NAT 
DECREASING 
IN [0 .. lineLength) 
DO
lineBuffer[i + indentChars] ← lineBuffer[i];
ENDLOOP;
 
lineLength ← lineLength + indentChars;
FOR i: NAT IN [0 .. indentChars) DO lineBuffer[i] ← IO.SP ENDLOOP;
END;
 
lineBuffer.length ← lineLength;
END;
 
OpenLine[];
DO 
BEGIN
-- Assert 0 <= lineLength <= maxLineLength
char: CHAR;
BEGIN
char ← text.GetChar[ ! IO.EndOfStream => GOTO endOfStream];
EXITS endOfStream => BEGIN char ← IO.CR; eof ← TRUE END
END;
 
IF eof AND lineLength = 0 THEN GOTO done;
IF char = 
IO.
FF 
THEN 
BEGIN
IF lineLength = 0 THEN BEGIN ff ← TRUE; GOTO done END;
text.Backup[char];
char ← IO.CR;
END;
 
IF char = IO.CR THEN GOTO done;
IF char = 
IO.
TAB
THEN 
BEGIN
THROUGH [0 .. spacesPerTab) 
DO
Append[IO.SP];
IF IsTabStop[] THEN EXIT;
IF lineLength > maxLineLength THEN EXIT;
ENDLOOP;
 
END
 
ELSE Append[char];
 
-- Assert 0 < lineLength <= maxLineLength+1
IF lineLength > maxLineLength 
THEN 
BEGIN
-- line won't fit in buffer; must insert extra line break
i: NAT ← IndexOfLastWhiteSpace[];
IF i = 
NAT.
LAST
THEN 
BEGIN
-- line won't break at white space; break at end and insert "~~"
UnAppend[3];  Append['~];  Append['~];
END
 
ELSE 
BEGIN
-- put back chars following the last white space; then discard the last white space
UnAppend[lineLength - (i+1)];
lineLength ← lineLength - 1;
END;
 
 
-- Assert 0 < lineLength <= maxLineLength
nextIndentChars ← 
IF indent
THEN indentChars
ELSE MIN[
IndexOfFirstNonwhiteSpace[] + extraIndentForFollowingLines,
maxCharsPerLine/2];
 
nextIndent ← TRUE;
GOTO done;
END;
 
EXITS done => BEGIN CloseLine[];  RETURN END
END ENDLOOP;
 
END;--FillLineBuffer
 
PlainTextStreamFromNode: 
PROC [from: TextNode.Ref, to: 
STREAM]
 = 
BEGIN
-- Writes a plain text representation of from and its descendants onto to, then Closes to.
WritePlain[h: to, root: from, restoreDashes: TRUE];
to.Close[];
RETURN;
END;
 
PressFileFromFile: 
PROC [from: 
ROPE, pressStream: 
IO.
STREAM, headingFontFamily: 
ROPE, textFontFamily: 
ROPE, pressFileName: 
ROPE, columnsPerPage: [1..100], pointSize: 
NAT, portrait: 
BOOL, ignoreNodes: 
BOOL, cmd: Commander.Handle, doNotClose: 
BOOL ← 
FALSE]
RETURNS [ok: BOOL] = BEGIN
-- Writes a plain text representation of file onto a press file named pressFileName.
-- Avoids producing internal Tioga document format if ignoreNodes and for plain text files.
-- ! IO.Error[$Failure] (FS errors)
fromStream: STREAM = FS.StreamOpen[from];
fromName, fromAttachedToName: ROPE;
cp: FS.ComponentPositions;
shortFileName, fileNameForLeaderPage, fileNameForHeading: ROPE;
numBytes: INT;
[fullFName: fromName, attachedTo: fromAttachedToName, bytes: numBytes] ←
FS.FileInfo[from];
[shortFileName, cp] ← FS.ExpandName[fromName];
shortFileName ← shortFileName.Substr[cp.base.start, cp.ext.start+cp.ext.length-cp.base.start];
fileNameForLeaderPage ← fromName;
fileNameForHeading ←
IO.PutFR["%g of %g", IO.rope[IF fromAttachedToName.IsEmpty[] THEN fromName ELSE fromAttachedToName], IO.time[FS.FileInfo[from].created]];
BEGIN
pressPages: INT;
IF ignoreNodes 
OR fromStream.GetLength[] = numBytes
THEN 
BEGIN
-- Read plain text directly from from.
cmd.out.PutF["Printing %g (text) ...", IO.rope[fromName]];
pressPages ← PressFromText[
text: fromStream, press: pressStream, headingFontFamily: headingFontFamily, textFontFamily: textFontFamily, columnsPerPage: columnsPerPage, pointSize: pointSize, portrait: portrait, shortFileName: shortFileName, fileNameForLeaderPage: fileNameForLeaderPage, fileNameForHeading: fileNameForHeading, pressFileName: pressFileName, doNotClose: doNotClose];
END
 
ELSE 
BEGIN
-- Built Tioga tree structure from from, then fork a process to produce a plain text version.
rootNode: TextNode.Ref;
tempStream: STREAM ← FS.StreamOpen["[]<>Temp>Print.temp", $create];
cmd.out.PutF["Making %g plain text ...", IO.rope[fromName]];
rootNode ← PutGet.FromFile[fromName
! PutGet.FromFileError => BEGIN
cmd.out.PutRope["Tioga error reading file\n"];
GOTO fail;
END];
cmd.out.PutRope[" formatting ..."];
PlainTextStreamFromNode[from: rootNode, to: tempStream];
tempStream ← FS.StreamOpen["[]<>Temp>Print.temp"];
pressPages ← PressFromText[
text: tempStream, press: pressStream, headingFontFamily: headingFontFamily, textFontFamily: textFontFamily, columnsPerPage: columnsPerPage, pointSize: pointSize, portrait: portrait, shortFileName: shortFileName, fileNameForLeaderPage: fileNameForLeaderPage, fileNameForHeading: fileNameForHeading, pressFileName: pressFileName, doNotClose: doNotClose];
TEditInput.FreeTree[rootNode];
tempStream.Close[];
END;
 
 
fromStream.Close[];
cmd.out.PutF[" %g %g\n",
IO.int[pressPages], IO.rope[IF pressPages > 1 THEN "pages." ELSE "page."]];
RETURN [ok: TRUE]
EXITS fail => BEGIN RETURN [ok: FALSE] END
END
 
END;
 
SendPressFile: 
PROC [fileName, server: 
ROPE, copies: 
INT, cmd: Commander.Handle]
RETURNS [result: REF ← NIL, msg: ROPE ← NIL] = BEGIN
-- Sends press file, giving feedback on its progress to cmd and checking for process abort.
-- "For:" name on break page will be that of the logged-in user.
lastState: PressPrinter.State ← $aborted;
aborted: BOOL ← FALSE;
SendPressFileProgress: 
PROC [handle: PressPrinter.Handle] = 
BEGIN
state: PressPrinter.State = handle.CurrentState[];
IF state = lastState THEN cmd.out.PutChar['.]
ELSE 
BEGIN
cmd.out.PutF["\n%g ", IO.rope[handle.CurrentStateMessage[]]];
IF state IN [$queued .. $serverTrouble] THEN cmd.out.PutRope["... "];
END;
 
lastState ← state;
BEGIN
Process.CheckForAbort[ ! ABORTED => GOTO abortTransmission];
EXITS abortTransmission => 
BEGIN
aborted ← TRUE;
PressPrinter.Abort[handle]
-- this proc is called again with state = $aborted; then ABORTED is raised by SendPressFile
END;
 
END;
 
END;--SendPressFileProgress
 
printerHandle: PressPrinter.Handle ← NIL;
printerHandle ← PressPrinter.SendPressFile[
fileName: fileName, server: server, copies: copies,
userName: UserCredentials.Get[].name,
progressProc: SendPressFileProgress ! ABORTED => CONTINUE];
IF printerHandle = NIL OR printerHandle.CurrentState[] # $done THEN aborted ← TRUE;
IF aborted 
THEN 
BEGIN
cmd.out.PutRope["Aborting press file send... "];
RETURN [$Failure, NIL];
END;
 
cmd.out.PutRope[".\n"];
END;
 
FileCheck: 
PROC
[fileName: ROPE, wDir: ROPE ← NIL] RETURNS [exists: BOOL, fullFName: ROPE] = BEGIN
-- Returns exists: TRUE iff file exists.  If exists then fullFName is filled in to make later lookup faster.
[fullFName: fullFName] ←
 FS.FileInfo[name: fileName, wDir: wDir
! FS.Error => IF error.group = $user THEN GOTO doesNotExist];
RETURN [exists: TRUE, fullFName: fullFName]
EXITS doesNotExist => RETURN [exists: FALSE, fullFName: NIL];
END;
 
CompleteFileName: 
PROC
[fileName: ROPE, wDir: ROPE ← NIL] RETURNS [fullFName: ROPE] = BEGIN
-- Returns NIL if can't find a completion that corresponds to an existing file
exists: BOOL;
[exists, fullFName] ← FileCheck[fileName, wDir];
IF exists OR fileName.Find["!"] >= 0 OR fileName.Find["."] >= 0 THEN RETURN;
FOR extList: 
LIST 
OF 
ROPE ← TEditProfile.sourceExtensions, extList.rest 
UNTIL extList = 
NIL 
DO
f: ROPE = fileName.Cat[".", extList.first];
[exists, fullFName] ← FileCheck[f, wDir];
IF exists THEN RETURN;
ENDLOOP;
 
END;
 
PressFileName: 
PROC [fileName: 
ROPE, newDir: 
ROPE] 
RETURNS [
ROPE] = 
BEGIN
cp: FS.ComponentPositions;
base: ROPE;
[fullFName: fileName, cp: cp] ← FS.ExpandName[name: fileName];
base ← fileName.Substr[cp.base.start, cp.base.length];
RETURN [Rope.Cat[newDir, base, ".press"]];
END;
 
WritePlain: 
PUBLIC 
PROC [h: 
IO.
STREAM, root: TextNode.Ref, restoreDashes: 
BOOL ← 
FALSE] = 
BEGIN
HasInitialDashes: 
PROC [r: 
ROPE] 
RETURNS [
BOOL] = 
BEGIN
loc: INT ← 0;
size: INT = Rope.Size[r];
c: CHAR;
IF r = NIL THEN RETURN [FALSE];
WHILE loc < size 
AND RopeEdit.BlankChar[c ← Rope.Fetch[r, loc]] 
DO
loc ← loc+1;
ENDLOOP;
 
IF loc > size OR c # '- OR Rope.Fetch[r, loc+1] # '- THEN RETURN [FALSE];
RETURN [TRUE];
END;
 
node: TextNode.Ref ← root;
level: INTEGER ← 0;
levelDelta: INTEGER;
first: BOOL ← TRUE;
DO
text: TextNode.RefTextNode;
[node, levelDelta] ← TextNode.Forward[node];
IF node=NIL THEN EXIT;
IF first THEN first ← FALSE
ELSE IO.PutChar[h, '\n]; -- carriage returns between nodes
level ← level+levelDelta;
IF (text ← TextNode.NarrowToTextNode[node])=NIL THEN LOOP;
THROUGH [1..level) DO IO.PutChar[h, '\t]; ENDLOOP; -- output level-1 tabs
IF restoreDashes 
AND text.comment 
AND ~HasInitialDashes[text.rope] 
THEN
IO.PutRope[h, "-- "]; -- restore the leading dashes for Mesa comments
 
IO.PutRope[h, text.rope];
ENDLOOP;
 
BEGIN
ENABLE IO.Error => IF ec = NotImplementedForThisStream THEN GOTO Exit;
IO.SetLength[h, IO.GetIndex[h]]
END
 
EXITS Exit => RETURN
END;
 
DoPrint: Commander.CommandProc = 
BEGIN
PROC [cmd: Commander.Handle] RETURNS [result: REF ← NIL, msg: Rope.ROPE ← NIL]
argv: CommandTool.ArgumentVector ← NIL;
i: NAT;
wDir: ROPE ← CommandTool.CurrentWorkingDirectory[];
fileName: ROPE;
columnsPerPage: INT ← -1;
pointSize: INT ← -1;
portrait: BOOL ← FALSE;
ignoreNodes: BOOL ← FALSE;
nCopies: INT ← 1;
host: ROPE ← UserProfile.Token[key: "Hardcopy.PressPrinter", default: NIL];
hostSpecified: BOOL ← NOT host.IsEmpty[];
pressFileName: ROPE ← NIL;
retainPressFile: BOOL ← FALSE;
headingFontFamily: ROPE ← "Gacha";
textFontFamily: ROPE ← "Gacha";
fileRef: TYPE ~ REF fileRec;
fileRec: 
TYPE ~ 
RECORD
[
next: fileRef ← NIL,
file: ROPE ← NIL
];
fileHead: fileRef ← NIL;
fileTail: fileRef ← NIL;
patternHead: fileRef ← NIL;
patternTail: fileRef ← NIL;
pressHandle ← NIL;
BEGIN 
--  (a) interpret the command line, modifying fileName
, ... , retainPressFile
.
IsParm: 
PROC [i: 
NAT] 
RETURNS [
BOOL] = 
BEGIN
RETURN [i # argv.argc-1 AND NOT argv[i].IsEmpty[] AND argv[i].Fetch[0] # '-];
END;
 
cmd.commandLine ← Rope.Cat[UserProfile.Token["Print.DefaultOptions"], " ", cmd.commandLine];
argv ← CommandTool.Parse[cmd ! CommandTool.Failed => BEGIN msg ← errorMsg;  CONTINUE; END];
IF argv = NIL THEN RETURN[$Failure, msg];
IF argv.argc < 2 THEN RETURN[$Failure, printHelpText];
FOR i ← 1, i.
SUCC 
UNTIL i = argv.argc 
DO
IF argv[i].Length[] = 2 
AND argv[i].Fetch[0] = '-
THEN 
BEGIN
SELECT argv[i].Fetch[1] 
FROM
'p => portrait ← TRUE;
'l => portrait ← FALSE;
'f => {
IF i = argv.argc - 1 THEN RETURN[$Failure, "No text font specified\n"];
i ← i.SUCC;
textFontFamily ← argv[i];
};
'F => {
IF i = argv.argc - 1 THEN RETURN[$Failure, "No heading font specified\n"];
i ← i.SUCC;
headingFontFamily ← argv[i];
};
'n => {
IF i = argv.argc - 1 THEN RETURN[$Failure, "No column count specified\n"];
i ← i.SUCC;
columnsPerPage ← Convert.IntFromRope[argv[i] ! Convert.Error => GOTO numberSyntaxError];
IF columnsPerPage NOT IN [1..100] THEN GOTO outOfRange;
};
's => {
IF i = argv.argc - 1 THEN RETURN[$Failure, "No point size specified\n"];
i ← i.SUCC;
pointSize ← Convert.IntFromRope[argv[i] ! Convert.Error => GOTO numberSyntaxError];
IF pointSize NOT IN [4..72] THEN GOTO outOfRange;
};
't => ignoreNodes ← TRUE;
'c => 
BEGIN 
IF i = argv.argc - 1 THEN RETURN[$Failure, "No copy numbers present\n"];
i ← i.SUCC;
nCopies ← Convert.IntFromRope[argv[i] ! Convert.Error => GOTO numberSyntaxError];
END;
'h => 
BEGIN 
hostSpecified ← TRUE;
IF i = argv.argc - 1
THEN host ← NIL
ELSE IF IsParm[i.
SUCC]
THEN BEGIN i ← i.SUCC;  host ← argv[i] END
ELSE host ← NIL;
 
 
END;
'r => 
BEGIN 
retainPressFile ← TRUE;
IF i = argv.argc - 1
THEN pressFileName ← NIL
ELSE IF IsParm[i.
SUCC]
THEN BEGIN i ← i.SUCC;  pressFileName ← argv[i] END
ELSE pressFileName ← NIL;
 
 
END;
ENDCASE =>  GOTO switchSyntaxError;
 
 
LOOP;
END;
 
IF patternTail = 
NIL
THEN patternHead ← patternTail ← NEW[fileRec]
ELSE {patternTail.next ← NEW[fileRec]; patternTail ← patternTail.next; };
 
patternTail.file ← argv[i];
ENDLOOP;
 
IF pointSize < 0 THEN pointSize ← IF portrait THEN 8 ELSE 6;
IF columnsPerPage < 0 THEN columnsPerPage ← IF portrait THEN 1 ELSE 2;
EXITS
switchSyntaxError => 
RETURN[
$Failure, IO.PutFR["Unrecognized switch: \"%g\"\n", IO.rope[argv[i]]]];
numberSyntaxError => 
RETURN[
$Failure, IO.PutFR["Unrecognized number: \"%g\"\n", IO.rope[argv[i]]]];
outOfRange => 
RETURN[
$Failure, IO.PutFR["Number out of range: \"%g\"\n", IO.rope[argv[i]]]];
 
END; -- (a)
 
BEGIN 
-- Validate fileName
 (add file extension if not specified) and check hostSpecified
NameProc: FS.NameProc = 
BEGIN
-- PROC [fullFName: ROPE] RETURNS [continue: BOOLEAN]; 
ValidateName [fullFName];
RETURN [TRUE];
END;
ValidateName: 
PROC [fullFName: 
ROPE] = 
BEGIN
cp: FS.ComponentPositions;
full: ROPE;
ext: ROPE;
ok: BOOL ← TRUE;
[fullFName: full, cp: cp] ← FS.ExpandName[fullFName];
ext ← full.Substr[cp.ext.start, cp.ext.length];
Here is the place to check for bad extensions, should you want to.
IF NOT ok THEN RETURN;
IF fileTail = 
NIL
THEN fileHead ← fileTail ← NEW[fileRec]
ELSE {fileTail.next ← NEW[fileRec]; fileTail ← fileTail.next; };
 
fileTail.file ← fullFName;
RETURN;
END;
patternTail ← patternHead;
IF patternTail = 
NIL 
THEN
RETURN[$Failure, "No file or pattern specified.\n"];
 
DO
fileName ← patternTail.file;
IF fileName.Find["*"] < 0
THEN 
BEGIN
name: ROPE ← CompleteFileName[fileName];
IF name # NIL THEN ValidateName[name];
END
 
ELSE BEGIN
IF fileName.Find["!"] < 0 THEN fileName ← fileName.Concat["!h"];
FS.EnumerateForNames[fileName, NameProc, wDir];
END;
 
 
IF patternTail.next = NIL THEN EXIT;
patternTail ← patternTail.next;
ENDLOOP;
 
IF 
NOT hostSpecified 
THEN
RETURN[$Failure, "No print server specified in user profile or with \"-h\"\n"];
 
IF host.IsEmpty THEN retainPressFile ← TRUE;
END;
 
BEGIN 
-- do the real work
-- File error handing is done at this level
exists: BOOL; 
fileName: ROPE;
closeIt: BOOL;
BEGIN
ENABLE 
BEGIN
IO.Error => 
BEGIN
error: FS.ErrorDesc;
IF ec # $Failure THEN GOTO cantHandle;
error ← FS.ErrorFromStream[stream ! IO.Error => GOTO cantHandle];
cmd.out.PutF["FS error: %g\n", IO.rope[error.explanation]];
GOTO cleanupAfterError;
EXITS cantHandle => REJECT
END;
 
FS.Error => 
BEGIN
cmd.out.PutF["FS error: %g\n", IO.rope[error.explanation]];
GOTO cleanupAfterError;
END;
 
ABORTED => 
BEGIN
GOTO cleanupAfterError;
END;
 
END;
 
IF fileHead = 
NIL 
THEN
RETURN[$Failure, "No files from enumeration.\n"];
 
fileTail ← fileHead;
fileName ← fileHead.file;
closeIt ← fileHead.next # NIL;
IF fileHead.next = 
NIL
THEN -- can only send 1 press file.
IF PressPrinter.IsAPressFile[fileName]
THEN 
BEGIN
cmd.out.PutF["File %g is already in press format\n", IO.rope[fileName]];
pressFileName ← fileName;
retainPressFile ← TRUE;
END
 
ELSE 
BEGIN
pressStream: IO.STREAM;
IF pressFileName.IsEmpty[] 
THEN pressFileName ← PressFileName[
fileName, IF retainPressFile THEN "" ELSE "[]<>Temp>"];
 
pressStream ←
FS.StreamOpen[fileName: pressFileName, accessOptions: $create, keep: IF retainPressFile THEN 2 ELSE 1];
IF NOT PressFileFromFile[from: fileName, pressStream: pressStream, headingFontFamily: headingFontFamily, textFontFamily: textFontFamily, pressFileName: pressFileName, columnsPerPage: columnsPerPage, pointSize: pointSize, portrait: portrait, ignoreNodes: ignoreNodes, cmd: cmd, doNotClose: closeIt] THEN GOTO cleanupAfterError;
pressStream.Close[];
END
 
 
 
ELSE BEGIN
pressStream: IO.STREAM;
IF pressFileName.IsEmpty[] 
THEN
pressFileName ← PressFileName[
fileName, IF retainPressFile THEN "" ELSE "[]<>Temp>"];
 
pressStream ← 
FS.StreamOpen[fileName: pressFileName, accessOptions: $create,
keep: IF retainPressFile THEN 2 ELSE 1];
DO
IF PressPrinter.IsAPressFile[fileName]
THEN 
BEGIN
cmd.out.PutF["File %g is already in press format.\nI can send only one at a time\n", IO.rope[fileName]];
GOTO cleanupAfterError;
END;
 
 
IF NOT PressFileFromFile[from: fileName, pressStream: pressStream, headingFontFamily: headingFontFamily, textFontFamily: textFontFamily, pressFileName: pressFileName, columnsPerPage: columnsPerPage, pointSize: pointSize, portrait: portrait, ignoreNodes: ignoreNodes, cmd: cmd, doNotClose: closeIt] THEN GOTO cleanupAfterError;
IF fileTail.next = NIL THEN EXIT;
fileTail ← fileTail.next;
fileName ← fileTail.file;
closeIt ← fileTail.next # NIL;
ENDLOOP;
 
pressStream.Close[];
END;
 
 
IF 
NOT host.IsEmpty[] 
THEN
[result, msg] ← SendPressFile[pressFileName, host, nCopies, cmd];
 
IF result # NIL THEN GOTO cleanupAfterError;
IF 
NOT retainPressFile 
THEN 
BEGIN
cmd.out.PutF["Deleting file %g ... ", IO.rope[pressFileName]];
FS.Delete[pressFileName];
cmd.out.PutChar['\n];
END;
 
[exists: exists] ← FileCheck["[]<>Temp>Print.temp"];
IF exists 
THEN 
BEGIN
cmd.out.PutRope["Deleting file []<>Temp>Print.temp ... "];
FS.Delete["[]<>Temp>Print.temp"];
cmd.out.PutChar['\n];
END;
 
cmd.out.PutRope["Finished Print.\n"];
RETURN [result: NIL, msg: NIL];
EXITS
cleanupAfterError => 
BEGIN
cmd.out.PutRope["Aborted Print.\n"];
RETURN [result: $Failure, msg: NIL];
END;
 
END;
 
END;
 
END;--DoPrint
 
printHelpText: ROPE = "Usage:
Print [-p] [-n nCols] [-s sizeFont] [-f textFont] [-F headingFont]
  [-h [hostName]] [-r [pfName]] [-c nCopies] [-t] file
 -p   portrait mode (default is landscape)
 -n nCols   number of columns (default is 2 for landscape, 1 for portrait)
 -s sizeFont   size of font in points (default is 6 for landscape, 8 for portrait)
 -f textFont   font to use for the text of each page (default is Gacha)
 -F headingFont   font to use for the heading of each page (default is Gacha)
 -h [hostName]   name of printer, empty sends to no printer (default is Hardcopy.PressPrinter entry of user profile)
 -r [pfName]   retain press file, naming it pfName if specified
 -c nCopies   number of copies to print
 -t   print text only, without indenting to show Tioga nodes
 
The file extension defaults according to the Tioga.SourceFileExtensions entry of user profile, like the Open command.  If the file is already in press format, it is simply sent to a printer.
";
Commander.Register["Print", DoPrint, printHelpText];