-- AltoDirMain.mesa
-- edited by Barth, October 14, 1980 2:26 PM
-- edited by Brotz, November 26, 1980 4:54 PM
-- edited by Schroeder, Wednesday Oct. 29, 1980 12:08 pm PST

DIRECTORY
AltoFileDefs,
crD: FROM "CoreDefs",
csD: FROM "CoreStreamDefs",
DirectoryDefs,
ImageDefs,
intCommon: FROM "IntCommon",
IODefs,
LaurelExecDefs,
MatchDefs,
ovD: FROM "OverviewDefs",
SegmentDefs,
Storage,
StreamDefs,
StringDefs,
TimeDefs;

AltoDirMain: PROGRAM
IMPORTS crD, csD, DirectoryDefs, ImageDefs, intC: intCommon, IODefs,
LaurelExecDefs, MatchDefs, SegmentDefs, Storage, StreamDefs, StringDefs,
TimeDefs =

BEGIN
OPEN StringDefs, IODefs;

-- Constants

numBufferPages: CARDINAL = 2;
promptChar: CHARACTER = ’>;

-- Types

commandIndex: TYPE = {rename, filestat, copy, delete, type, list, quit};

-- Variables

capFileFilter: STRING ← [50];
argList: STRING ← Storage.String[50];
fileStatString: STRING;
cc: CARDINAL;
sawFile: BOOLEAN;
ci: commandIndex;

-- Routines

WriteError: PROCEDURE [error: STRING]=
BEGIN
WriteLine[""L];
WriteString["? "L];
WriteLine[error];
END; -- of WriteError


IsSpace: PROCEDURE[c: CHARACTER] RETURNS [BOOLEAN] =
{RETURN[c = TAB OR c = SP]};


GetCommand: PROCEDURE RETURNS [ci: commandIndex] =
-- Entry Invariants: argList should contain the default command line, if any, if
-- argList.length is zero then the default will be set to "list *.mail", the display
-- should be at the beginning of the line the command is to be typed in on.
-- Exit Invariants: ci will contain a valid commandIndex, argList will contain the
-- remainder of the command line after parsing off the command name by
-- looking for [SP]*[~SP]*[SP]*, the display will be at the beginning of the
-- line following the line the command was typed on.
BEGIN
maxCommandLength: CARDINAL = 8; -- update this if maxlength(ac) changes
ac: ARRAY commandIndex OF STRING =
[rename: "RENAME"L,
filestat: "FILESTAT"L,
copy: "COPY"L,
delete: "DELETE"L,
type: "TYPE"L,
list: "LIST"L,
quit: "QUIT"L];

TestEndChar: PROCEDURE [c: CHARACTER] RETURNS [BOOLEAN] =
{RETURN[c = CR OR c = ’?]};

RubOutCommand: PROCEDURE =
BEGIN
argList.length ← 0;
WriteLine[" XXX"L];
WriteChar[promptChar];
END; -- of RubOutCommand --

ParseCommand: PROCEDURE RETURNS [BOOLEAN] =
BEGIN
i, i2: CARDINAL ← 0;
tci: commandIndex;
matchCount: CARDINAL ← 0;
cm: STRING ← [maxCommandLength];

SkipSpaces: PROCEDURE =
{WHILE IsSpace[argList[i]] AND i < argList.length DO i ← i + 1 ENDLOOP};

