/* * Simple Spooler * Copyright (C) 2026, Ken McLeod. * * This application registers itself as a virtual printer on the AppleTalk * network and receives print jobs using PAP (Printer Access Protocol). * It can optionally send the print job to a locally-connected ImageWriter * printer which otherwise would not be visible on the network. It can also * generate a PostScript file from the print job automatically, if the IWEm * file is present in the spool directory. * * Written by: Ken McLeod, 2026-01-07 * Last update: 2026-07-27 * * Some code was adapted from the Neighborhood Watch sample application * by Ricardo Batista, found on Apple Developer CD Volume IX, and from the * MoreFiles utility routines by Jim Luther, found on the March 1994 Dev CD. * * This software is provided as-is, with no warranties expressed or implied, * under the terms of the BSD 2-Clause License. See separate "LICENSE" file. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Logging.h" #include "SpoolFiles.h" #include "PrintQueue.h" #include "PAP.h" /* resource IDs */ #define appleMenu 128 #define fileMenu 129 #define editMenu 130 #define controlMenu 131 #define quitItem 1 #define undoItem 1 #define cutItem 3 #define copyItem 4 #define pasteItem 5 #define clearItem 6 #define selectAllItem 8 #define startSpoolerItem 1 #define stopSpoolerItem 2 #define settingsItem 4 #define statusItem 5 #define okItem 1 #define cancelItem 2 #define spoolerNameItem 3 #define iconLWItem 6 #define iconLQItem 7 #define iconIWItem 8 #define modemPortItem 10 #define printerPortItem 11 #define colorRibbonCBoxItem 13 #define sheetFeederCBoxItem 14 #define iw1ModeCBoxItem 15 #define noReverseCBoxItem 16 #define printingEnabledItem 17 #define scheduleUserItem 18 #define printerTypeUserItem 19 #define localPortUserItem 20 #define autoRunCBoxItem 21 #define logWindowID 128 #define spoolerDialogID 128 #define upDownArrowsPictID 128 #define typeStringsID 128 #define nameStringsID 129 #define portStringsID 130 #define buttonStringsID 131 #define okStrIndex 1 #define cancelStrIndex 2 #define startStrIndex 3 #define saveStrIndex 4 #define kIconFrameInset -5 #define kIconFrameSize 2 #define kButtonFrameInset -4 #define kButtonFrameSize 3 #define kButtonFrameRadius 18 #define kMinWindowHeight 100 #define kMinWindowWidth 416 #define kPrefsVersion 3 /* function prototypes */ void DoAbout(void); void DoStatus(void); OSErr ConfigureSpooler(Boolean startingServer); OSErr StartSpooler(void); OSErr StopSpooler(void); void SetMenus(void); Boolean Setup(void); Boolean RunEventLoop(void); Boolean DoCommand(long mResult); Boolean HandleMouseDowns(void); void LoadPreferences(); void SavePreferences(); char ourName[256]; char ourType[34]; char ourPort[34]; char ourPrefix[4]; int serverRef = 0; Boolean inFront; EventRecord myEvent; MenuHandle myMenu[5]; WindowPtr textWindow = 0L; TEHandle textTE = 0L; Rect windowRect = { 40, 10, 320, 490 }; /* default, global coordinates */ ControlHandle textScrollBar = 0L; PicHandle arrowsPictHdl = 0L; short defaultPrinterItem = iconIWItem; short defaultPortItem = modemPortItem; Boolean defaultColorRibbon = false; Boolean defaultSheetFeeder = false; Boolean defaultPrintEnabled = true; Boolean defaultIW1Mode = false; Boolean defaultNoReverse = false; Boolean defaultAutoRun = false; short curSelectedIndex = 0; /* selected segment in date scheduler */ short schedStartHour = 0; /* default start to 12 AM */ short schedEndHour = 24; /* default end to next 12 AM */ char sAMStr[4]; /* pascal string with first 2 chars of AM string from itl0 */ char sPMStr[4]; /* pascal string with first 2 chars of PM string from itl0 */ char s12HrCycle; /* values: 0=24hr, 1=12hr */ char *sepStr = "\p----------------------------------------------------"; void main() { if (!Setup()) { return; } /* either auto-run the server, or present the settings panel */ DoCommand((controlMenu << 16) + startSpoolerItem); while (RunEventLoop()) ; StopSpooler(); SavePreferences(); UnloadLogWindow(); } Boolean Setup(void) { short count; for (count = 0; count < 6; count++) { MoreMasters(); } InitGraf(&qd.thePort); InitFonts(); InitWindows(); TEInit(); InitMenus(); InitDialogs(0L); DrawMenuBar(); FlushEvents(everyEvent,0L); InitCursor(); MaxApplZone(); SetCursor(*GetCursor(watchCursor)); InitAllPacks(); SetMenus(); if (!InitLogWindow(logWindowID, &textWindow, &textTE, &textScrollBar)) { return(false); } if (InitSpoolFolder() != noErr) { Message(LOG_ERR_X,"\pCould not find or create spool folder!"); } LoadPreferences(); ShowLogWindow(&windowRect); inFront = true; return(true); } void SetMenus() { /* load menus from resource file */ int i; for (i=appleMenu-appleMenu; i<=controlMenu-appleMenu; i++) { myMenu[i] = GetMenu(appleMenu+i); } AddResMenu(myMenu[appleMenu-appleMenu],'DRVR'); /* install menus in menu bar */ for (i=appleMenu-appleMenu; i<=controlMenu-appleMenu; i++) { InsertMenu(myMenu[i], 0); } EnableItem(myMenu[controlMenu-appleMenu],startSpoolerItem); EnableItem(myMenu[editMenu-appleMenu],copyItem); EnableItem(myMenu[editMenu-appleMenu],selectAllItem); DrawMenuBar(); } Boolean RunEventLoop() { register char theChar; register Boolean event; WindowPtr w; GrafPtr savePort; Rect box; Point mouse; CheckATP(); /* check incoming requests and responses */ CheckPrintQueue(defaultPrintEnabled, schedStartHour, schedEndHour); event = GetNextEvent(everyEvent,&myEvent); w = FrontWindow(); if ((w == textWindow) && inFront && textTE) { GetPort(&savePort); SetPort(textWindow); /* %%% don't need to blink insertion point; text isn't editable */ /*TEIdle(textTE);*/ box = w->portRect; GetMouse(&mouse); box.right -= 16; box.bottom -= 16; if (PtInRect(mouse,&box)) { SetCursor(*(GetCursor(iBeamCursor))); } else { InitCursor(); } SetPort(savePort); } if (event) { switch (myEvent.what) { case app4Evt: inFront = (myEvent.modifiers & 128) ? true : false; break; case activateEvt: w = (WindowPtr) myEvent.message; if (w == textWindow) { ActivateLogWindow(myEvent.modifiers & activeFlag); } break; case keyDown: case autoKey: theChar = myEvent.message & charCodeMask; if (myEvent.modifiers & cmdKey) { if (myEvent.modifiers & shiftKey) { if (theChar == 'l') { SetLogLevel(LogLevel()+1); if (LogLevel() > LOG_DEBUG) { SetLogLevel(LOG_ERR); } MessageWithIntValue(LOG_ERR,"\pSetting log level:", LogLevel()); } else if (theChar == 's') { DoStatus(); } } if (!DoCommand(MenuKey(theChar))) { return(false); } } else { w = FrontWindow(); /* %%% don't accept keyboard input into log text */ #if 0 if (w == textWindow) { short lines; HLock((Handle)textTE); lines = (**textTE).nLines; TEKey(theChar,textTE); if (lines != (**textTE).nLines) AdjustTextScrollBar(); HUnlock((Handle)textTE); } #endif } break; case updateEvt: w = (WindowPtr) myEvent.message; if (w == textWindow) UpdateLogWindow(); break; case mouseDown: if (!HandleMouseDowns()) return(false); break; default: break; } } return(true); } Boolean DoCommand(long mResult) { OSErr err; register short theItem; char st[256]; theItem = LoWord(mResult); switch (HiWord(mResult)) { case appleMenu: GetItem(myMenu[0],theItem,st); if (theItem > 2) OpenDeskAcc(st); else DoAbout(); break; case fileMenu: switch (theItem) { case quitItem: return(false); break; default: break; } break; case editMenu: if (!SystemEdit(theItem-1)) { switch (theItem) { case undoItem: break; case cutItem: if (textTE) { TECut(textTE); AdjustTextScrollBar(); } ZeroScrap(); TEToScrap(); break; case copyItem: if (textTE) { TECopy(textTE); } ZeroScrap(); TEToScrap(); break; case pasteItem: TEFromScrap(); if (textTE) { TEPaste(textTE); AdjustTextScrollBar(); } break; case clearItem: if (textTE) { TEDelete(textTE); AdjustTextScrollBar(); } break; case selectAllItem: if (textTE) { int index; HLock((Handle)textTE); index = (**textTE).teLength; HUnlock((Handle)textTE); TESetSelect(0,index,textTE); } break; default: break; } } break; case controlMenu: switch (theItem) { case startSpoolerItem: err = StartSpooler(); if (err == noErr) { StartPrintQueue(ourPrefix,ourPort); Message(LOG_ERR,"\pSpooler started"); EnableItem(myMenu[controlMenu-appleMenu],stopSpoolerItem); DisableItem(myMenu[controlMenu-appleMenu],startSpoolerItem); } else { Message(LOG_ERR,"\pSelect Control > Start Spooler to start the server"); EnableItem(myMenu[controlMenu-appleMenu],startSpoolerItem); DisableItem(myMenu[controlMenu-appleMenu],stopSpoolerItem); } break; case stopSpoolerItem: err = StopSpooler(); if (err == noErr) { StopPrintQueue(); Message(LOG_ERR,"\pSpooler stopped"); } else { MessageWithIntValue(LOG_ERR,"\pERROR: Could not stop spooler:",err); } EnableItem(myMenu[controlMenu-appleMenu],startSpoolerItem); DisableItem(myMenu[controlMenu-appleMenu],stopSpoolerItem); break; case settingsItem: err = ConfigureSpooler(false); if (err == noErr && serverRef) { /* spooler already running, recursively stop and restart it */ Boolean savedAutoRun = defaultAutoRun; DoCommand((controlMenu << 16) + stopSpoolerItem); defaultAutoRun = true; /* don't put up the dialog twice */ DoCommand((controlMenu << 16) + startSpoolerItem); defaultAutoRun = savedAutoRun; } break; case statusItem: DoStatus(); break; default: break; } break; default: break; } HiliteMenu(0); return(true); } Boolean HandleMouseDowns() { WindowPtr whichWindow; Rect box; long new; short v, h; switch (FindWindow(myEvent.where,&whichWindow)) { case inSysWindow: SystemClick(&myEvent,whichWindow); break; case inMenuBar: return(DoCommand(MenuSelect(myEvent.where))); break; case inGrow: box = qd.screenBits.bounds; box.top = kMinWindowHeight; box.left = kMinWindowWidth; new = GrowWindow(whichWindow,myEvent.where,&box); if (new) { v = HiWord(new); h = LoWord(new); SetPort(whichWindow); SizeWindow(whichWindow,h,v,true); EraseRect(&(whichWindow->portRect)); InvalRect(&(whichWindow->portRect)); if (whichWindow == textWindow) { GrowLogWindow(h,v); } } break; case inGoAway: if (!TrackGoAway(whichWindow,myEvent.where)) break; break; case inDrag: if ((myEvent.modifiers & cmdKey) || (FrontWindow() == whichWindow)) DragWindow(whichWindow,myEvent.where,&qd.screenBits.bounds); else { SelectWindow(whichWindow); SetPort(whichWindow); } break; case inContent: if (whichWindow != FrontWindow()) { SelectWindow(whichWindow); SetPort(whichWindow); } else { if (whichWindow == textWindow) { HandleMouseInText(&myEvent); } } break; default: break; } return(true); } void CenterRect(Rect *r) { /* centers the rectangle horizontally on main screen, * one-third of the way down vertically from the top. */ OffsetRect(r, - r->left + ((qd.screenBits.bounds.right - qd.screenBits.bounds.left) - (r->right - r->left)) / 2, - r->top + (((qd.screenBits.bounds.bottom - qd.screenBits.bounds.top - GetMBarHeight()) - (r->bottom - r->top)) / 3) + GetMBarHeight()); } DialogTHndl CenterDialog(short whichID) { DialogTHndl d = (DialogTHndl) GetResource('DLOG', whichID); if ((d != 0L) && (ResError() == noErr)) { HLock((Handle)d); CenterRect(&(*d)->boundsRect); HUnlock((Handle)d); } return(d); } void LoadDateFormatStrings(void) { /*Intl0Hndl h = (Intl0Hndl)GetResource('itl0',16383); /* explicit user overrides? */ Intl0Hndl h = (Intl0Hndl)IUGetIntl(0); /* get current date & time formats */ if (h) { HLock((Handle)h); s12HrCycle = ((**h).timeCycle == 0xFF) ? true:false; /* 0x00,0x01:24h, 0xFF:12hr */ if (s12HrCycle) { /* these are C strings with a leading space character that we skip */ BlockMove(&(**h).mornStr[1],&sAMStr[1],2); BlockMove(&(**h).eveStr[1],&sPMStr[1],2); sAMStr[0] = sPMStr[0] = 2; } else { BlockMove("\p00",sAMStr,3); BlockMove(sAMStr,sPMStr,3); } HUnlock((Handle)h); ReleaseResource((Handle)h); } else { s12HrCycle = true; BlockMove("\pAM",sAMStr,3); BlockMove("\pPM",sPMStr,3); } } void GetScheduleStrings(short scheduledHour, char *theHourStr, char *theAmPmStr) { /* fill in the provided Pascal string buffers, given scheduledHour as 0..24 */ /* note: each buffer must be able to hold at least 2 bytes + 1 length byte. */ short hour = scheduledHour; BlockMove(sAMStr,theAmPmStr,3); if (hour >= 12) { hour = (hour == 12) ? 12 : hour - 12; if (scheduledHour < 24) { BlockMove(sPMStr,theAmPmStr,3); } } theHourStr[0] = 2; if (!s12HrCycle) { theHourStr[1] = (scheduledHour < 10) ? '0':(scheduledHour < 20) ? '1':'2'; theHourStr[2] = '0' + (scheduledHour % 10); } else if (hour > 9) { theHourStr[1] = '1'; theHourStr[2] = '0' + (hour - 10); } else if (hour < 1) { theHourStr[1] = '1'; theHourStr[2] = '2'; } else { theHourStr[1] = ' '; theHourStr[2] = '1' + (hour - 1); } } void HandleClickInScheduler(Point pt, Rect *r, Boolean redrawAll) { Boolean redraw = redrawAll; short index; Rect tmp = *r; Rect parts[7] = { { tmp.top, tmp.left+0, tmp.bottom, tmp.left+0 }, { tmp.top, tmp.left+2, tmp.bottom, tmp.left+21 }, { tmp.top, tmp.left+22, tmp.bottom, tmp.left+43 }, { tmp.top, tmp.left+44, tmp.bottom, tmp.left+54 }, { tmp.top, tmp.left+54, tmp.bottom, tmp.left+73 }, { tmp.top, tmp.left+74, tmp.bottom, tmp.left+95 }, { tmp.top, tmp.left+98, tmp.bottom, tmp.right }, }; for (index = 1; index < 6; index++) { if (index == 3 || (!s12HrCycle && (index == 2 || index == 5))) { continue; /* not selectable */ } if (PtInRect(pt, &parts[index])) { InvertRect(&parts[curSelectedIndex]); /* deselect the current selection first */ if (curSelectedIndex == index) { curSelectedIndex = 0; /* we deselected this, so now there is no selection */ } else { InvertRect(&parts[index]); curSelectedIndex = index; /* this index is now the current selection */ } break; } } if (PtInRect(pt, &parts[6])) { /* click in arrow control */ Boolean increase = false; redraw = true; if (pt.v < parts[6].top + ((parts[6].bottom-parts[6].top)/2)) { /* down arrow, increase or toggle value */ increase = true; } switch (curSelectedIndex) { case 0: /* nothing was selected, so select first item as a hint */ curSelectedIndex = 1; InvertRect(&parts[curSelectedIndex]); break; case 1: /* start hour (0..24) */ if (increase) { if (schedStartHour < 24) { schedStartHour++; } } else { if (schedStartHour > 0) { schedStartHour--; } } if (schedStartHour > schedEndHour) { schedEndHour++; /* push it forward */ } break; case 2: /* start am/pm */ if (schedStartHour == 24) { /* toggle start from AM to PM (24 to 12) */ schedStartHour -= 12; if (schedEndHour < schedStartHour) { schedEndHour = schedStartHour; } } else if (schedStartHour < 12) { /* toggle start from AM to PM (0..11 to 12..23) */ schedStartHour += 12; if (schedEndHour < schedStartHour) { schedEndHour = schedStartHour; } } else { /* toggle from PM to AM (12..23 to 0..11) */ schedStartHour -= 12; } break; case 4: /* end hour (0..24) */ if (increase) { if (schedEndHour < 24) { schedEndHour++; } } else { if (schedEndHour > 0) { schedEndHour--; } } if (schedEndHour < schedStartHour) { schedStartHour = schedEndHour; } break; case 5: /* end am/pm */ if (schedEndHour == 24) { /* toggle start from AM to PM (24 to 12) */ schedEndHour -= 12; if (schedStartHour > schedEndHour) { schedStartHour = schedEndHour; } } else if (schedEndHour < 12) { /* toggle start from AM to PM (0..11 to 12..23) */ schedEndHour += 12; if (schedEndHour < schedStartHour) { schedEndHour = schedStartHour; } } else { /* toggle from PM to AM (12..23 to 0..11) */ schedEndHour -= 12; if (schedEndHour < schedStartHour) { schedStartHour = schedEndHour; } } break; default: break; } } if (redraw) { char startHourStr[4]; char startAmPmStr[4]; char endHourStr[4]; char endAmPmStr[4]; GetScheduleStrings(schedStartHour, startHourStr, startAmPmStr); GetScheduleStrings(schedEndHour, endHourStr, endAmPmStr); TextFont(1); for (index = 1; index < 6; index++) { EraseRect(&parts[index]); MoveTo(parts[index].left,tmp.bottom-5); switch (index) { case 1: DrawString(startHourStr); break; case 2: DrawString(startAmPmStr); break; case 3: DrawString("\p-"); break; case 4: DrawString(endHourStr); break; case 5: DrawString(endAmPmStr); break; default: break; } if (index == curSelectedIndex) { InvertRect(&parts[index]); /* re-select current selection */ } } TextFont(0); } } void ShowButtonPress(DialogPtr theDialog, short theItem) { long finalCount; short tmpInt; Handle tmpHandle; Rect tmpRect; GetDItem(theDialog, theItem, &tmpInt, &tmpHandle, &tmpRect); HiliteControl((ControlHandle)tmpHandle, true); /* "push" */ Delay(10L, &finalCount); HiliteControl((ControlHandle)tmpHandle, false); /* "release" */ } pascal Boolean MyDialogFilter(DialogPtr theDialog, EventRecord *theEvent, short *itemHit) { char tmpChar; short tmpInt; Handle tmpHandle; Rect tmpRect, schedRect, picRect; Point tmpPt = { 0,0 }; switch(theEvent->what) { case keyDown: case autoKey: /* convert the key to char */ tmpChar = (unsigned char)(theEvent->message & charCodeMask); if (tmpChar == '\r' || tmpChar == '\n' || tmpChar == '\3') { /* return or enter pressed */ *itemHit = okItem; ShowButtonPress(theDialog, okItem); return true; } else if (((theEvent->modifiers & cmdKey) && (tmpChar == '.')) || (tmpChar == 0x1B)) { /* command-period or escape pressed */ *itemHit = cancelItem; ShowButtonPress(theDialog, cancelItem); return true; } return false; case mouseDown: GetDItem(theDialog, scheduleUserItem, &tmpInt, &tmpHandle, &tmpRect); tmpPt = theEvent->where; GlobalToLocal(&tmpPt); if (PtInRect(tmpPt, &tmpRect)) { HandleClickInScheduler(tmpPt, &tmpRect, false); } return false; case updateEvt: /* is the update event for this window? */ if (theEvent->message == (long)theDialog) { /* draw default button indicator */ GetDItem(theDialog, okItem, &tmpInt, &tmpHandle, &tmpRect); PenSize(kButtonFrameSize,kButtonFrameSize); InsetRect(&tmpRect,kButtonFrameInset,kButtonFrameInset); FrameRoundRect(&tmpRect,kButtonFrameRadius,kButtonFrameRadius); PenNormal(); /* draw group boxes around user items */ GetDItem(theDialog, printerTypeUserItem, &tmpInt, &tmpHandle, &tmpRect); PenSize(1,1); PenPat(&qd.gray); FrameRect(&tmpRect); PenNormal(); GetDItem(theDialog, localPortUserItem, &tmpInt, &tmpHandle, &tmpRect); PenSize(1,1); PenPat(&qd.gray); FrameRect(&tmpRect); PenNormal(); /* draw arrow control */ GetDItem(theDialog, scheduleUserItem, &tmpInt, &tmpHandle, &tmpRect); HLock((Handle)arrowsPictHdl); picRect = (**arrowsPictHdl).picFrame; HUnlock((Handle)arrowsPictHdl); schedRect = tmpRect; tmpRect.left = schedRect.right - (picRect.right-picRect.left); tmpRect.bottom = schedRect.top + (picRect.bottom-picRect.top); DrawPicture(arrowsPictHdl,&tmpRect); HandleClickInScheduler(tmpPt,&schedRect,true); /* draw current values */ } return false; default: return false; } return false; } OSErr ConfigureSpooler(Boolean willStartServer) { char portStr[256]; char typeStr[256]; char nameStr[256]; GrafPtr savePort; DialogTHndl dtHdl; DialogPtr aDialog; short item; Handle H; Rect box, printerBox, portBox; DialogRecord dlgRec; /* preload stuff */ LoadPreferences(); LoadDateFormatStrings(); arrowsPictHdl = (PicHandle)GetResource('PICT',upDownArrowsPictID); GetPort(&savePort); /* get handle to dialog template and center it before displaying */ dtHdl = CenterDialog(spoolerDialogID); aDialog = GetNewDialog(spoolerDialogID, (Ptr)&dlgRec, (WindowPtr) -1L); if (aDialog) { /* set up default and alternate button titles */ GetIndString(nameStr,buttonStringsID,(willStartServer) ? startStrIndex:saveStrIndex); GetDItem(aDialog, okItem, &item, &H, &box); SetCTitle((ControlHandle)H, nameStr); /* should we disable button if server is already running? */ /* (no, just stop and restart the server to pick up new settings) */ /*HiliteControl((ControlHandle)H,(serverRef!=0)?255:0);*/ GetIndString(nameStr,buttonStringsID,cancelStrIndex); GetDItem(aDialog, cancelItem, &item, &H, &box); SetCTitle((ControlHandle)H, nameStr); /* set up default name, printer type, and printer options */ GetIndString(typeStr,typeStringsID,defaultPrinterItem-(iconLWItem-1)); GetIndString(nameStr,nameStringsID,defaultPrinterItem-(iconLWItem-1)); GetDItem(aDialog, spoolerNameItem, &item, &H, &box); SetIText(H, (ourName[0] > 0) ? ourName : nameStr); GetDItem(aDialog, defaultPrinterItem, &item, &H, &box); printerBox = box; GetDItem(aDialog, colorRibbonCBoxItem, &item, &H, &box); SetCtlValue((ControlHandle)H,defaultColorRibbon); HiliteControl((ControlHandle)H,(defaultPrinterItem==iconLWItem)?255:0); GetDItem(aDialog, sheetFeederCBoxItem, &item, &H, &box); SetCtlValue((ControlHandle)H,defaultSheetFeeder); HiliteControl((ControlHandle)H,(defaultPrinterItem==iconLWItem)?255:0); GetDItem(aDialog, iw1ModeCBoxItem, &item, &H, &box); SetCtlValue((ControlHandle)H,defaultIW1Mode); HiliteControl((ControlHandle)H,(defaultPrinterItem==iconLWItem)?255:0); GetDItem(aDialog, noReverseCBoxItem, &item, &H, &box); SetCtlValue((ControlHandle)H,defaultNoReverse); HiliteControl((ControlHandle)H,(defaultPrinterItem==iconLWItem)?255:0); GetDItem(aDialog, printingEnabledItem, &item, &H, &box); SetCtlValue((ControlHandle)H,defaultPrintEnabled); GetDItem(aDialog, autoRunCBoxItem, &item, &H, &box); SetCtlValue((ControlHandle)H,defaultAutoRun); /* set up default port */ GetIndString(portStr,portStringsID,defaultPortItem-(modemPortItem-1)); GetDItem(aDialog, defaultPortItem, &item, &H, &box); portBox = box; SetPort(aDialog); ShowWindow(aDialog); InitCursor(); /* draw frames for initial selections */ PenSize(kIconFrameSize,kIconFrameSize); InsetRect(&printerBox,kIconFrameInset,kIconFrameInset); FrameRect(&printerBox); InsetRect(&portBox,kIconFrameInset,kIconFrameInset); FrameRect(&portBox); PenNormal(); item = 0; while ((item != okItem) && (item != cancelItem)) { if (item >= iconLWItem && item <= iconIWItem) { short tmpItem, index; for (index = iconLWItem; index <= iconIWItem; index++) { GetDItem(aDialog, index, &tmpItem, &H, &box); InsetRect(&box,kIconFrameInset,kIconFrameInset); PenSize(kIconFrameSize,kIconFrameSize); PenPat(&qd.white); FrameRect(&box); PenNormal(); } GetDItem(aDialog, item, &tmpItem, &H, &box); InsetRect(&box,kIconFrameInset,kIconFrameInset); PenSize(kIconFrameSize,kIconFrameSize); PenPat(&qd.black); FrameRect(&box); PenNormal(); GetIndString(typeStr, 128, item-(iconLWItem-1)); GetIndString(nameStr, 129, item-(iconLWItem-1)); GetDItem(aDialog, spoolerNameItem, &tmpItem, &H, &box); SetIText(H, nameStr); GetDItem(aDialog, colorRibbonCBoxItem, &tmpItem, &H, &box); HiliteControl((ControlHandle)H,(item==iconLWItem)?255:0); GetDItem(aDialog, sheetFeederCBoxItem, &tmpItem, &H, &box); HiliteControl((ControlHandle)H,(item==iconLWItem)?255:0); GetDItem(aDialog, iw1ModeCBoxItem, &tmpItem, &H, &box); HiliteControl((ControlHandle)H,(item!=iconIWItem)?255:0); if (item!=iconIWItem) { SetCtlValue((ControlHandle)H,0); } GetDItem(aDialog, noReverseCBoxItem, &tmpItem, &H, &box); HiliteControl((ControlHandle)H,(item==iconLWItem)?255:0); if (item==iconLWItem) { SetCtlValue((ControlHandle)H,0); } } else if (item >= modemPortItem && item <= printerPortItem) { short tmpItem, index; for (index = modemPortItem; index <= printerPortItem; index++) { GetDItem(aDialog, index, &tmpItem, &H, &box); InsetRect(&box,kIconFrameInset,kIconFrameInset); PenSize(kIconFrameSize,kIconFrameSize); PenPat(&qd.white); FrameRect(&box); PenNormal(); } GetDItem(aDialog, item, &tmpItem, &H, &box); InsetRect(&box,kIconFrameInset,kIconFrameInset); PenSize(kIconFrameSize,kIconFrameSize); PenPat(&qd.black); FrameRect(&box); PenNormal(); GetIndString(portStr, 130, item-(modemPortItem-1)); } else if ((item >= colorRibbonCBoxItem && item <= printingEnabledItem) || (item == autoRunCBoxItem)) { short tmpItem, curValue; GetDItem(aDialog, item, &tmpItem, &H, &box); curValue = GetCtlValue((ControlHandle)H); SetCtlValue((ControlHandle)H,!curValue); } ModalDialog((ModalFilterProcPtr)MyDialogFilter, &item); } if (item == okItem) { short tmpItem; Boolean hasColorRibbon, hasSheetFeeder; Boolean iw1Mode, noReverse, printEnabled, autoRun; GetDItem(aDialog, spoolerNameItem, &tmpItem, &H, &box); GetIText(H, nameStr); GetDItem(aDialog, colorRibbonCBoxItem, &tmpItem, &H, &box); hasColorRibbon = (GetCtlValue((ControlHandle)H) != 0); GetDItem(aDialog, sheetFeederCBoxItem, &tmpItem, &H, &box); hasSheetFeeder = (GetCtlValue((ControlHandle)H) != 0); GetDItem(aDialog, iw1ModeCBoxItem, &tmpItem, &H, &box); iw1Mode = (GetCtlValue((ControlHandle)H) != 0); GetDItem(aDialog, noReverseCBoxItem, &tmpItem, &H, &box); noReverse = (GetCtlValue((ControlHandle)H) != 0); GetDItem(aDialog, printingEnabledItem, &tmpItem, &H, &box); printEnabled = (GetCtlValue((ControlHandle)H) != 0); GetDItem(aDialog, autoRunCBoxItem, &tmpItem, &H, &box); autoRun = (GetCtlValue((ControlHandle)H) != 0); SetPrinterOptions(hasColorRibbon, hasSheetFeeder,iw1Mode,noReverse); defaultColorRibbon = hasColorRibbon; defaultSheetFeeder = hasSheetFeeder; defaultPrintEnabled = printEnabled; defaultIW1Mode = iw1Mode; defaultNoReverse = noReverse; defaultAutoRun = autoRun; } curSelectedIndex = 0; ReleaseResource((Handle)arrowsPictHdl); arrowsPictHdl = 0L; CloseDialog(aDialog); } SetPort(savePort); UpdateLogWindow(); if (item == okItem) { /* save our name, type, and local printer port */ BlockMove(&nameStr[1],&ourName[1],nameStr[0]); ourName[0] = nameStr[0]; BlockMove(&typeStr[1],&ourType[1],typeStr[0]); ourType[0] = typeStr[0]; BlockMove(&portStr[1],&ourPort[1],portStr[0]); ourPort[0] = portStr[0]; /* prefix is based on selected printer type */ ourPrefix[0] = 3; ourPrefix[1] = ourType[1]; /* 'L' or 'I' */ ourPrefix[2] = (ourType[2] == 'Q') ? 'Q' : 'W'; ourPrefix[3] = '-'; /* save for next time (after spool folder is set up) */ SavePreferences(); } return (item == okItem) ? noErr : userCanceledErr; } OSErr StartSpooler(void) { OSErr err = noErr; if (!defaultAutoRun) { err = ConfigureSpooler(true); } if (err == noErr) { char nameStr[256]; EntityName name; PAPStatusRec stRec; name.zoneStr[0] = 1; name.zoneStr[1] = '*'; BlockMove(ourType, name.typeStr, 33L); BlockMove(ourName, name.objStr, 33L); BlockMove("\pRegistering", stRec.statusStr, 12L); err = SLInit(&name, kPAPMaxQuantum, &stRec, &serverRef); if (err != noErr) { MessageWithIntValue(LOG_ERR_X,"\pRegistering name:",err); (void) SLClose(serverRef); serverRef = 0; } else { BlockMove("Registered Ò", &nameStr[1],12L); nameStr[0] = 12; BlockMove(&ourName[1],&nameStr[nameStr[0]+1],(long)((short)ourName[0])); nameStr[0] += ourName[0]; BlockMove("Ó in local zone", &nameStr[nameStr[0]+1],15L); nameStr[0] += 15; Message(LOG_ERR,nameStr); } } return err; } OSErr StopSpooler(void) { OSErr err = SLClose(serverRef); serverRef = 0; return err; } void DoAbout() { Message(0,sepStr); Message(0,"\pSimple Spooler is a basic print spooler application."); Message(0,"\pIt presents itself as an AppleTalk printer and will"); Message(0,"\prespond to Printer Access Protocol (PAP) messages."); Message(0,"\pVersion 0.3, Copyright © 2026 Ken McLeod."); Message(0,sepStr); } void DoStatus() { Message(0,sepStr); MessageWithIntValue(0,"\pSpooler active:",serverRef); MessageWithQuotedString(0,"\pSpooler name:",ourName); MessageWithQuotedString(0,"\pPrinter type:",ourType); MessageWithIntValue(0,"\pColor ribbon:",(defaultColorRibbon)?1:0); MessageWithIntValue(0,"\pSheet feeder:",(defaultSheetFeeder)?1:0); MessageWithIntValue(0,"\pImageWriter 1 mode:",(defaultIW1Mode)?1:0); MessageWithIntValue(0,"\pSkip reverse feed:",(defaultNoReverse)?1:0); MessageWithQuotedString(0,"\pPrint to port:",ourPort); MessageWithIntValue(0,"\pPrinting enabled:",(defaultPrintEnabled)?1:0); MessageWithIntValue(0,"\pScheduled start hour:",schedStartHour); MessageWithIntValue(0,"\pScheduled end hour:",schedEndHour); MessageWithIntValue(0,"\pJobs spooled:",JobsSpooled()); MessageWithIntValue(0,"\pJobs printed:",JobsPrinted()); Message(0,sepStr); } void LoadPreferences() { char *p; Ptr prefData = 0L; long count = 4L+2L+8+18+256+34+34; OSErr err = CopyDataFromPrefsFile(&count,&prefData); if (err != noErr || !prefData) { return; } if (*((long*)&prefData[0]) != 'pref' || *((short*)&prefData[4]) != kPrefsVersion) { DisposPtr(prefData); return; } defaultPrinterItem = *((char*)&prefData[6]); defaultPortItem = *((char*)&prefData[7]); defaultColorRibbon = *((char*)&prefData[8]); defaultSheetFeeder = *((char*)&prefData[9]); defaultPrintEnabled = *((char*)&prefData[10]); defaultIW1Mode = *((char*)&prefData[11]); schedStartHour = *((char*)&prefData[12]); schedEndHour = *((char*)&prefData[13]); /* window rect in global coordinates */ windowRect = *((Rect*)&prefData[14]); defaultNoReverse = *((char*)&prefData[22]); defaultAutoRun = *((char*)&prefData[23]); /* reserved chars at prefsData[24..31] */ p = (char*)&prefData[32]; BlockMove(p,ourName,p[0]+1); p += p[0]+1; BlockMove(p,ourType,p[0]+1); p += p[0]+1; BlockMove(p,ourPort,p[0]+1); DisposPtr(prefData); /* let PAP module know about the new options */ SetPrinterOptions(defaultColorRibbon,defaultSheetFeeder,defaultIW1Mode,defaultNoReverse); } void SavePreferences() { char *p, val; Boolean hasColorRibbon, hasSheetFeeder; Boolean iw1Mode, noReverse, printEnabled, autoRun; long length = 4L+2L+8+18+ourName[0]+1+ourType[0]+1+ourPort[0]+1; Ptr prefData = NewPtrClear(length); if (!prefData) { return; } /* offset 0: magic 'pref' tag (4 bytes) */ *((long*)&prefData[0]) = 'pref'; /* offset 4: prefs version (2 bytes) */ *((short*)&prefData[4]) = kPrefsVersion; /* offset 6: selected printer item */ val = iconIWItem; if (ourType[1] == 'L') { val = (ourType[2] == 'Q') ? iconLQItem : iconLWItem; } *((char*)&prefData[6]) = val; /* offset 7: selected printer port item */ val = (ourPort[1] == 'M') ? modemPortItem : printerPortItem; *((char*)&prefData[7]) = val; /* offset 8, 9: boolean values of Color Ribbon and Sheet Feeder */ GetPrinterOptions(&hasColorRibbon,&hasSheetFeeder,&iw1Mode,&noReverse); *((char*)&prefData[8]) = (hasColorRibbon) ? -1 : 0; *((char*)&prefData[9]) = (hasSheetFeeder) ? -1 : 0; /* offset 10, 11: boolean values of Print Enabled and IW1 Mode */ printEnabled = defaultPrintEnabled; *((char*)&prefData[10]) = (printEnabled) ? -1 : 0; iw1Mode = defaultIW1Mode; *((char*)&prefData[11]) = (iw1Mode) ? -1 : 0; /* offset 12: scheduled start hour for printing (1 byte) */ *((char*)&prefData[12]) = (char)(schedStartHour & 0xFF); /* offset 13: scheduled end hour for printing (1 byte) */ *((char*)&prefData[13]) = (char)(schedEndHour & 0xFF); /* offset 14-15:top, 16-17:left, 18-19:bottom, 20-21:right (window rect) */ *((Rect*)&prefData[14]) = ((GrafPtr)textWindow)->portRect; LocalToGlobal((Point*)&prefData[14]); LocalToGlobal((Point*)&prefData[18]); /* offset 22, 23: boolean values of No Reverse and Auto Run */ *((char*)&prefData[22]) = (noReverse) ? -1 : 0; autoRun = defaultAutoRun; *((char*)&prefData[23]) = (autoRun) ? -1 : 0; /* reserved chars from [24..31] */ p = (char*)&prefData[32]; BlockMove(ourName,p,ourName[0]+1); p += ourName[0]+1; BlockMove(ourType,p,ourType[0]+1); p += ourType[0]+1; BlockMove(ourPort,p,ourPort[0]+1); (void)SaveDataToPrefsFile(length,prefData); DisposPtr(prefData); }