1 Introduction
2 Ground Rules

Building a File System
3 File Systems
4 File Content Data Structure
5 Allocation Cluster Manager
6 Exceptions and Emancipation
7 Base Classes, Testing, and More
8 File Meta Data
9 Native File Class
10 Our File System
11 Allocation Table
12 File System Support Code
13 Initializing the File System
14 Contiguous Files
15 Rebuilding the File System
16 Native File System Support Methods
17 Lookups, Wildcards, and Unicode, Oh My
18 Finishing the File System Class

The Init Program
19 Hardware Abstraction and UOS Architecture
20 Init Command Mode
21 Using Our File System
22 Hardware and Device Lists
23 Fun with Stores: Partitions
24 Fun with Stores: RAID
25 Fun with Stores: RAM Disks
26 Init wrap-up

The Executive
27 Overview of The Executive
28 Starting the Kernel
29 The Kernel
30 Making a Store Bootable
31 The MMC
32 The HMC
33 Loading the components
34 Using the File Processor
35 Symbols and the SSC
36 The File Processor and Device Management
37 The File Processor and File System Management
38 Finishing Executive Startup

Users and Security
39 Introduction to Users and Security
40 More Fun With Stores: File Heaps
41 File Heaps, part 2
42 SysUAF
43 TUser
44 SysUAF API

I/O
45 Shells and UCL
46 UOS API, the Application Side
47 UOS API, the Executive Side
48 I/O Devices
49 Streams
50 Terminal Output Filters
51 The TTerminal Class
52 Handles
53 Putting it All Together
54 Getting Terminal Input
55 QIO

Glossary/Index


Download sources
Download binaries

Terminal Output Filters

Now that we've dealt with the raw layer of terminal (stream) output, it is time to take a look at the "cooked" layer. The raw layer of a device stack is the part that interfaces directly to the driver. The cooked layer adds user-friendly handling of the I/O. In essence, it "cooks" the raw data for user consumption. This is a concept we will use in the future as well.

The UOS FiP TTerminal (as distinguished from the HAL TTerminal) class, is the class that UOS will use to perform cooked terminal I/O. It uses an input filter and an output filter that handle the cooking of the data. Here is the abstract output filter class:

TOutput_Filter = class
                     public // Instance data...
                          Flags : cardinal ;
                          Term : TTerminal ;
                          Name : string ;

                     public // API...
                         function Write( S : string ;
                             Async : boolean ) : cardinal ;
                             virtual ; abstract ;
                         procedure Write_To_Driver ; virtual ; abstract ;
                 end ;
The output filter has a pointer to the TTerminal instance, a set of flags, and a Name, which we will describe in a future article. The Write method is used to write data to the terminal device and is called from the TTerminal instance. It is the job of the filter to buffer a certain amount of output data and hold it until the device is ready to receive it. The Write_To_Driver method flushes some (or all) data from the buffer to the device.

The reason that output needs to be buffered is because the computer may be quite able to write data out to the device faster than the device can process them - especially things like printers. In some cases, the computer can write the data several orders of magnitude faster than the device can process them. Further, the user can request that output be paused at any aribitrary time (for example, when changing paper on the printer). Some printers will automatically request a pause in output when they run out of paper.

The buffers are queues of bytes, which are descendents of the following class:

type TByte_Queue = class
                       public // Property handlers...
                           procedure Set_Size( Value : cardinal ) ;
                               virtual ; abstract ;
                           function Get_Size : cardinal ; // Max size
                               virtual ; abstract ;

                       public // API...
                           function Append( I : int64 ; Count : cardinal ) : integer ;
                               virtual ; abstract ;
                           function Peek( size : cardinal ) : int64 ;
                               virtual ; abstract ;
                           function Advance( Count : cardinal ) : int64 ;
                               virtual ; abstract ;
                           function Length : cardinal ; // Current size
                               virtual ; abstract ;

                       public // Properties...
                           property Size : cardinal
                               read Get_Size
                               write Set_Size ;
                   end ;