IF argList.length = 0 THEN -- ignore blank lines --
{WriteLine[""L]; RETURN[FALSE]};
SkipSpaces[]; -- skip leading spaces
UNTIL argList.text[i] = SP OR i >= argList.length DO -- pick out the command
IF cm.length < cm.maxlength THEN
{cm[cm.length] ← argList[i]; cm.length ← cm.length+1}
ELSE {WriteError["Unknown Command"L]; RETURN[FALSE]};
i ← i+1;
ENDLOOP;
SkipSpaces[]; -- skip trailing spaces
FOR i2 IN [i .. argList.length) DO
argList[i2 - i] ← argList[i2]
ENDLOOP;
argList.length ← argList.length - i; -- reduce command line size by command length
MatchDefs.Capitalize[cm, cm]; -- convert to canonical form for matching
FOR tci IN commandIndex DO
BEGIN
IF cm.length > ac[tci].length THEN LOOP;
FOR i2 IN [0 .. cm.length) DO
IF cm[i2] # ac[tci].text[i2] THEN GOTO notfound;
ENDLOOP;
ci ← tci; -- this command matches at least partially, remember it
IF cm.length = ac[tci].length THEN RETURN[TRUE]; -- exact match
matchCount ← matchCount + 1;
EXITS
notfound => NULL;
END;
ENDLOOP;
SELECT matchCount FROM
= 1 => RETURN[TRUE]; -- only one partial match, use it
> 1 => WriteError["Ambiguous Command"L];
ENDCASE => WriteError["Unknown Command"L];
RETURN[FALSE];
END; -- of ParseCommand --

IF argList.length = 0 THEN AppendString[to: argList, from: "list *.mail"L];
DO
WriteChar[promptChar];
SELECT ReadEditedString[argList, TestEndChar, TRUE
! Rubout => {RubOutCommand; RESUME};
LineOverflow => {IncreaseStringSize[]; RESUME[argList]}] FROM
CR => IF ParseCommand[] THEN {WriteLine[""L]; RETURN};
’? => BEGIN
WriteLine["?"L];
WriteString["Commands are:"L];
FOR ci IN commandIndex DO
WriteString[" "L];
WriteString[ac[ci]];
ENDLOOP;
WriteLine[""L];
WriteLine["Control DEL cancels commands in progress."L];
END;
ENDCASE;
ENDLOOP;
END; -- of GetCommand --


GetAtom: PROCEDURE [a: STRING] RETURNS [BOOLEAN]=
BEGIN
IF cc >= argList.length THEN RETURN[FALSE];
a.length ← 0;
UNTIL IsSpace[argList[cc]] OR cc >= argList.length DO
a[a.length] ← argList[cc];
a.length ← a.length + 1;
cc ← cc + 1;
ENDLOOP;
WHILE IsSpace[argList[cc]] AND cc < argList.length DO
cc ← cc + 1;
ENDLOOP;
RETURN[TRUE];
END; -- of GetAtom --


IncreaseStringSize: PROCEDURE =
BEGIN
temp: STRING ← Storage.String[argList.length + 50];
AppendString[temp, argList];
Storage.FreeString[argList];
argList ← temp;
END; -- of IncreaseStringSize --


ScanDirectory: PROCEDURE [processFile: PROC [POINTER TO AltoFileDefs.FP,
STRING] RETURNS [BOOLEAN]] =
BEGIN
fileFilter: STRING ← [50];
cc: CARDINAL ← 0;

sawFile ← FALSE;
WHILE GetAtom[fileFilter] DO
IF StreamDefs.ControlDELtyped[] THEN EXIT;
MatchDefs.Capitalize[s: fileFilter, capS: capFileFilter];
DirectoryDefs.EnumerateDirectory[processFile];
ENDLOOP;
IF ~sawFile AND ~StreamDefs.ControlDELtyped[] THEN
WriteLine["No such files"L]
ELSE IF ci = list THEN WriteLine[""L];
END; -- of ScanDirectory --


FileMessage: PROCEDURE[msg: STRING, fileName: STRING] =
BEGIN
WriteString[msg];
WriteChar[SP];
WriteLine[fileName];
END; -- of FileMessage --


DeleteAFile: PROCEDURE [p: POINTER TO AltoFileDefs.FP, fileName: STRING]
RETURNS [BOOLEAN] =
BEGIN
fileHandle: crD.UFileHandle;
errorCode: ovD.ErrorCode;
capFileName: STRING ← [50];

