[comp.windows.news] blankscreen.ps: NeWS screensaver and lock program.

sjs@spectral.ctt.bellcore.com (Stan Switzer) (03/28/89)

"Blankscreen" is a substitute for the SunView "screenblank" and
"lockscreen" programs.

Rather than repeat a description here, I refer interested parties to
the documentation at the beginning of the program.  Be sure to read
up to and including the "Warnings" section.

Now, if only I had a NeWS "mailtool" clone I'd be one happy camper.

Enjoy!

Stan Switzer  sjs@ctt.bellcore.com
------------
%
% BlankScreen: Lights out!
%
% Copyright (C) 1989 by Stan Switzer. All rights reserved.
% This program is provided for unrestricted use, provided that this 
% copyright message is preserved. There is no warranty, and no author 
% or distributer accepts responsibility for any damage caused by this 
% program.
%
% DESCRIPTION:
%
% BlankScreen quietly monitors your keyboard and mouse for activity,
% invoking a screen-saver when there has been no activity in a user-
% selectable time period.  If, after another user-selectable time period,
% there has been no keyboard or mouse activity, BlankScreen quietly enters
% a "lock" mode which demands your login password before releasing your
% workstation.
%
% At invocation, the variables /BlankTime and /LockTime control the amount
% of idle time that causes blanking and amount of blanked time that leads
% to locking.  Changing these values later has no effect (to protect against
% accidents).  The default times for these values are three minutes
% and two minutes respectively.  If you find locking objectionable, you
% can disable it by setting /LockTime to 0.
%
% When blankscreen is restarted, it kills any other running (but
% unlocked) blankscreen processes.  This allows you to change the
% timer parameters without compromising the security of the
% locking function.
%
% If /BlankFKey is defined, it is the name of a function key to use to
% blank the screen on demand.  Similarly for /LockFKey.  I use the following:
%	/BlankFKey /FunctionF6 def  /LockFKey /FunctionF7 def
%
% The poem can be changed by assigning a different array of strings
% to /Poetry before invoking blankscreen.  A value of "null" disables
% the poetry entirely.
%
% The colors used in the blanking display can be changed by assigning values to
% the appropriate variables before invocation.  Interested parties should
% look at the first few lines of source.
%
% WARNINGS:
%
% This program checks passwords by trying to connect to your FTP
% server (yes, this is a hack).  You would do well to see if you can
% "ftp localhost" before you begin using this program.
%
% Additionally, either (USER) or (LOGNAME) must be in the environment
% for the locking mode to know who you are.
%
% If either of these two assumptions fails to hold, setting "LockTime"
% to 0 disables automatic locking, thus avoiding the problem.
%
% Because this program goes to great lengths to defeat the keyboard focus
% manager, it depends on things perhaps that might easily change from one
% release on NeWS to another.  I only know for sure that it works in
% NeWS 1.1 with a Sun-3 keyboard.
%
% Finally, "forkunix" must work (I don't know about non-UNIX NeWS
% implementations) and there must be a reasonably standard "date"
% command.
%
% NeWS Notes:
%
% Apparently, mouse transitions caught by a non-canvas specific and
% non-exclusive interests do not require redistribution, but key transitions
% do.  Since this seems much too arbitrary, I use an exclusive
% interest and redistribute everything.  Probably, I should be registering
% interests relative to a framebuffer overlay instead of a null canvas.
%
% Interests having dictionaries of procs for both their Name and Action
% do not execute either procedure but instead return both an event and array
% from awaitevent.  The array contains the two procedures that should have
% been executed.  Maybe I should have been using "forkeventmgr" anyway to
% avoid all of this hassle (then again, for security reasons, maybe not).
%
% There are certain interesting techniques in here that you may feel free
% to borrow.  My favorite is the "condition" handler.  A close runner-up
% is the FTP password hack (thanks Don) and the date query.  Please
% do not use the keyboard handling as a model for input handling since
% this code intentionally subverts the very mechanisms you should
% generally be using.
%
% ------------
% Stan Switzer   sjs@ctt.bellcore.com
%
% 3/27/89 - SJS
%	Release 1.0

% Start a new dictionary here to prevent patching the locking code, prevent
% unauthorized access to the /Password entry, and to avoid systemdict
% namespace polution.

100 dict begin { end } pop    % { end } pop is to fool formatters!

/default { % key val -> -   def into current dictionary, val is default
    1 index where { 2 index get exch pop } if def
} def