Each queue has a size, which is the maximum physical capacity, in bytes. It also has a current logical size, in bytes. The Append method is used to add data to the queue. Advance is used to remove the next item from the queue. Peek is used to look at the next item without removing it from the queue.

Here is the queue class that we will use in our filters.

type TByte_Ring_Queue = class( TByte_Queue )
                            public // Constructor
                                constructor Create ;

                            private // Instance data...
                                Buffer : Ansistring ;
                                Front : cardinal ;
                                // Front of queue (offset where next item is read)
                                Back : cardinal ;
                                // Back of queue (offset where next item is written)

                            public // Property handlers...
                                procedure Set_Size( Value : cardinal ) ;
                                    override ;
                                function Get_Size : cardinal ; override ;

                            public // API...
                                function Append( I : int64 ; Count : cardinal ) : integer ;
                                    override ;
                                function Peek( size : cardinal ) : int64 ;
                                    override ;
                                function Advance( Count : cardinal ) : int64 ;
                                    override ;
                                function Length : cardinal ; override ;
                        end ;
TByte_Ring_Queue is implemented as a ring queue, which is a high-performance, low-footprint queue. Such a queue has a Front and a Back pointer into a buffer (in this case, a string), which indicate the head and tail of the queue. When data is added to the end of the queue, if the tail extends past the end of the string - and there is room in the queue - the data is then written to the beginning of the string. This is why we call it a "ring" queue - the data wraps around from the end of the string to the beginning. This eliminates the need to copy data during addition or removal of data from queue, which would significantly slow down the queue opration. Now, let's look at the implementation.

// Constructor

constructor TByte_Ring_Queue.Create ;

begin
    inherited Create ;

    Size := 256 ; // Default size
end ;


// Property handlers...

procedure TByte_Ring_Queue.Set_Size( Value : cardinal ) ;

begin
    setlength( Buffer, Value ) ;
    Front := 1 ;
    Back := 1 ;
end ;


function TByte_Ring_Queue.Get_Size : cardinal ;

begin
    Result := System.length( Buffer ) ;
end ;
The constructor initializes the the buffer to a size of 256. The buffer can be resized, as desired, after construction. The string buffer is simply resized as requested, and the Front and Back indexes are set to 1. When the Front and Back are the same, the queue is empty. Returning the size of the queue is as simple as returning the length of the buffer string.

// API...

function TByte_Ring_Queue.Append( I : int64 ; Count : cardinal ) : integer ;

begin
    if( Count >= Size - Length ) then
    begin
        Count := Size - Length - 1 ;
    end ;
    Result := Count ;
    while( Count > 0 ) do
    begin
        dec( Count ) ;
        Buffer[ Back ] := chr( I and 255 ) ;
        I := I shr 8 ;
        inc( Back ) ;
        if( Back > Size ) then
        begin
            Back := 1 ;
        end ;
    end ;
end ;
The Append method takes a 64-bit integer and a count. A count of 1 indicates a single byte (the low 8 bits of I). A count of 2 indicates two bytes (the low 16 bits of I), and so forth - up to 8 bytes. First we make sure that the passed count isn't going to overflow the buffer and count is reduced to fit, if necessary. The result is set to the amount actually added to the queue. We then loop through Count bytes, adding each byte to the tail of the queue (indicated by the Back index). We increment the Back index and, if it goes past the end of the string, we set it to 1. Count is decremented and we loop back if there are still bytes to add.

function TByte_Ring_Queue.Peek( size : cardinal ) : int64 ;

var I : int64 ;
    Offset, Shift : integer ;

begin
    Result := 0 ;
    Shift := 0 ;
    Offset := Front ;
    while( Size > 0 ) do
    begin
        I := ord( Buffer[ Offset ] ) ;
        Result := Result or ( I shl Shift ) ;
        dec( Size ) ;
        inc( Offset ) ;
        if( Offset > Size ) then
        begin
            Offset := 1 ; // Wrap to start of buffer
        end ;
        Shift := Shift + 8 ;
    end ;