IF StreamDefs.ControlDELtyped[] THEN RETURN[TRUE];
MatchDefs.Capitalize[s: fileName, capS: capFileName];
IF ~MatchDefs.IsMatch[name: capFileName, pattern: capFileFilter] THEN RETURN[FALSE];
sawFile ← TRUE;
[errorCode, fileHandle] ← crD.OpenFile[intC.user, fileName, read];
IF errorCode # ovD.ok THEN FileMessage["Can’t open file"L, fileName]
ELSE BEGIN
WriteString["Delete File "L];
WriteString[fileName];
WriteString[" [Confirm]"L];
SELECT ReadChar[] FROM
SP, CR, ’y, ’Y, ESC => BEGIN
errorCode ← crD.DeleteFile[fileHandle];
IF errorCode # ovD.ok THEN WriteLine[" Can’t delete"L]
ELSE WriteLine[" Deleted"L];
END;
ENDCASE => BEGIN
errorCode ← crD.CloseFile[fileHandle];
IF errorCode # ovD.ok THEN WriteLine[" Can’t close file"L];
WriteLine[" Not deleted"L];
END;
END;
RETURN[FALSE];
END; -- of DeleteAFile --


TypeFile: PROCEDURE [p: POINTER TO AltoFileDefs.FP, fileName: STRING]
RETURNS [BOOLEAN]=
BEGIN
capFileName: STRING ← [50];
bsh: csD.StreamHandle;
char: CHARACTER;
inBravoStuff: BOOLEAN ← FALSE;

MatchDefs.Capitalize[s: fileName, capS: capFileName];
IF ~MatchDefs.IsMatch[name: capFileName, pattern: capFileFilter] THEN
RETURN[FALSE];
sawFile ← TRUE;
bsh ← csD.OpenFromName[fileName, intC.user, byte, read, 1
! csD.Error => {FileMessage["Can’t open file"L, fileName]; GO TO Return}];
DO
IF StreamDefs.ControlDELtyped[] THEN EXIT;
char ← csD.Read[bsh ! csD.Error =>
BEGIN
IF reason # ovD.endOfStream
THEN FileMessage["Error Reading File"L, fileName];
EXIT;
END];
IF inBravoStuff
THEN inBravoStuff ← char # CR
ELSE inBravoStuff ← char = ControlZ;
IF ~inBravoStuff THEN WriteChar[char];
ENDLOOP;
csD.Destroy[bsh];
GO TO Return;
EXITS
Return => RETURN[StreamDefs.ControlDELtyped[]];
END; -- of TypeFile --


ListFile: PROCEDURE[p: POINTER TO AltoFileDefs.FP, fileName: STRING]
RETURNS [BOOLEAN]=
BEGIN
capFileName: STRING ← [50];

IF StreamDefs.ControlDELtyped[] THEN RETURN[TRUE];
MatchDefs.Capitalize[s: fileName, capS: capFileName];
IF MatchDefs.IsMatch[name: capFileName, pattern: capFileFilter] THEN
BEGIN
IF fileName[fileName.length - 1] = ’. THEN
fileName.length ← fileName.length - 1; -- remove trailing period
WriteString[fileName];
WriteString[" "L];
sawFile ← TRUE;
END;
RETURN[FALSE];
END; -- of ListFile --


oFileName: STRING ← [50];
obsh: csD.StreamHandle;


InnerCopyLoop: PROCEDURE[p:POINTER TO AltoFileDefs.FP, iFileName:STRING]
RETURNS [BOOLEAN]=
BEGIN
ibsh: csD.StreamHandle;
capFileName: STRING ← [50];
flagQuit: BOOLEAN ← FALSE;

MatchDefs.Capitalize[s: iFileName, capS: capFileName];
IF ~MatchDefs.IsMatch[name: capFileName, pattern: capFileFilter] THEN
RETURN[FALSE];
sawFile ← TRUE;
BEGIN ENABLE csD.Error =>
BEGIN
FileMessage[SELECT reason FROM
ovD.illegalFilename, ovD.fileInUse => "Can’t open file"L
ENDCASE => "Disk error while copying"L,
iFileName];
flagQuit ← TRUE;
CONTINUE
END;
ibsh ← csD.OpenFromName[iFileName, intC.user, byte, read, numBufferPages];
csD.StreamCopy[from: ibsh, to: obsh, fromItems: csD.GetLength[ibsh]];
END;
csD.Destroy[ibsh];
flagQuit ← flagQuit OR StreamDefs.ControlDELtyped[];
RETURN[flagQuit];
END; -- of InnerCopyLoop --


