InterminalImpl.mesa
Copyright © 1985 by Xerox Corporation. All rights reserved.
Stone, June 23, 1982 2:21 pm color cursor and pen
Swinehart, April 29, 1982 5:16 pm, register with multiplexor, etc.
Doug Wyatt, April 24, 1985 10:11:59 pm PST
Russ Atkinson (RRA) January 9, 1986 0:27:37 am PST
DIRECTORY
Basics USING [LongDiv, LongMult],
Booting USING [CheckpointProc, RollbackProc, RegisterProcs],
ClassIncreek USING [Action, ActionBody],
ClassInscript USING [Inscript, SetWritePage, WriteEntry],
Interminal USING [CursorArray, DownUp, HasPenType, KeyName, KeyState, MousePosition, Side],
InterminalBackdoor USING [],
Intime USING [AdjustEventTime, DeltaDeltaTime, DeltaTime, EventTime, EventTimeDifference, maxDeltaDeltaTime, maxDeltaTime, msPerDeltaTick, MsTicks, MsTicksToDeltaTime, ReadEventTime, SubtractMsTicksFromEventTime],
Loader USING [MakeProcedureResident, MakeGlobalFrameResident],
Process USING [Priority, GetPriority, SetPriority],
Terminal USING [Create, GetBWCursorPattern, GetBWCursorPosition, GetColorCursorPattern, GetKeys, GetMousePosition, Position, RegisterNotifier, SetBWCursorPattern, SetBWCursorPosition, SetColorCursorPattern, SetColorCursorPosition, SetColorCursorState, SetMousePosition, SwapAction, SwapNotifier, Virtual, WaitForBWVerticalRetrace],
TerminalFace USING [refreshRate],
UserProfile USING [CallWhenProfileChanges, Number, ProfileChangedProc];
InterminalImpl: MONITOR
IMPORTS Basics, Booting, ClassInscript, Intime, Loader, Process, Terminal, TerminalFace, UserProfile
EXPORTS Interminal, InterminalBackdoor
SHARES Interminal
= BEGIN OPEN Interminal, ClassIncreek, ClassInscript, Intime;
inscript: ClassInscript.Inscript ← NIL;
terminal: PUBLIC Terminal.Virtual ← Terminal.Create[];
Interminal.MousePosition now has a boolean on it to declare which display the point is on to keep things consistent with the hardware world, we do all our internal calculations with InternalMP, then construct a MousePosition when recording the Action
wordlength: CARDINAL = 16;
Internal: ERROR[code: INTEGER] = CODE; -- "can't happen errors"
Values at time t, maintained correct by ActionRecorder main loop
mousePosition: Terminal.Position ← terminal.GetMousePosition[];
keyState: KeyState ← [bits[ALL[up]]]; -- 𡤌urrentKeys^, see init --
eventTime: EventTime; -- ←ReadEventTime[], initialized below
Values at time t-1, also maintained by main loop
lastMousePosition: Terminal.Position;
lastKeyState: KeyState ← [bits[ALL[up]]]; -- ←keyState, to start, see init
lastEventTime: EventTime;
allUp: PUBLIC KeyState ←
[words[words: [177777B, 177777B, 177777B, 177777B, 177777B]]];
doTrack: BOOLTRUE;
hotX: INTEGER ← 0;
hotY: INTEGER ← 0;
vBWEscape: INTEGER ← 10; -- escape velocity from BW display
vColorEscape: INTEGER ← 10; -- escape velocity from color display
allZeros: CursorArray ← ALL[0];
Mouse grain threshhold stuff
grainDots: INTEGER;
defaultGrainDots: INTEGER𡤅
grainMsTicks: Intime.MsTicks;
defaultGrainTicks: Intime.MsTicks� -- 5 actions per second
grainTicks: INTEGER;
grainTicksLeft: INTEGER;
rR: INTEGER ~ TerminalFace.refreshRate*2;
grainMsPerTick: Intime.MsTicks ← (1000+rR-1)/rR; -- System pulse duration in Ms.
SetCursorOffset: PUBLIC SAFE PROC[deltaX, deltaY: INTEGER,
enableTracking: BOOLTRUE] = CHECKED {
hotX ← deltaX; hotY ← deltaY; doTrack ← enableTracking;
};
GetCursorOffset: PUBLIC SAFE PROC RETURNS[deltaX, deltaY: INTEGER,
trackingEnabled: BOOL] = CHECKED {
RETURN[deltaX: hotX, deltaY: hotY, trackingEnabled: doTrack];
};
SetCursorPosition: PUBLIC SAFE PROC[posX, posY: INTEGER,
enableTracking: BOOLTRUE] = CHECKED {
doTrack ← enableTracking;
terminal.SetBWCursorPosition[[x: posX, y: posY]];
};
GetCursorPosition: PUBLIC SAFE PROC RETURNS[deltaX, deltaY: INTEGER,
trackingEnabled: BOOL] = CHECKED {
pos: Terminal.Position = terminal.GetBWCursorPosition[];
RETURN[deltaX: pos.x, deltaY: pos.y, trackingEnabled: doTrack];
};
hasColor: BOOLFALSE;
DisplayRec: TYPE = RECORD[xMin,xMax,yMin,yMax: INTEGER, color: BOOL];
left,right,display: POINTER TO DisplayRec;
leftRep: DisplayRec;
rightRep: DisplayRec;
This procedure runs as an independent process. It constitutes the low-level action recording mechanisms for user input actions.
ActionRecorder: PROC = {
Runs as detached process, records terminal actions.
DO
WaitForTick[]; -- run once per tick
IF hasColor
THEN HardCase[]
ELSE {
mousePosition ← terminal.GetMousePosition[];
mousePosition.x ← MAX[MIN[display.xMax, mousePosition.x], display.xMin];
mousePosition.y ← MAX[MIN[display.yMax, mousePosition.y], display.yMin];
terminal.SetMousePosition[mousePosition];
IF doTrack THEN
terminal.SetBWCursorPosition[[x: mousePosition.x+hotX, y: mousePosition.y+hotY]];
};
keyState ← [bits[terminal.GetKeys[]]];
IF keyState # lastKeyState
THEN {
eventTime ← ReadEventTime[];
[] ← EnterKeyState[];
}
ELSE DoMousePosition[];
ENDLOOP;
};
HardCase: PROC = INLINE {
moves the mouse between displays
check if mouse is moving out of current display
pattern: CursorArray;
updateCursorBits: BOOLFALSE;
mouse: Terminal.Position ← terminal.GetMousePosition[];
toH,fromH: INTEGER;
hideCursor: PROC = INLINE {
IF display.color
THEN {
pattern ← terminal.GetColorCursorPattern[];
[] ← terminal.SetColorCursorPosition[[-100,-100]];
}
ELSE {
pattern ← terminal.GetBWCursorPattern[];
terminal.SetBWCursorPattern[allZeros];
terminal.SetBWCursorPosition[[-100,-100]] --hides bw cursor
};
};
setCursorBits: PROC = INLINE {
IF display.color THEN terminal.SetColorCursorPattern[pattern]
ELSE terminal.SetBWCursorPattern[pattern];
};
mouse.y ← MAX[MIN[mouse.y,display.yMax], display.yMin];
SELECT TRUE FROM
mouse.x>display.xMax => { -- the mouse is moving right
IF display=left AND (mouse.x-display.xMax>(IF display.color THEN vColorEscape ELSE vBWEscape)) THEN { -- move to right display
hideCursor[];
fromH ← display.yMax;
display ← right;
toH ← display.yMax;
mousePosition.x ← display.xMin;
terminal.SetMousePosition[mousePosition];
an integer divide, but we get our precision where we need it
mouse.y ← Basics.LongDiv[Basics.LongMult[mouse.y,toH],fromH];
setCursorBits[]}
ELSE mousePosition.x ← display.xMax };
mouse.x<display.xMin => { -- the mouse is moving left
IF display=right AND (display.xMin-mouse.x>(IF display.color THEN vColorEscape ELSE vBWEscape)) THEN { -- move to left display
hideCursor[];
fromH ← display.yMax;
display ← left;
toH ← display.yMax;
mousePosition.x ← display.xMax;
terminal.SetMousePosition[mousePosition];
an integer divide, but we get our precision where we need it
mouse.y ← Basics.LongDiv[Basics.LongMult[mouse.y,toH],fromH];
setCursorBits[]}
ELSE mousePosition.x ← display.xMin };
ENDCASE => mousePosition.x ← mouse.x;
mousePosition.y ← mouse.y;
terminal.SetMousePosition[mousePosition];
IF doTrack THEN {
cursorPosition: Terminal.Position ~ [x: mousePosition.x+hotX, y: mousePosition.y+hotY];
IF ~display.color THEN
terminal.SetBWCursorPosition[cursorPosition]
ELSE
terminal.SetColorCursorPosition[cursorPosition];
};
};
DoMousePosition: PROC = INLINE {
check threshholds
IF (grainTicksLeft←grainTicksLeft-1) > 0 THEN RETURN;
grainTicksLeft←grainTicks;
IF mousePosition#lastMousePosition THEN {
eventTime ← ReadEventTime[];
[]𡤎nterMousePosition[FALSE];
};
};
mouseAB: mousePosition ActionBody ←
[contents: mousePosition[mousePosition: [mousePosition.x, FALSE, mousePosition.y]]];
maxDeltaMouse: INTEGER = LAST[[-8..8)];
EnterMousePosition: PROC[enterFullPos: BOOL] RETURNS [enteredFull: BOOLFALSE] = {
in this procedure we convert the mousePosition, which corresponds to the cursor's coordinates to a value which is relative to the lower left corner of the display
dX: INTEGER = mousePosition.x-lastMousePosition.x;
dY: INTEGER = mousePosition.y-lastMousePosition.y;
mouseA: Action;
IF NOT enterFullPos AND (ABS[dX]<=maxDeltaMouse AND ABS[dY]<=maxDeltaMouse)
THEN {
dMouseB: deltaMouse ActionBody ← [contents: deltaMouse[value: [dX, -dY]]];
flip the origin
mouseA←@dMouseB;
}
ELSE {
y: INTEGERIF display.color
THEN terminal.colorHeight-mousePosition.y-1
ELSE terminal.bwHeight-mousePosition.y-1;
mouseAB.mousePosition←[mousePosition.x, display.color, y];
mouseA←@mouseAB;
};
IF EnterAction[mouseA].enteredFull THEN RETURN[TRUE];
lastMousePosition ← mousePosition;
};
KMethod: TYPE = {action, state};
chordWaits: NAT ← 0;
The number of screen refreshes to wait before accepting Red or Blue transtitions as given. This determines how close together Red&Blue have to be up/down to synthesize Yellow up/down. The default is to cater to 3-button users. Values in the range of 3 to 5 are recommended.
MouseClickIndex: NAT = 0;
EnterKeyState: PROC [method: KMethod ← action] RETURNS [enteredFull: BOOLFALSE] = {
recording kbd action or recording current downness
Kn: PROC [v: CARDINAL] RETURNS [KeyName] = INLINE {RETURN[LOOPHOLE[v]]};
Kv: PROC [n: KeyName] RETURNS [CARDINAL] = INLINE {RETURN[LOOPHOLE[n]]};
Hi-res mouse recording
IF mousePosition#lastMousePosition AND EnterMousePosition[FALSE].enteredFull
THEN RETURN [TRUE];
DO
scanNeeded: BOOLFALSE;
tempState: KeyState ← keyState;
tempTime: EventTime;
FOR i: CARDINAL IN [0..SIZE[KeyState]) DO
IF keyState.words[i] # lastKeyState.words[i] THEN
IF chordWaits # 0 AND i = MouseClickIndex THEN {
We are trying to synthesize Yellow from Red and Blue
newRed: DownUp ← keyState.bits[Red];
newBlue: DownUp ← keyState.bits[Blue];
oldRed: DownUp ← lastKeyState.bits[Red]; -- Left
oldBlue: DownUp ← lastKeyState.bits[Blue]; -- Right
oldYellow: DownUp ← lastKeyState.bits[Yellow]; -- Center
noChange: BOOLFALSE;
noEvent: BOOLFALSE;
THROUGH [0..chordWaits) DO
We loop until
Red and Blue are both down or both up => Yellow down or up
(and we clear the observed state of Red and Blue)
A key state change occurs => use Red & Blue as they are
(and we get to go through this whole thing again)
we exhaust the chordWaits => use Red & Blue as they are
SELECT TRUE FROM
newRed = newBlue => {
Chord Yellow up or down.
IF keyState.bits[Yellow] = up THEN {
keyState.bits[Yellow] can be set to down either from the real keyboard or from chording. This is a conveience for testing, since it is unlikely that anyone with a working 3-button mouse normally wants to use chording instead of the Yellow (middle) button.
tempState.bits[Yellow] ← keyState.bits[Yellow] ← newRed;
tempState.bits[Red] ← keyState.bits[Red] ← up;
tempState.bits[Blue] ← keyState.bits[Blue] ← up;
};
EXIT;
};
newRed # oldRed, newBlue # oldBlue => {
It is a transition of Red or Blue, not Yellow yet.
WaitForTick[];
tempState ← [bits[terminal.GetKeys[]]];
FOR kw: CARDINAL IN [0..SIZE[KeyState]) DO
IF tempState.words[i] # keyState.words[i] THEN {
scanNeeded ← TRUE;
EXIT};
ENDLOOP;
IF scanNeeded THEN {
tempTime ← ReadEventTime[];
newRed ← tempState.bits[Red];
newBlue ← tempState.bits[Blue];
IF newRed # newBlue THEN EXIT;
If the states of Red & Blue differ, then we want to record both actions.
IF newRed = oldYellow THEN EXIT;
A fast red or blue down and up should not be ignored. So we only want to loop if this will actually change Yellow.
};
};
ENDCASE => {
No chording involved
noChange ← TRUE;
EXIT;
};
ENDLOOP;
IF keyState.words[MouseClickIndex] = lastKeyState.words[MouseClickIndex] THEN {
Not really a transition
noEvent ← TRUE;
For debugging
LOOP;
};
};
FOR j: KeyName IN [Kn[i*wordlength]..Kn[(i + 1)*wordlength]) DO
x: DownUp = keyState.bits[j];
IF x # lastKeyState.bits[j] THEN {
aUpDown: keyUp ActionBody ←
SELECT TRUE FROM
x = up => [contents: keyUp[value: j]],
method = action => [contents: keyDown[value: j]],
ENDCASE => [contents: keyStillDown[value: j]];
IF EnterAction[@aUpDown].enteredFull THEN RETURN[TRUE];
};
ENDLOOP;
ENDLOOP;
lastKeyState ← keyState;
IF NOT scanNeeded THEN EXIT;
keyState ← tempState;
eventTime ← tempTime;
ENDLOOP;
};
EnterFullKeyState: PROC RETURNS [enteredFull: BOOL] = {
aAllUp: allUp ActionBody ← [contents: allUp[]];
IF EnterKeyState[].enteredFull THEN RETURN[TRUE]; -- enter any new actions --
lastKeyState ← [bits[ALL[up]]];
IF EnterAction[@aAllUp].enteredFull THEN RETURN[TRUE];--reader should clear state next time
RETURN[EnterKeyState[state].enteredFull]; -- enter current state of all down keys --
};
Unconditionally enter the current time, preferably as an increment to the previous entry. Assume that an absolute time value is entered occasionally.
EnterTime: PROC RETURNS [dTime: DeltaDeltaTime, enteredFull: BOOL] = INLINE {
a: deltaEventTime ActionBody;
difference: MsTicks ← EventTimeDifference[@eventTime, @lastEventTime];
deltaTime: CARDINAL; -- s/b DeltaTime, but can be out of range
[deltaTime, difference] ← MsTicksToDeltaTime[difference];
IF deltaTime > maxDeltaTime THEN RETURN[0, EnterFullTime[].enteredFull];
SubtractMsTicksFromEventTime[@eventTime, difference];
adjust time for roundoff in delta
IF deltaTime <= maxDeltaDeltaTime THEN RETURN[deltaTime, FALSE];
a ← [contents: deltaEventTime[value: deltaTime]];
RETURN[0, EnterAction[@a].enteredFull];
};
Unconditionally enter the current absolute time. --
EnterFullTime: PROC RETURNS [enteredFull: BOOL] = {
a: eventTime ActionBody ← [contents: eventTime[eventTime: eventTime]];
RETURN[EnterAction[@a].enteredFull];
};
EnterFullState: PROC = INLINE {
IF ~EnterFullTime[].enteredFull AND
~ EnterFullKeyState[].enteredFull THEN []𡤎nterMousePosition[TRUE];
};
InsertAction: PUBLIC SAFE PROC [action: ClassIncreek.ActionBody] = TRUSTED {
a procedure to fake actions
[] ← EnterAction[@action];
};
EnterAction: PROC [action: Action] RETURNS [enteredFull: BOOLFALSE] = {
may insert single action, or may record the full state of the input devices.
enteredFull tells which.
length: CARDINAL = SELECT action.kind FROM
deltaEventTime => SIZE[deltaEventTime ActionBody],
keyDown => SIZE[keyDown ActionBody],
keyStillDown => SIZE[keyStillDown ActionBody],
keyUp => SIZE[keyUp ActionBody],
allUp => SIZE[allUp ActionBody],
eventTime => SIZE[eventTime ActionBody],
deltaMouse => SIZE[deltaMouse ActionBody],
mousePosition => SIZE[mousePosition ActionBody],
penPosition => SIZE[penPosition ActionBody],
timedOut => SIZE[timedOut ActionBody],
ENDCASE => ERROR Internal[1];
Would be nice if something in the language would handle this. It's not only convenience and efficiency; the code should not have to enumerate the tag type here.
action.deltaDeltaTime ← 0;
WITH action SELECT FROM
eventTime, deltaEventTime => NULL;
ENDCASE => {
[action.deltaDeltaTime, enteredFull] ← EnterTime[];
IF enteredFull THEN RETURN[TRUE]; --full action record was made
};
IF ~WriteEntry[inscript, DESCRIPTOR[action, length]] THEN {
page full, start a new one with complete information
IF ~SetWritePage[inscript] THEN RETURN[FALSE];
action lost because inscript could not be expanded
EnterFullState[];
RETURN[TRUE]; };
lastEventTime ← eventTime; -- time only changes when something's been entered
};
SetMousePosition: PUBLIC SAFE PROC [pos: MousePosition] = CHECKED {
used to change hardware's idea of where the mouse is; use with great care!!
terminal.SetMousePosition[[x: pos.mouseX, y: pos.mouseY]];
};
GetMousePosition: PUBLIC SAFE PROC RETURNS [pos: MousePosition] = TRUSTED {
p: Terminal.Position = terminal.GetMousePosition[];
pos ← [p.x, display.color, p.y];
};
DefaultMouseGrain: PUBLIC SAFE PROC RETURNS [ticks: Intime.MsTicks, dots: INTEGER] = CHECKED {
RETURN[defaultGrainTicks, defaultGrainDots];
};
SetMouseGrain: PUBLIC SAFE PROC [ticks: Intime.MsTicks, dots: INTEGER] = CHECKED {
grainDots←MIN[defaultGrainDots, dots];
grainMsTicks←MIN[defaultGrainTicks, ticks];
grainTicks←(grainMsTicks+grainMsPerTick-1)/grainMsPerTick;
grainTicksLeft𡤀
};
SetCursorPattern: PUBLIC SAFE PROC [cursorPattern: CursorArray] = TRUSTED {
IF display.color
THEN terminal.SetColorCursorPattern[cursorPattern]
ELSE terminal.SetBWCursorPattern[cursorPattern];
};
GetCursorPattern: PUBLIC SAFE PROC RETURNS [cursorPattern: CursorArray] = TRUSTED {
IF display.color
THEN RETURN[terminal.GetColorCursorPattern[]]
ELSE RETURN[terminal.GetBWCursorPattern[]];
};
TurnOnColorCursor: PUBLIC SAFE PROC [side: Side] = TRUSTED {
color: POINTER TO DisplayRec;
IF hasColor THEN TurnOffColorCursor[];
IF NOT terminal.hasColorDisplay THEN RETURN;
SELECT side FROM
left => {display ← right; color ← left};
right => {display ← left; color ← right};
ENDCASE => ERROR;
color^ ← [xMin: 0, xMax: terminal.colorWidth-1, yMin: 0, yMax: terminal.colorHeight-1, color: TRUE];
hasColor ← TRUE;
terminal.SetColorCursorPosition[[-100, -100]];
[] ← terminal.SetColorCursorState[$visible];
};
TurnOffColorCursor: PUBLIC SAFE PROC = TRUSTED {
IF hasColor THEN {
set current display to bw
IF display.color THEN {
display ← (IF display=left THEN right ELSE left);
[] ← terminal.SetColorCursorState[$invisible];
terminal.SetBWCursorPattern[terminal.GetColorCursorPattern[]];
};
hasColor ← FALSE;
IF display=left THEN right^ ← left^ ELSE left^ ← right^;
make both recs bw
};
};
this declares on which display the pen is located
HasPen: PUBLIC SAFE PROC [display: HasPenType] = CHECKED { };
WaitForTick: PROC = INLINE {
WaitForEnabled[];
terminal.WaitForBWVerticalRetrace[];
};
WaitForEnabled: ENTRY PROC = INLINE {
UNTIL terminalHandlerEnabled DO
WAIT enableTerminalHandler;
ENDLOOP;
};
terminalProcess: PROCESS;
StartActionRecorder: PUBLIC SAFE PROC [scr: ClassInscript.Inscript] = TRUSTED {
<<Implementations of Intime and ClassInscript are assumed to be locked.>>
KeyboardPriority: Process.Priority = 6;
save: Process.Priority = Process.GetPriority[];
initialize the display records to reflect the standard display
inscript ← scr;
right ← @rightRep; left ← @leftRep;
right^ ← left ^ ← [xMin: 0, xMax: terminal.bwWidth-1,
yMin: 0, yMax: terminal.bwHeight-1, color: FALSE];
display ← right; --most bw displays are to the right of the color monitor
Process.SetPriority[KeyboardPriority];
terminalProcess ← FORK ActionRecorder[];
Process.SetPriority[save];
Terminal.RegisterNotifier[terminal, InscriptController];
Booting.RegisterProcs[c: InscriptCPProc, r: InscriptRBProc];
SetMouseGrain[defaultGrainTicks, defaultGrainDots];
EnableTerminalHandler[]; -- InscriptController[enable]; We're it.
};
InscriptController: Terminal.SwapNotifier = TRUSTED {
[vt: Virtual, action: SwapAction, clientData: REF ANY]
SELECT action FROM
coming => {
IF terminalHandlerCurrent = 0 THEN EnableTerminalHandler[];
terminalHandlerCurrent ← terminalHandlerCurrent + 1;
};
going => {
terminalHandlerCurrent ← terminalHandlerCurrent - 1;
IF terminalHandlerCurrent = 0 THEN DisableTerminalHandler[];
};
here, gone => NULL;
ENDCASE=>ERROR;
};
InscriptCPProc: Booting.CheckpointProc = TRUSTED {
DisableTerminalHandler[];
};
InscriptRBProc: Booting.RollbackProc = TRUSTED {
EnableTerminalHandler[];
};
terminalHandlerCurrent: INTEGER ← 0;
terminalHandlerEnabled: BOOLFALSE;
enableTerminalHandler: CONDITION;
EnableTerminalHandler: ENTRY PROC = {
ENABLE UNWIND => NULL;
Intime.AdjustEventTime[TRUE];
eventTime ← lastEventTime ← ReadEventTime[];
keyState ← [bits[terminal.GetKeys[]]];
lastKeyState ← keyState;
EnterFullState[];
terminalHandlerEnabled←TRUE;
NOTIFY enableTerminalHandler;
};
DisableTerminalHandler: ENTRY PROC = {terminalHandlerEnabled ← FALSE};
SetColorDisplaySide: PUBLIC SAFE PROC[side: Side] = CHECKED {
IF GetColorDisplaySide[]#side THEN {
temp: POINTER TO DisplayRec ~ right;
right ← left; left ← temp;
};
};
GetColorDisplaySide: PUBLIC SAFE PROC RETURNS[Side] = TRUSTED {
RETURN[IF left.color THEN left ELSE right];
};
ProcessProfile: UserProfile.ProfileChangedProc = CHECKED {
cw: INT ← UserProfile.Number["Interminal.chordWaits", 0];
SELECT cw FROM
> 1000 => cw ← 1000;
< 0 => cw ← 0;
ENDCASE;
chordWaits ← (cw+grainMsPerTick-1)/grainMsPerTick;
vBWEscape ← UserProfile.Number["Interminal.VBWEscape", 0];
vColorEscape ← UserProfile.Number["Interminal.VColorEscape", 0];
};
START HERE
Loader.MakeProcedureResident[--e.g.--ActionRecorder];
Loader.MakeGlobalFrameResident[--e.g.--ActionRecorder];
UserProfile.CallWhenProfileChanges[ProcessProfile];
END.