end ;
The Peek method allows us to look at the next value(s) on the queue without modifying the queue in the process. Up to 8 bytes can be returned in a single 64-bit integer. We start at the Front index of the buffer, get the value there, add it to the result (shifting the bits if/as necessary), and moce to the next index, keeping in mind the need to wrap around to the start of the buffer if we go past the end. This is repeated until we have all the requested data. Note that neither Back nor Front is altered.

function TByte_Ring_Queue.Advance( Count : cardinal ) : int64 ;

begin
    if( Count > Length ) then
    begin
        Count := Length ;
    end ;
    Result := Peek( Count ) ;
    Front := Front + Count ;
    if( Front > Size ) then
    begin
        Front := Front - Size ;
    end ;
end ;
The Advance method reads data from the queue and also removes it. It calls Peek to get the data, then adjusts the Front index past the read data. Again, if Front goes past the end of the buffer, we wrap it around to the start. Front - Size will set it to the proper index.

function TByte_Ring_Queue.Length : cardinal ;

begin
    if( Front <= Back ) then
    begin
        Result := Back - Front ;
    end else
    begin
        Result := Size - Front + Back ;
    end ;
end ;
Finally, the Length method returns the logical size (amount of data present) of the queue. If Front is Less than, or equal to, Back, the size is simply a matter of subtracting Front from Back. Otherwise, the data wraps around the end of the buffer. In such a case the length will be the amount of buffer after Front (Size - Front) plus Front (which is how far the data extends from the start of the buffer).

Now that we have a buffer class for use, we can examine the output filter. Here is the TDefault_Output_Filter class:

type TDefault_Output_Filter = class( TOutput_Filter )
                                  public // Constructors and destructors...
                                      constructor Create ;
                                      destructor Destroy ; override ;

                                  protected // Buffer...
                                      _Buffer : TByte_Queue ; // Buffer
                                      _Controls : array[ 0..31 ] of byte ;

                                  public // API...
                                      function Write( S : string ;
                                          Async : boolean ) : cardinal ;
                                          override ;
                                      procedure Write_To_Driver ; override ;
                              end ;
TDefault_Output_Filter adds a buffer and overrides the methods.

      // Control Character Codes
const CCC_HT = $100 ;
const CCC_VT = $200 ;
const CCC_CR = $400 ;
const CCC_LF = $800 ;
const CCC_FF = $1000 ;
const CCC_CC = $2000 ; // ^X
const CCC_BS = $4000 ;

constructor TDefault_Output_Filter.Create ;

begin
    inherited Create ;

    _Buffer := TByte_Ring_Queue.Create ;
    for Loop := 1 to 31 do
    begin
        _Controls[ Loop ] := CCC_CC ;
    end ;
    _Controls[ _DEL ] := _DEL ;
    _Controls[ _BEL ] := _BEL ;
    _Controls[ _BS ] := CCC_BS ;
    _Controls[ _HT ] := CCC_HT ;
    _Controls[ _VT ] := CCC_VT ;
    _Controls[ _CR ] := CCC_CR ;
    _Controls[ _LF ] := CCC_LF ;
    _Controls[ _FF ] := CCC_FF ;
    _Controls[ _DC1 ] := 0 ;
    _Controls[ _DC3 ] := 0 ;
end ;


destructor TDefault_Output_Filter.Destroy ;

begin
    _Buffer.Free ;

    inherited Destroy ;
end ;
The constructor and destructor create and free the buffer. We use the default buffer size of 256 bytes. The constructor also sets up the _Controls array, which we will discuss later in this article.

procedure TDefault_Output_Filter.Write_To_Driver ;

var S : Ansistring ;
    UEC : TUnified_Exception ;