/BlankTime 180 default
/LockTime 120 default

/Testing? false def
% /Testing? true def

/conprint { pop pop } def
/Conprint { sprintf console exch writestring console flushfile } def
% /conprint /Conprint load def

/SC { % key grayshade pastelhue-or-color -> -   set color
    dup type /colortype ne { .35 1 hsbcolor } if % pastel hue
    ColorDisplay? not 3 1 roll ifelse
    dup type /colortype ne { dup dup rgbcolor } if % gray value
    default
} def
/BlankColor 0 0 0 0 rgbcolor SC
/HourColor 1 .5 SC
/MinColor 1 .5  SC
/ClockColor 1 .5 SC
/TextColor 1 .5  SC
/QuoteColor 1 .5 SC

% Generally, I prefer that users wishing to change the displayed verses do
% so by defining the new verses before "running" this file.  This way, we
% can avoid unnecesary speciation of this program.

/Poetry [
    (`The time has come', the Walrus said,)
    (       `To talk of many things:)
%   (Of shoes\320and ships\320and sealing wax\320)
    (Of shoes\0\320and ships\320and sealing wax\320) % "\0"? see below!
    (       Of cabbages\320and kings\320)
    (And why the sea is boiling hot\320)
    (       And whether pigs have wings.')
    (                 \320 Lewis Carroll)
] default
% It is a complete mystery to me why, but sometimes (not always)
% the dash after "shoes" does not display!  Somehow, the \0 seems to avoid
% the problem, but I'm stumped why it would.  It appears to be a scanner
% bug because "Poetry 2 get ==" shows a \0 instead of a \320.  All attempts
% to isolate the problem in a simpler program fail to manifest the bug.
% Now here's the wierd thing:  If I comment out the line with the "\0",
% uncomment the one without it, and reverse them (putting the now
% commented-out line first), the dash shows!  Then, if I delete the
% comment, the dash doesn't show!  In fact, virtually any change
% immediately before the line in question makes the bug go away.
% Update: prior to posting, I modified the prefatory comments, and now
% the bug does not appear.  Present theory is that scanner breaks
% when \nnn crosses a psio buffer boundary.

% We are going to be paranoid and hoist certain non-operator procs to
% this private dictionary to prevent insertion of trojan horses in the
% password collection and checking logic.  I know of a few holes here, but
% I don't see the point of documenting them, if you get my drift.  Still,
% considering how "append," for instance, is used in here, it is only
% prudent to protect ourselves.

/CopyProc { % proc -> proc'
    dup type /arraytype eq { % array?
	dup xcheck exch 0 exch { % forall -- exec? n item
	    CopyProc exch 1 add
	} forall
	array astore exch { cvx } if
    } if
} def

[ /append ] {  % hoist these global procs ...
    dup load CopyProc def
} forall

/GetDate { % - -> (str) true -or- false
    { 25 dict begin % fork (to keep events from being confused)
	/Interest createevent dup begin
	    /Name [ /TimeOut /Date ] def
	end dup expressinterest def
	/Timer Interest createevent copy dup begin
	    /Name /TimeOut def
	    /TimeStamp currenttime .25 add def
	end dup sendevent def
	(echo "createevent dup begin)
	(         /Name /Date def /Action (`date`)) append
	(      def end sendevent" | psh) append forkunix
	awaitevent dup /Name get /TimeOut eq {
	    pop [ false ]
	} {
	    Timer recallevent
	    /Action get [ exch true ]
	} ifelse
    end } fork waitprocess aload pop
} def
% GetDate stack

/GetHHMMSS { % - -> hh mm ss true -or- false
    GetDate {
	3 { ( ) search pop pop pop } repeat
	( ) search 4 2 roll pop pop pop
	2 { (:) search pop exch pop cvi exch } repeat cvi
	true
    } {
	false
    } ifelse
} def
% GetHHMMSS

/Midnight {
    Midnightproc waitprocess pop
    /Midnightproc null store
    Midnight
} def
/Midnightproc { 
    /Midnight currenttime GetHHMMSS
    % try again if it didn't work (maybe server wasn't started yet)
    true { GetHHMMSS } ifelse
    { % worked?
	3 -1 roll 60 mul 3 -1 roll add exch 60 div add sub store true
    } { % not worked?
	pop pop false
    } ifelse
} fork def

/NeWSLogoThing Object dictbegin
dictend classbegin
    /Font /Times-Roman findfont 92 scalefont def
    /String (NeWS) def
    /Verses Poetry def
    /NVerses Verses null eq { 0 } { Verses length } ifelse def
    /Poetry? NVerses 0 gt def
    /ClockRad Poetry? 46 36 ifelse def
    /MinHand ClockRad 7 sub def
    /HourHand ClockRad 20 sub def
    /Vfont /Times-Roman findfont 14 scalefont def
    /LogoUp Poetry? 19 0 ifelse def
    /VerseNo 0 def
    /Verse { Verses VerseNo dup 0 lt 1 index NVerses ge or {
	pop /VerseNo 0 store 0 } if get } def
    /InitVerse { /VerseNo -1 store } def
    /NextVerse { /VerseNo VerseNo 1 add store } def
    /PaintHand { % deg r -> -
	exch gsave rotate
	-4 -4 moveto 4 -4 lineto 0 exch lineto closepath fill
	grestore
    } def
    /PaintClock { % x y -> -
	gsave translate
	ClockColor setcolor
	gsave 12 { 30 rotate 
	    -2 ClockRad moveto 4 0 rlineto -2 -6 rlineto closepath fill
	} repeat grestore
	currenttime Midnight sub cvi dup 3600 mod 60 div
	HourColor setcolor
	-30 mul HourHand PaintHand
	MinColor setcolor
	60 mod -6 mul MinHand PaintHand
	grestore
    } def
    /GSave { gsave Font setfont TextColor setcolor } def
    /size { GSave String stringwidth pop ClockRad 2 mul add
	Font fontascent LogoUp add grestore } def
    /paint { GSave
	currentpoint ClockRad add exch ClockRad add exch PaintClock
	ClockRad 2 mul 0 rmoveto
	gsave 0 LogoUp rmoveto String show grestore
	Poetry? { Vfont setfont QuoteColor setcolor Verse show } if
	grestore
    } def
classend def

/TheThing /new NeWSLogoThing send def
/ThingX null def /ThingY null def

% Thing stuff from the menu package ...
/ShowThingDict dictbegin
    LiteMenu /&ShowThingDict get /def load forall
dictend def
/ThingSizeDict dictbegin
    LiteMenu /&ThingSizeDict get /def load forall
dictend def
/IconString { icondict exch get cvis } def
/ThingSize {
    ThingSizeDict begin gsave dup type exec end grestore
    .5 add cvi exch .5 add cvi exch
} def
/ShowThing {
    gsave currentpoint translate
    ShowThingDict begin dup type exec end
    grestore
} def

% GetUserid: ->
%		false   	 -- didn't work
%		(userid) true	 -- got user id
/GetUserid {
    (USER) { getenv } stopped {
	pop (LOGNAME) { getenv } stopped {
	    pop false
	} true ifelse
    } true ifelse
} def

% Check password using ftp server (Don Hopkins thought of using FTP).
%
% CheckPW: (userid) (password) ->
%		false   	 -- check didn't work
%		ok? true	 -- OK? is true iff PW is correct
%
% NOTE: we wish to distinguish failure of the checking procedure from
%	failure of the check itself.
/CheckPW {
    { 25 dict begin
	/Password exch def
	/User exch def
	/S (%socketc21) (rw) file def	     % socket
	/R { S exch readline } def	     % str -> str -- read from socket
	/W { S exch writestring S flushfile } def % str -> - -- write socket
	/SR { % (good) (bad) timeoutsec -> good? true -or- false -- srch
	    /Time exch 60 div def
	    /Interest createevent dup begin
		/Name /DoneEvent def
	    end dup expressinterest def
	    /Bad exch def
	    /Good exch def
	    /Match1 { anchorsearch { pop pop true } { pop false } ifelse } def
	    /Match {
		dup type /arraytype eq {
		    { Match1 { true exit } if } forall
		} {
		    Match1
		} ifelse
	    } def
	    /Str 200 string def
	    /Proc { % fork
		{ %  loop
		    Str R not { /Ugly exit } if
		    dup Good Match { /Good exit } if
		    dup Bad Match { /Bad exit } if
		    pop
		} loop exch pop
		Interest createevent copy dup /Action 4 -1 roll put sendevent
	    } fork def
	    /Timer Interest createevent copy dup begin
		/Action /TimeOut def
		/TimeStamp currenttime Time add def
	    end dup sendevent def
	    awaitevent /Action get
	    dup /TimeOut eq {
		Proc killprocess
		pop /Ugly
	    } if
	    dup /Ugly eq {
		Timer recallevent
		pop false
	    } {
		/Good eq true
	    } ifelse
	    Interest revokeinterest 
	} def
	
	{ % only once through:
	    (220 ) (xxx ) 15 SR
	    not { false exit } if not { false exit } if
	    (user ) User append (\n) append W
	    (331 ) [ (530 ) (500 ) ] 15 SR
	    not { false exit } if not { false exit } if
	    (pass ) Password append (\n) append W
	    (230 ) [ (530 ) (500 ) ] 20 SR not { false exit } if
	    true exit
	} loop
	dup 2 1 ifelse array astore
	(quit\n) W
	S closefile
    end } fork exch pop exch pop waitprocess aload pop
} def

/UserID GetUserid not (Unknown User) if def

Testing? {
    /win framebuffer /new DefaultWindow send def
    /reshapefromuser win send
    /map win send
    /CanvasToBlank /ClientCanvas win send def
} {
    /CanvasToBlank framebuffer def
} ifelse

/CoverCanvas { % canvas => canvas'
    gsave dup setcanvas
    newcanvas
    dup begin
	/Transparent false def
	/Retained false def
	/Mapped false def
    end
    clippath dup reshapecanvas
    /nouse /nouse_m 2 index setstandardcursor
    grestore
} def

/Blanket CanvasToBlank CoverCanvas def

/QueryW 350 def /QueryH 85 def /QueryR 10 def
/QueryMessage () def
/QueryFont /Times-Roman findfont 18 scalefont def
/QueryCanvas gsave Blanket dup setcanvas
newcanvas dup begin
    /Transparent false def
    /Retained false def
    /Mapped false def
end
clippath pathbbox 4 2 roll pop pop
exch QueryW sub 2 div exch QueryH sub 2 idiv translate
QueryR 0 0 QueryW QueryH rrectpath dup reshapecanvas
grestore def

/PWstring 100 string def /PWpos 0 def
/Password () def
/AddPW {
    PWpos PWstring length lt {
	PWstring PWpos 3 -1 roll put
	/PWpos PWpos 1 add store
	/Password PWstring 0 PWpos getinterval store
    } { pop } ifelse % Thanks Don
} def
/DelPW {
    /PWpos PWpos 1 sub 0 max store
    /Password PWstring 0 PWpos getinterval store
} def
/ClearPW {
    /PWpos 0 store
    /Password PWstring 0 PWpos getinterval store
    0 1 Password length 1 sub { Password exch 0 put } for % zero PW string
} def
/State null def

/seconds { 60 div } def

/Member? { % item array -> bool
    false 3 1 roll { % forall
	1 index eq { exch pop true exch } if
    } forall
    pop
} def

/Secure? { State { /Lock /Query /Check } Member? } def

% Timer while in state:
/StateTimes 10 dict dup begin
    /Sleep 15 BlankTime 2 div min seconds def
    /Monitor BlankTime seconds def
    /Blank 3 seconds def
    /Lock Blank def
    /Query Blank def
    /Check Blank def
end def

/Conditions [
    dictbegin  % Keyboard and mouse monitoring:
	/Condition  { State { /Monitor /Blank /Lock } Member? } def
	/Enter	{ MonitorInterests /expressinterest load forall } def
	/Leave	{ MonitorInterests /revokeinterest load forall } def
    dictend
    dictbegin  % Screen is blanked:
	/Condition	{ State { /Blank /Lock /Query /Check } Member? } def
	/Enter	{
	    /InitVerse TheThing send MoveThing
	    Blanket canvastotop
	    Blanket /Mapped true put
	    /BlankCount 0 store
	} def
	/Leave  	{ Blanket /Mapped false put } def
	/Remain	{ Blanket canvastotop MoveThing } def
    dictend
    dictbegin  % Querying:
	/Condition	{ State /Query eq } def
	/Enter	{
	    ClearPW
	    /QueryMessage  (Enter Password.) store
	    QueryInterests /expressinterest load forall
	    /QueryCount 0 store
	} def
	/Leave  	{ QueryInterests /revokeinterest load forall } def
    dictend
    dictbegin  % Checking:
	/Condition	{ State /Check eq } def
	/Enter	{
	    /QueryMessage  (Checking Password.) store
	    RepaintQuery
	    CheckInterest expressinterest
	} def
	/Leave  	{ CheckInterest revokeinterest } store
    dictend
    dictbegin  % Querying or checking:
	/Condition	{ State { /Query /Check } Member? } def
	/Enter	{
	    QueryCanvas /Mapped true put
	} def
	/Leave  	 {
	    QueryCanvas /Mapped false put
	    ClearPW
	} def
    dictend
] def

% General-purpose condition handler: monitors changes to boolean conditions
% and invokes associated handling routines.

/HandleConditions {
    0 begin
	Conditions { % forall
	    begin
		/Value Condition def % eveluate condition
		Prev Value {
		    { True Same Remain Was }	% Is and was
		    { True Different Enter WasNot } % Is and was not
		} {
		    { False Different Leave Was }   % Is not and was
		    { False Same Desists WasNot }   % Is not and was not
		} ifelse ifelse
		/Prev Value def
	    end
	} forall
    end
} dup 0 dictbegin
    /Prev false def 	     % initial value of the condition
    /Condition {false} def   % Condition check proc
    { /True /False /Enter /Leave /Same
	/Different /Remain /Desists /Was /WasNot }
    { nullproc def } forall
dictend put def

% time to lock:
/LockSteps LockTime seconds StateTimes /Lock get idiv def
/BlankCount 0 def

% time to query:
/QueryTime 30 seconds def
/QuerySteps QueryTime StateTimes /Query get idiv def
/QueryCount 0 def

% Change to a new state:
/newState { % state -> -
    (>> % -> %\n) [ State 3 index ] conprint
    /State exch store
    HandleConditions
    TimerEvent dup /IsQueued get {
	dup recallevent
    } if
    dup /TimeStamp StateTimes State get pause currenttime add put
    sendevent
} def

/MoveThing {
    gsave Blanket setcanvas
    clippath pathbbox 4 2 roll pop pop
    /size TheThing send
    3 -1 roll exch sub random mul cvi
    3 1 roll sub random mul cvi exch
    /ThingY exch store
    /ThingX exch store
    /NextVerse TheThing send
    PaintBlanket
    grestore
} def

/PaintBlanket {
    BlankColor fillcanvas
    ThingX ThingY moveto /paint TheThing send
} def

/RepaintBlanket {
    gsave Blanket setcanvas
    PaintBlanket
    grestore
} def

/RepairBlanket {
    gsave Blanket setcanvas
    damagepath clipcanvas
    PaintBlanket
    newpath clipcanvas
    grestore
} def

/PaintQuery {
    1 fillcanvas
    QueryFont setfont
    0 setshade
    12 60 moveto (Userid:  ) show UserID show
    12 40 moveto (Password:  ) show
    % Password show
    (#%*&@!%&@#X?#No!*NeWS!*%$#@$@!) % cryptic missive
    dup length 0 exch PWpos min getinterval show
    2 0 rmoveto
    5 -5 rlineto -11 0 rlineto closepath fill
    30 15 moveto QueryMessage show
} def

/RepaintQuery {
    gsave QueryCanvas setcanvas
    PaintQuery
    grestore
} def

/RepairQuery {
    gsave QueryCanvas setcanvas
    damagepath clipcanvas
    PaintQuery
    newpath clipcanvas
    grestore
} def

/ScreenBlank {
    % What to do when the timer expires:
    /TimerInterest createevent dup begin
	/Name 2 dict dup begin
	    /Timer {
		State { % case
		    /Sleep {
			/Monitor newState
		    }
		    /Monitor {
			/Blank newState
		    }
		    /Blank {
			/BlankCount BlankCount 1 add store
			BlankCount LockSteps gt LockTime 0 ne and
			/Lock /Blank ifelse newState
			(>> Count % %\n) [ BlankCount LockSteps ] conprint
		    }
		    /Lock {
			/Lock newState
		    }
		    /Query {
			/QueryCount dup load 1 add store
			QueryCount QuerySteps gt /Lock /Query ifelse newState
			(>> QCount % %\n) [ QueryCount QuerySteps ] conprint
		    }
		} case
	    } def
	end def
    end dup expressinterest def
    
    % External control:
    /ControlInterest createevent dup begin
	/Name /BlankScreen def
	/Action 5 dict dup begin
	    /Destroy {
		Secure? not { currentprocess killprocessgroup } if
	    } def
	    /BlankOrLock {
		State { % case
		    /Sleep { /Blank newState }
		    /Default { /Lock newState }
		} case
	    } def
	    /Blank {
		Secure? not { /Blank newState } if
	    } def
	    /Lock {
		/InitVerse TheThing send
		/Lock newState
	    } def
	    /Query {
		/Query newState
	    } def
	end def
    end dup expressinterest def
    
    % We clone this event from the interest to get the /Process value:
    /TimerEvent TimerInterest createevent copy dup begin
	/Name /Timer def
    end def
    
    /MonitorInterests [
	createevent dup begin
	    /Priority 20 def
	    /Exclusivity true def
	    /Action 1 dict dup begin
		/DownTransition {
		    Secure? {
			/Query newState
		    } {
			dup redistributeevent pause
			/Sleep newState
		    } ifelse
		} def
	    end def
	end
	createevent dup begin
	    /Priority 5 def
	    /Name 1 dict dup begin
		/MouseDragged {
		    Secure? {
			/Query newState
		    } {
			/Sleep newState
		    } ifelse
		} def
	    end def
	end
    ] def
    
    /DamageInterest createevent dup begin
	/Name 1 dict dup begin
	    /Damaged { RepairBlanket } def
	end def
	/Canvas Blanket def
    end dup expressinterest def

    % It is important to understand that this is THE HARD WAY to get keyboard
    % input and it is done this way solely to subvert the focus manager.
    % Normally, you would want to cooperate with the focus manager and use
    % addkbdinterests (which is MUCH simpler).
    
    /QueryInterests [
	createevent dup begin
	    /Name 1 dict dup begin
		/Damaged { RepairQuery } def
	    end def
	    /Canvas QueryCanvas def
	end
	createevent dup begin
	    /Priority 20 def
	    /Name ascii_keymap def
	    /Action 2 dict dup begin
		/DownTransition {
		    /QueryCount 0 store
		    dup /Name get
		    dup 32 ge 1 index 127 lt and {
			dup AddPW RepaintQuery
		    } if
		    dup 8 eq 1 index 127 eq or {
			DelPW RepaintQuery
		    } if
		    dup 10 eq 1 index 13 eq or {
			/Check newState
			{
			    UserID Password ClearPW CheckPW not false if
			    CheckInterest createevent copy begin
				/Name /PWresult def
				/Action exch def
				currentdict
			    end sendevent
			} fork pop
		    } if
		    pop
		} def
	    end def
	    /Exclusivity true def
	end
	createevent dup begin % Handle shifts here
	    % priority below key processing
	    /Priority 19 def
	    /Name 20 dict dup begin
		keyboard_positions { % forall
		    dup 0 get type /arraytype ne { [ exch ] } if
		    % Name [ [key class] [key class] ... ]
		    { % forall
			aload pop /shift_key ne { pop } { % Name key
			    [	/dup cvx UI_private /begin cvx 5 index
				/do_shift_key cvx /end cvx ] cvx
			    % Name key { dup D begin Name do_shift_key end }
			    def % in /Name entry of this this interest
			} ifelse
		    } forall
		    pop
		} forall
	    end def
	    /Exclusivity true def
	end
	createevent dup begin % absorb all other KB input
	    % priority below shift processing
	    /Priority 18 def
	    /Exclusivity true def
	    /Action /DownTransition def
	end
    ] def
    
    /CheckInterest createevent dup begin
	/Name 1 dict dup begin
	    /PWresult {
		dup /Action get	/Sleep /Lock ifelse newState
	    } def
	end def
	/Canvas QueryCanvas def    % avoid fraud!
    end def
    
    /Sleep newState
    
    { % Event processing loop:
	awaitevent
	dup type /arraytype eq {
	    /exec load forall
	} if
	(>> Event % % % -> %\n) exch
	[ exch begin Name Action Canvas end State ] conprint
    } loop
} def

% kill our illustrious predecessors
createevent dup begin
    /Name /BlankScreen def
    /Action /Destroy def
end sendevent

/BlankFKey where { pop
    BlankFKey {
	createevent dup begin
	    /Name /BlankScreen def
	    /Action /Blank def
	end sendevent
    } bindkey
} if

/LockFKey where { pop
    LockFKey {
	createevent dup begin
	    /Name /BlankScreen def
	    /Action /Lock def
	end sendevent
    } bindkey
} if

% let the dust settle before we enter the fray!
pause pause

BlankTime 0 ne { { newprocessgroup ScreenBlank } fork pop } if

{ begin } pop end  % do it this way to fool formatters!