CopyFile: PROCEDURE =
BEGIN
errorCode: ovD.ErrorCode;
ofh: crD.UFileHandle;
oFileName:STRING ← [50];

IF ~GetAtom[oFileName]
OR ~(argList[cc] = ’← AND IsSpace[argList[cc + 1]]) THEN
{WriteLine["Form is: destination ← source source ..."L]; RETURN};
cc ← cc + 2; -- throw away seperator
WHILE IsSpace[argList[cc]] DO
cc ← cc+ 1;
ENDLOOP;
[errorCode, ofh] ← crD.OpenFile[intC.user, oFileName, read];
IF crD.CloseFile[ofh] # ovD.ok THEN
FileMessage["Couldn’t close file"L, oFileName];
IF errorCode = ovD.ok THEN
{FileMessage["File already exists:"L, oFileName]; RETURN};
[errorCode, ofh] ← crD.OpenFile[intC.user, oFileName, update];
IF errorCode # ovD.ok THEN
{FileMessage["Can’t open file"L, oFileName]; RETURN};
obsh ← csD.Open[ofh, byte, overwrite, numBufferPages];
ScanDirectory[InnerCopyLoop];
csD.Close[obsh ! csD.Error =>
{FileMessage["Couldn’t close byte stream for file"L, oFileName]; CONTINUE}];
IF crD.CloseFile[ofh] # ovD.ok THEN
FileMessage["Couldn’t close file"L, oFileName];
END; -- of CopyFile --


StatFile: PROCEDURE [p: POINTER TO AltoFileDefs.FP, fileName: STRING]
RETURNS [BOOLEAN]=
BEGIN
capFileName: STRING ← [50];
afh: SegmentDefs.FileHandle;
page, byte: CARDINAL;
bytes: LONG CARDINAL;
read, write, create: TimeDefs.PackedTime;

WriteTime: PROCEDURE[s: STRING, t: TimeDefs.PackedTime]=
BEGIN
AppendString[fileStatString, s];
AppendChar[fileStatString, ’:];
TimeDefs.AppendDayTime[fileStatString, TimeDefs.UnpackDT[t]];
AppendString[fileStatString, " "L];
END; -- of WriteTime --