begin
    setlength( S, 1 ) ;
    while(
           ( ( TFiP_Stream( Term.Stream )._Device.Updated_Info.Status and DS_Output_Ready ) = DS_Output_Ready )
           and
           ( _Buffer.Length > 0 )
         ) do
    begin
        S[ 1 ] := Ansichar( _Buffer.Advance( 1 ) ) ;
        TFiP_Stream( Term.Stream ).Write_Data( PAnsiChar( S )[ 0 ], 1, UEC ) ;
    end ;
end ;
This method writes a single character at a time from the buffer to the driver. First we check to see if the device is ready for output and to verify that the buffer has data. If both conditions are met, we get the next byte from the buffer via _Buffer.Advance and then write the data to the stream.

Before we look at the Write method, we need to look at the output filter flags that the output filer can use.

// Terminal output filter flags...
const TOFF_Null = 1 ; // Ignore output (throw it away)
const TOFF_Paused = 2 ; // Don't send output to device
const TOFF_Noformfeed	= 4 ; // Simulate FF with LFs (otherwise send FF)
const TOFF_Notab = 8 ; // UOS converts tabs to spaces (otherwise send TAB)
const TOFF_NoWrap = 16 ; // UOS doesn't output CRLF when input/output reaches the terminal width
const TOFF_NoVertical = 32 ; // Translate output controls to ^C format, otherwise send untranslated control chracters
TOFF_Null is a means of discarding output without terminating the running process. All output sent while this flag is active is simply ignored. TOFF_Paused indicates the output is not to be sent to the device when the flag is active. However, unlike TOFF_Null, the output is buffered up for later output to the device.
The remaining flags have to do with how to handle special characters. To understand special characters, we need to take some time to look into character sets. Back in our articles on the UOS File System, we talked about the various Unicode formats, including UTF8. A Unicode character - regardless of format - can be viewed as a numeric value from 0 to the maximum value allowed by the format. UTF16 has values 0 through 65535, for instance. UTF8 characters can be any value from 0 to 4294967295, but the number of bytes used depends on the actual character value. UTF8 character values from 0 through 127 can be represented as a single byte (with the uppermost bit being zero). These 128 possible values also happen to match a character encoding known as 7-bit ASCII (American Standard Code for Information Interchange), which also matches the low 128 characters of what is called the ANSI character standard. Since a byte contains 8 bits, the use of all 8 bits is sometimes called 8-bit ASCII. The problem is that ASCII didn't define the upper 128 characters (originally the high bit was ignored and so the upper ASCII characters matched the lower ASCII characters). Most terminals use ASCII characters (some older ones use what is called EBCDIC characters, but we'll ignore non-ASCII terminals for now). Why is all of this important? Because the low 32 values (character values 0 through 31) are special "control" characters. They do not display any glyphs when output, but they may cause some behaviors on the terminal. For instance, control character 13 is known as CR (carriage return) and causes the print position to move to the beginning of the line. When CR is combined with LF (ascii value 10, or "line feed"), a new line is started on the terminal. Just to make things more confusing, some older systems implemented special graphics glyphs for some or all of these control characters. Also, some terminals use the upper 128 ASCII values as special characters different from the Windows "ANSI" standard. However, we will ignore all of those oddball cases for the time being. Following is a table of ASCII characters so that you can get an idea about how the values map to glyphs and special controls.

ASCII (Decimal)

ASCII (Hex)

Character Glyph

ASCII Name

Character Name

HTML Entity

Comments

0

00


NUL

Null



1

01


SOH

Start of heading


Ctl+A

2

02


STX

Start of text


Ctl+B

3

03


ETX

End of text


Ctl+C

4

04


EOT

End of Transmission


Ctl+D

5

05


ENQ

Enquiry


Ctl+E

6

06


ACK

Acknowledge


Ctl+F

7

07


BEL

Bell


Ctl+G

8

08


BS

Backspace


Ctl+H

9

09

HT

Horizontal Tab

#TAB

Ctl+I

10

0A


LF

Linefeed

#RS

Ctl+J

11

0B


VT

Vertical Tab


Ctl+K

12

0C


FF

Form Feed


Ctl+L

13

0D


CR

Carriage Return

#RE

Ctl+M

14

0E


SO

Shift Out


Ctl+N

15

0F


SI

Shift In


Ctl+O

16

10


DLE

Data Link Escape


Ctl+P

17

11


DC1

Device Control 1


Ctl+Q

18

12


DC2

Device Control 2


Ctl+R

19

13


DC3

Device Control 3


Ctl+S

20

14


DC4

Device Control 4


Ctl+T

21

15


NAK

Negative Acknowledge


Ctl+U

22

16


SYN

Synchronous Idle


Ctl+V

23

17


ETB

End of Transmission Block


Ctl+W

24

18


CAN

Cancel


Ctl+X

25

19


EM

End of Medium


Ctl+Y

26

1A


SUB

Substitute


Ctl+Z

27

1B


ESC

Escape



28

1C


FS

File Separator



29

1D


GS

Group Separator



30

1E


RS

Record Separator



31

1F


US

Unit Separator



32

20

SPACE

Space



33

21

!


Exclamation



34

22


Quote



35

23

#





36

24

$


Dollar



37

25

%


Percent



38

26

&


Ampersand

amp


39

27

'


Apostrophe



40

28

(


Left Parenthesis



41

29

)


Right Parenthesis



42

2A

*


Asterisk



43

2B

+


Plus



44

2C

,


Comma



45

2D

-


Dash



46

2E

.


Period



47

2F

/


Slash



48

30

0


Zero



49

31

1


One



50

32

2


Two



51

33

3


Three



52

34

4


Four



53

35

5


Five



54

36

6


Six



55

37

7


Seven



56

38

8


Eight



57

39

9


Nine



58

3A

:


Colon



59

3B

;


Semicolon



60

3C

<


Less than

lt


61

3D

=


Equal



62

3E

>


Greater than

gt


63

3F

?


Question



64

40

@


Commercial at



65

41

A





66

42

B





67

43

C





68

44

D





69

45

E





70

46

F





71

47

G





72

48

H





73

49

I





74

4A

J





75

4B

K





76

4C

L





77

4D

M





78

4E

N





79

4F

O





80

50

P





81

51

Q





82

52

R





83

53

S





84

54

T





85

55

U





86

56

V





87

57

W





88

58

X





89

59

Y





90

5A

Z





91

5B

[


Left Bracket



92

5C

\


Backslash



93

5D

]


Right Bracket



94

5E

^


Caret



95

5F

_


Underscore



96

60