IF StreamDefs.ControlDELtyped[] THEN RETURN[TRUE];
MatchDefs.Capitalize[s: fileName, capS: capFileName];
IF ~MatchDefs.IsMatch[name: capFileName, pattern: capFileFilter]
THEN RETURN[FALSE];
sawFile ← TRUE;
afh ← SegmentDefs.NewFile[fileName, SegmentDefs.Read
! SegmentDefs.FileNameError => {afh ← NIL; CONTINUE}];
IF afh = NIL THEN
{FileMessage["Can’t open file"L, fileName]; RETURN[FALSE]};
[read, write, create] ← SegmentDefs.GetFileTimes[afh];
[page, byte] ← SegmentDefs.GetEndOfFile[afh];
fileStatString.length ← 0;
AppendString[fileStatString, fileName];
AppendString[fileStatString, " "L];
bytes ← byte + LONG[(page - 1)] * 512;
AppendLongNumber[fileStatString, bytes, 10];
AppendString[fileStatString, " bytes "L];
AppendNumber[fileStatString, IF byte = 0 THEN page + 2 ELSE page + 1, 10];
AppendString[fileStatString, " pages
"L];
WriteTime[" Create"L, create];
WriteTime["Write"L, write];
WriteTime["Read"L, read];
WriteLine[fileStatString];
SegmentDefs.SetFileTimes[afh, read, write, create];
SegmentDefs.ReleaseFile[afh ! SegmentDefs.FileError => CONTINUE];
RETURN[FALSE];
END; -- of StatFile --


RenameFile: PROCEDURE=
BEGIN
oldFileName: STRING ← [50];
newFileName: STRING ← [50];
fh: crD.UFileHandle;
errorCode: ovD.ErrorCode;
fp: AltoFileDefs.FP;
dir: StreamDefs.DiskHandle;
cfh: SegmentDefs.FileHandle;

ComplainSyntax: PROCEDURE =
{WriteLine[
"Form is: oldfilename newfilename OR newfilename ← oldfilename"L]};

IF ~GetAtom[oldFileName] OR ~GetAtom[newFileName] THEN
{ComplainSyntax; RETURN};
IF newFileName.length = 1 AND newFileName[0] = ’← THEN
BEGIN
newFileName.length ← 0;
AppendString[newFileName, oldFileName];
IF ~GetAtom[oldFileName] THEN {ComplainSyntax; RETURN}
END;
IF ~EquivalentStrings[oldFileName, newFileName] THEN
BEGIN
[errorCode, fh] ← crD.OpenFile[intC.user, newFileName, read];
IF crD.CloseFile[fh] # ovD.ok THEN
FileMessage["Couldn’t close file"L, newFileName];
IF errorCode = ovD.ok THEN
{WriteString[newFileName]; WriteLine[" already exists"]; RETURN};
END;
IF ~DirectoryDefs.DirectoryLookup[@fp, oldFileName, FALSE] THEN
{FileMessage["Cannot find"L, oldFileName]; RETURN};
IF ~DirectoryDefs.DirectoryPurgeFP[@fp] THEN
{FileMessage["Couldn’t remove from directory:"L, oldFileName]; RETURN};
dir ← StreamDefs.CreateWordStream
[SegmentDefs.NewFile["SysDir", SegmentDefs.ReadWriteAppend],
StreamDefs.ReadWriteAppend];
IF newFileName[newFileName.length - 1] # ’. THEN
BEGIN
newFileName[newFileName.length] ← ’.;
newFileName.length ← newFileName.length + 1;
END;
IF DirectoryDefs.Insert[dir, @fp, newFileName] THEN
FileMessage["Couldn’t insert new file:", newFileName];
dir.destroy[dir];
IF (cfh ← crD.LookupInFileCache[oldFileName]) # NIL THEN
crD.FreeCacheEntry[cfh];
IF (cfh ← crD.LookupInFileCache[newFileName]) # NIL THEN
crD.FreeCacheEntry[cfh];
END; -- of RenameFile --


WriteHerald: PROCEDURE =
BEGIN
time: STRING ← [25];
TimeDefs.AppendDayTime
[time, TimeDefs.UnpackDT[ImageDefs.BcdVersion[].time]];
WriteString["AltoDir of "L];
WriteLine[time];
WriteLine["Type ? for help."L];
END; -- of WriteHerald --


-- Main Program

LaurelExecDefs.MakeMenuCommandCallable[newMail];
LaurelExecDefs.MakeMenuCommandCallable[mailFile];
LaurelExecDefs.MakeMenuCommandCallable[display];
LaurelExecDefs.MakeMenuCommandCallable[delete];
LaurelExecDefs.MakeMenuCommandCallable[undelete];
LaurelExecDefs.MakeMenuCommandCallable[moveTo];
WriteHerald[];
DO
ci ← GetCommand[];
cc ← 0;
StreamDefs.ResetControlDEL;
SELECT ci FROM
rename => RenameFile;
filestat => BEGIN
fileStatString ← Storage.String[200];
ScanDirectory[StatFile];
Storage.FreeString[fileStatString];
END;
delete => ScanDirectory[DeleteAFile];
type => ScanDirectory[TypeFile];
list => ScanDirectory[ListFile];
copy => CopyFile;
quit => EXIT;
ENDCASE;
ENDLOOP;

Storage.FreeString[argList];

END. -- of AltoDirMain --