`


Grace accent



97

61

a





98

62

b





99

63

c





100

64

d





101

65

e





102

66

f





103

67

g





104

68

h





105

69

i





106

6A

j





107

6B

k





108

6C

l





109

6D

m





110

6E

n





111

6F

o





112

70

p





113

71

q





114

72

r





115

73

s





116

74

t





117

75

u





118

76

v





119

77

w





120

78

x





121

79

y





122

7A

z





123

7B

{


Left Brace



124

7C

|


Vertical Bar



125

7D

}


Right Brace



126

7E

~


Tilde



127

7F



DEL

Delete




Character values 0 through 31, and 127, are non-printing characters with special meanings, although not all control values are used on all platforms. UOS generally considers the following control values to be significant:
CharacterSignificance
NULSpecifically has no significance, so is often used as filler or to mark the end of data.
ETXUsed to request an interruption of program execution
EOTRequest status report
BELSounds a tone when output
BSUsed to move the print position left one character. Sometimes used as a delete-previous-character feature
HTMove to next horizontal tab position
LFMove to following line
VTMove to next vertical tab position (some printers)
FFMove to next page (printer)
CRMove to beginning of line, or as a means of submitting input (the Enter key)
SITurn output off
DC1Unpause I/O
DC3Pause I/O
NAKDelete line
CANUsed to force interruption of program execution
EMUsed to end input
ESCUsed to begin an escape sequence or as a delimiter

EOT, SI, DC1, and DC3 are generally processed as special characters - they are not considered data input.
LF, FF, CR, and ESC generally serve as input delimiters.
Insofar as the output filter flags are concerned, a FF (Form feed) character is normally sent to the device, where printers move to the top of the next page. However, some printers don't support this, so setting TOFF_NoFormFeed causes the output filter to translate FF into enough LFs (linefeeds) to move to the top of the next page.
Many devices will automatically position to the next tab stop when they receive a HT character. However, some do not support this, so if TOFF_Notab is set, the filter will output sufficient spaces to simulate moving to the next tab stop.
Finally, most devices will automatically move to the next line when the current line is filled with characters (automatic wrapping). Some devices, however, will simply overwrite the last character on the line if the line fills up. The TOFF_NoWrap flag causes the output filter to insert a CR and LF when the right margin is reached.

Now let's look at the Write method.

function TDefault_Output_Filter.Write( S : string ; Async : boolean ) : cardinal ;

var C : AnsiCHar ;
    I, Work, X : integer ;
    Translate : word ;

begin
    Result := 0 ;
    if( ( Flags and TOFF_Null ) <> 0 ) then
    begin
        Result := length( S ) ;
        exit ; // Throw output away
    end ;
    Term.Writes := Term.Writes + 1 ;
    if( Term.Tab_Stops = 0 ) then
    begin
        Term.Tab_Stops := 8 ;
    end ;
    if( Term.Char_Rows = 0 ) then
    begin
        Term.Char_Rows := 66 ;
    end ;
The write method returns the number of characters processed. We initialize this to 0. If the output flags have TOFF_Null set, we throw away the output by setting the result equal to the number of characters passed to the routine and then exit. After all, in this case, we've "processed" all the characters.
Next we increment the number of write operations. Then we make sure that we have respectable default values for the terminal. We will discuss the TTerminal class in the next article, but for now just understand that terminals have Char_Rows, Char_Columns, and Tab_Stops values that indicate the number of character rows per page (or screen), the number of character columns, and the number of spaces between tab stops, respectively. If Tab_Stops or Char_Rows are currently 0, we default them (to 8 and 66). Note that a Char_Columns value of 0 indicates an unknown width, and we don't default that.

    I := 0 ;
    while( I < length( S ) ) do
    begin
        inc( I ) ;
        if(
            ( ( Flags and TOFF_NoWrap ) = 0 )
            and
            ( Term.Column >= Term.Char_Columns )
            and
            ( Term.Char_Columns > 0 ) // Columns are not undefined
            and
            Positive_Character_Movement( S[ I ] )
          ) then
        begin
            insert( CR, S, I ) ;
            insert( LF, S, I + 1 ) ;
        end ;
        Translate := Translate_Character( S[ I ] ) ;
Next, we loop through each character passed to us. I is used as the offset into the passed output string. As we loop through the characters, we increment the index. If TOFF_NoWrap isn't in the flags, then we will wrap the output if we exceed the line width, but only if Char_Columns is greater than 0, and the next character moves the character position further to the right. In such a case, we insert a CR and a LF. These are inserted at the current position so that the following code will first process the CR.
Then we translate the character. Normally, translation results in no change to the character, but for certain characters the translation may convert some control characters to nulls - especially those that are part of flow-control. However, the main purpose of the Translate function is to determine special processing for certain characters. The translation result may be a special (non-character) code (see CCC_*).

        case Translate of
            CCC_HT : begin
                         Work := Term.Column + Term.Tab_Stops ;
                         Work := trunc( int( Term.Column div Term.Tab_Stops ) * Term.Tab_Stops ) ;
                         if( ( Flags and TOFF_Notab ) <> 0 ) then // Simulate tabs
                         begin
                             delete( S, I, 1 ) ;
                             for X := Term.Column to Work do
                             begin
                                 insert( ' ', S, I ) ; // Space to next column
                             end ;
                         end else
                         begin
                             Term.Column := Work ;
                         end ;
                     end ;
Based on the translation code, we perform different operations. The CCC_HT code is used for horizontal tabs. A horizontal tab moves the terminal position to the next tab position. By default, tab stops are every eight (8) spaces on character-cell (fixed character width) terminals, but this setting can be changed by the user for each individual terminal. In any case, we calculate the next tab position (column). If the TOFF_Notab flag is set, we will simulate a tab by deleting the tab character and then spacing to the proper column. Otherwise, we update the terminal column. It should be noted that if the physical device doesn't handle the HT character - or handles it in a way that differs from our internal model - then our internal position will differ from the physical position. Thus, incorrect terminal settings can possibly cause some output issues. This is unavoidable, but probably never catastrophic.

        CCC_FF : begin
                     Work := Term.Line + Term.Char_Rows ;
                     Work := trunc( int( Term.Line div Term.Char_Rows ) * Term.Char_Rows ) ;
                     if( ( Flags and TOFF_Noformfeed ) <> 0 ) then // Simulate FF
                     begin
                         delete( S, I, 1 ) ;
                         for X := Term.Line to Work do
                         begin
                             insert( LF, S, I ) ; // lF to next line
                         end ;
                     end else
                     begin
                         Term.Line := Work ;
                     end ;
                 end ;
The CCC_FF code is used for formfeeds. A form feed moves a printer's position to the top of the next page. By default, a page is assumed to be 66 lines long, but this can be altered by the user for a given terminal. In much the same way that we process the horizontal tab, we process form feeds. We calculate the number of lines until the top of the next page, based on Char_Rows. If the TOFF_Noformfeed flag is set, we output sufficient line feeds (LFs) to move to the top of the next page. Otherwise we simply update the terminal row position.

        CCC_VT : begin
                     if( Term.VTab_Stops > 0 ) then
                     begin
                         Work := Term.Line + Term.Char_Rows ;
                         Work := trunc( int( Term.Line div Term.VTab_Stops ) * Term.VTab_Stops ) ;
                         if( ( Flags and TOFF_Noverticaltab ) <> 0 ) then // Simulate VT
                         begin
                             delete( S, I, 1 ) ;
                             for X := Term.Line to Work do
                             begin
                                 insert( LF, S, I ) ; // lF to next line
                             end ;
                         end else
                         begin
                             Term.Line := Work ;
                         end ;
                     end ;
                 end ;
The CCC_VT code is used for vertical tabs. Some printers have the ability to set tab stops down the page (vertically) in much the same way that HT moves to horizontal tab stops. The vertical tab stops (VTab_Stops) can be set by the user for a given terminal. If it is 0, vertical tabs are assumed to be non-significant. otherwise, we handle it in much the same way that we handled HT and FF operations.

        CCC_CC : begin
                     C := AnsiChar( ord( S[ I ] ) + 64 ) ;
                     insert( '^', S, I ) ;
                     insert( C, S, I + 1 ) ;
                     delete( S, I, 1 ) ;
                 end ;
    end ; // case Translate

Finally, we handle the CCC_CC code. In the case where we don't want to output the actual control code to the terminal, but want to display that there was a code, this code can be used to indicate that a character ought to be converted to a caret (^) followed by a symbol that indicates which control code. For instance, control code 1 would be shown as "^A". We simply add the control code value to 64 to create the character after the caret. The control character is deleted and the caret and symbol are inserted in its place.

        Translate := Translate_Character( S[ I ] ) ;
        case Translate of
            CCC_BS : Term.Column := Term.Column - 1 ;
            CCC_CR : Term.Column := 0 ;
            CCC_LF : Term.Line := Term.Line + 1 ;
            CCC_HT, CCC_VT, CCC_FF, _DEL : ; // No further processing
            else if( ( Translate >= 32 ) and ( Translate <> 127 ) ) then
                 begin
                     Term.Column := Term.Column + 1 ;
                 end else
                 if( Translate >= 0 ) then
                 begin
                     S[ I ] := AnsiChar( Translate ) ;
                 end ;
        end ;
The next step is to determine horizontal movement on the line. First, we call Translate again since the last cases may have changed the current character. Next, we process the character, depending on the translation value. For a backspace (CCC_BS), the position moves left one column. For carriage return (CCC_CR), we reset the column to 0. For line feed (CCC_LF) we leave the column alone and increment the line instead. Other special codes are skipped since we already processed it. For other cases (non-code values) we handle normal characters (those greater than 31 and not 127) by incrementing the column by 1, otherwise we replace the actual character with the translation.

        if( _Buffer.Length >= _Buffer.Size - 1 ) then // Output buffer is full
        begin
            if( Async ) then
            begin
                Result := length( S ) - I + 1 ;
                exit ;
            end else
            begin
                Term.Kernel.USC.Block( 0, PS_IOW, Term ) ;
            end ;
        end else
        begin
            _Buffer.Append( ord( S[ I ] ), 1 ) ;
            inc( Result ) ;
            if( ( Flags and TOFF_Paused ) = 0 ) then // Output not paused
            begin
                if( ( TFiP_Stream( Term.Stream )._Device.Info.Status and DS_Output_Ready ) = DS_Output_Ready ) then
                begin
                    Write_To_Driver ;
                end ;
            end ;
        end ;
    end ; // while ( I <= length( S ) )
end ; // TDefault_Output_Filter.Write
Now we are ready to output the character. If the buffer is full, we can't proceed. In this case, we have two options. If the output operation is asynchronous (Async is true), we simply exit with a return value indicating the number of characters that we actually processed. If the request wasn't asynchronous, we ask the kernel to block the process. We will talk about blocked processes in a future article, but for now just understand that a blocked process does nothing until it is no longer blocked. The process will be unblocked when the buffer starts to empty.
If the buffer isn't full, we append the current character to the buffer, increment the result value, and check to see if the output is paused (the TOFF_Paused flag is set). If output is paused, the buffer simply fills up with characters. If output isn't paused, we check the device status to see if the device is ready for output. If the device is ready, we call Write_To_Driver to send the output to the device. As long as the device is ready to accept output, we will continue to output the buffer to the device to keep the buffer from filling up.
Finally, we loop back to process the next character in the output string.

Here's the local Translate function:

    function Translate_Character( S : AnsiChar ) : word ;

    begin
        if( S = DEL ) then
        begin
            Translate := _Controls[ 0 ] ;
        end else
        if( ( S < ' ' ) and ( S <> NUL ) ) then
        begin
            Translate := _Controls[ ord( S ) ] ;
        end else
        begin
            Translate := ord( S ) ;
        end ;
    end ;
We only support translation for control characters (1-31 and 127). NUL cannot be translated. Rather than have a 128-element array, we have a 32-element array and use index 0 for the DEL character. For any value outside of these control characters, we simply return the numeric value of the character.

Here's the local Positive_Character_Movement function:

    function Positive_Character_Movement( S : Ansichar ) : boolean ;

    var Translate : word ;

    begin
        if( S > #127 ) then
        begin
            Result := True ;
            exit ;
        end ;
        Translate := Translate_Character( S ) ;
        if( ( Translate >= 32 ) and ( Translate <> 127 ) ) then
        begin
            Result := True ;
            exit ;
        end ;
        case Translate of
            CCC_HT, CCC_CC, _HT : Result := True ;
            else Result := False ;
        end ;
    end ;
This function simply indicates whether the passed character would result in the movement of the current output column by one or more.

That's it for the default output filter. In the next article, we will discuss the TTerminal class, which uses these filters.