Use ANSI escape codes for screen control

We will need some kind of screen building technique here for the various upcoming projects. Parilux needs a simple screen that shows the states of the LED's, the inputs and the I2C interface. To do this, there are several methods:

I guess you got the message: I will use the ANSI control codes for quick and easy graphical control. The "plan de campagne" (this is french for 'attack plan') will be to get familiar with the ANSII codes and on the fly develop an ANSI library for mocka.

A first source

To wet your appetite I show a first little program. It clears the screen and does some fancy printing. It's not a high level program, so I do not need to explain what is does and how it does it. Read it:

MODULE cls;

IMPORT	InOut;


PROCEDURE CtrlSeq (seq		: ARRAY OF CHAR);

BEGIN
   InOut.Write (33C);
   InOut.WriteString (seq);
   InOut.WriteBf
END CtrlSeq;


BEGIN
   CtrlSeq ('[2J');
   CtrlSeq ('[0;0H');

   CtrlSeq ('[31;41m'); 	InOut.Write (' ');
   CtrlSeq ('[33;44m');		InOut.WriteString ('                                       ');
   CtrlSeq ('[31;41m'); 	InOut.Write (' ');

   InOut.WriteString ("This is line 1");	InOut.WriteLn;		InOut.WriteBf;
   CtrlSeq ('[31;40m');
   InOut.WriteString ("This is line 2");	InOut.WriteLn;		InOut.WriteBf;
   CtrlSeq ('[32m');
   InOut.WriteString ("This is line 3");	InOut.WriteLn;		InOut.WriteBf;
   CtrlSeq ('[33m');
   InOut.WriteString ("This is line 4");	InOut.WriteLn;		InOut.WriteBf;
END cls.
   
What it does is shown below. The screen is cleared, the cursor goes to the home position and some printing is performed in different colours. This program was written within one hour after having printed the ANSI codes. ANSI is basic. But it's also fast..

The ANSI codes

In the table below are the ANSI control codes as they exist for the xterm terminal (most widely used in Linux and X11).

ANSI code Function
ESC[2J Clear screen
ESC[K Clear from cursor position to end of line
ESC[#;@H Move cursor position to line '#', column '@'
ESC[#m Define screen colours and attributes. These control sequences can be chained by using a semicolon: 'ESC[1;32m' combines the actions of ESC[1m and ESC[32m. Below are the parameters for this function and their meanings:

Code Does
Attributes
0 Normal intensity
1 High intensity
4 Underline (only in monochrome mode)
5 Blinking
7 Reverse colour
8 Invisible
Text colours
30 Grey/Black
31 Red
32 Green
33 Brown/Yellow
34 Blue
35 Magenta
36 Cyan
37 Greyish/White
Background colours
40 Black
41 Red
42 Green
43 Yellow
44 Blue
45 Magenta
46 Cyan
47 White

ESC[s Save cursor position for later recall
ESC[u Recall previously saved cursor position
ESC[#A Move cursor up over '#' lines
ESC[#B Move cursor down over '#' lines
ESC[#C Move cursor right over '#' columns
ESC[#D Move cursor left over '#' columns
ESC[#;@R Reports current cursor position (line and column)

If you look at the ANSI control codes you can get an indication how the curses package got it's name. In most comics and cartoons, the # and @ tokens are acronyms for swearings and curses. At least, this is my reasoning.

Make it modular

After we found out how it worked, it's getting time to pour the lot in a set of predefined functions. That's what I did in the 'Try1' program listed below.

MODULE Try1;

IMPORT	InOut, Strings, NumConv;


TYPE	AttrMode 		= (Normal, Bright, Underlined, Blinking, Reverse, Invisible);
	Colours			= (Black, Red, Green, Yellow, Blue, Magenta, Cyan, White);


PROCEDURE CtrlSeq (seq		: ARRAY OF CHAR);

BEGIN
   InOut.Write (33C);
   InOut.WriteString (seq);
   InOut.WriteBf
END CtrlSeq;


PROCEDURE Cls;

BEGIN
   CtrlSeq ('[2J')
END Cls;


PROCEDURE Cursor (lin, col	: CARDINAL);

VAR   str, tmp		: ARRAY [0..9] OF CHAR;
      ok1, ok2		: BOOLEAN;

BEGIN
   str := '[';
   NumConv.Num2Str (lin, 10, tmp, ok1);
   Strings.Append (str, tmp);
   Strings.Append (str, ';');
   NumConv.Num2Str (col, 10, tmp, ok2);
   Strings.Append (str, tmp);
   Strings.Append (str, 'H');
   IF  ok1 AND ok2  THEN
      CtrlSeq (str)
   END
END Cursor;


PROCEDURE Attribute (attr    : AttrMode);

BEGIN
   CASE  attr  OF
     Bright	: CtrlSeq ('[1m')	|
     Underlined	: CtrlSeq ('[4m')	|
     Blinking	: CtrlSeq ('[5m')	|
     Reverse	: CtrlSeq ('[7m')	|
     Invisible	: CtrlSeq ('[8m')
   ELSE
     CtrlSeq ('[0m')
   END
END Attribute;


PROCEDURE Foreground (colour 	: Colours);

BEGIN
   CASE  colour  OF
     Black	: CtrlSeq ('[30m')	|
     Red	: CtrlSeq ('[31m')	|
     Green	: CtrlSeq ('[32m')	|
     Yellow	: CtrlSeq ('[33m')	|
     Blue	: CtrlSeq ('[34m')	|
     Magenta	: CtrlSeq ('[35m')	|
     Cyan	: CtrlSeq ('[36m')
   ELSE
     CtrlSeq ('[37m')
   END
END Foreground;


PROCEDURE Background (colour 	: Colours);

BEGIN
   CASE  colour  OF
     Black	: CtrlSeq ('[40m')	|
     Red	: CtrlSeq ('[41m')	|
     Green	: CtrlSeq ('[42m')	|
     Yellow	: CtrlSeq ('[43m')	|
     Blue	: CtrlSeq ('[44m')	|
     Magenta	: CtrlSeq ('[45m')	|
     Cyan	: CtrlSeq ('[46m')
   ELSE
     CtrlSeq ('[47m')
   END
END Background;


BEGIN
   Cls;
   Cursor (0, 0);

   Background (Red);		InOut.Write (' ');
   Background (Black);		Attribute (Bright);		Foreground (Yellow);
   InOut.WriteString ('                                       ');
   Background (Red);		Attribute (Normal);		Foreground (Cyan);
   InOut.Write (' ');

   InOut.WriteString ("This is line 1");	InOut.WriteLn;		InOut.WriteBf;
   Foreground (Red);
   Background (Black);
   InOut.WriteString ("This is line 2");	InOut.WriteLn;		InOut.WriteBf;
   Foreground (Green);
   InOut.WriteString ("This is line 3");	InOut.WriteLn;		InOut.WriteBf;
   Foreground (Yellow);
   Attribute (Bright);
   InOut.WriteString ("This is line 4");	InOut.WriteLn;		InOut.WriteBf;
   Foreground (White);
   Background (Black);
   Attribute (Normal);
END Try1.
   
The output is as follows. Do some tests with the placement of the 'Normal' attribute. It is not just the reverse of 'Bright'. It will reset ALL other colour and attribute settings to not bright white on black.


Until now, the ANSI related subroutines are:

I'm not sure yet if I will make more functions. With these, you can do most of the tricks you will ever need.

Make a module out of the functions.

Now that we know how to make ANSI functions, it is time to modularize them. So I made an ANSI library consisting of ANSI.DEF and ANSI.MOD. Below is the source of ANSI.DEF (the definition module). As you can see, I broke my word and have added two more derived functions.

DEFINITION MODULE ANSI;

TYPE	AttrMode 		= (Normal, Bright, Underlined, Blinking, Reverse, Invisible);
	Colours			= (Black, Red, Green, Yellow, Blue, Magenta, Cyan, White);


PROCEDURE Cls;

	(*  Clear the view screen area 			*)


PROCEDURE Cursor (lin, col	: CARDINAL);

	(*  Set cursor to line = lin, column = col	*)	


PROCEDURE Attribute (attr    : AttrMode);

	(*  Set foreground attribute to 'attr'		*)


PROCEDURE Foreground (colour 	: Colours);

	(*  Set foreground colour to 'colour'		*)


PROCEDURE Background (colour 	: Colours);

	(*  Set background colour to 'colour'		*)


PROCEDURE Colour (fg, bg     : Colours; attr	: AttrMode);

	(*  Set foreground, background and attribute 	*)


PROCEDURE Fill (lin, col, len	: CARDINAL);

	(*  put cursor to (lin, col) and print 'len' spaces	*)

END ANSI.
   
Not very difficult. This is the public part of the module. It contains the function prototypes (*as they are called in a lesser language) and the TYPEs that the user needs to know.

Below is the implementation module: the actual runtime routines that the user does not need to be aware of when writing the software.

IMPLEMENTATION MODULE ANSI;

IMPORT	InOut, Strings, NumConv;


CONST	Escape 			= 33C;


PROCEDURE CtrlSeq (seq		: ARRAY OF CHAR);

BEGIN
   InOut.Write (Escape);
   InOut.WriteString (seq);
   InOut.WriteBf
END CtrlSeq;


PROCEDURE Cls;

BEGIN
   CtrlSeq ('[2J')
END Cls;


PROCEDURE Cursor (lin, col	: CARDINAL);

VAR   str, tmp		: ARRAY [0..9] OF CHAR;
      ok1, ok2		: BOOLEAN;

BEGIN
   str := '[';
   NumConv.Num2Str (lin, 10, tmp, ok1);
   Strings.Append (str, tmp);
   Strings.Append (str, ';');
   NumConv.Num2Str (col, 10, tmp, ok2);
   Strings.Append (str, tmp);
   Strings.Append (str, 'H');
   IF  ok1 AND ok2  THEN
      CtrlSeq (str)
   END
END Cursor;


PROCEDURE Attribute (attr    : AttrMode);

BEGIN
   CASE  attr  OF
     Bright	: CtrlSeq ('[1m')	|
     Underlined	: CtrlSeq ('[4m')	|
     Blinking	: CtrlSeq ('[5m')	|
     Reverse	: CtrlSeq ('[7m')	|
     Invisible	: CtrlSeq ('[8m')
   ELSE
     CtrlSeq ('[0m')
   END
END Attribute;


PROCEDURE Foreground (colour 	: Colours);

BEGIN
   CASE  colour  OF
     Black	: CtrlSeq ('[30m')	|
     Red	: CtrlSeq ('[31m')	|
     Green	: CtrlSeq ('[32m')	|
     Yellow	: CtrlSeq ('[33m')	|
     Blue	: CtrlSeq ('[34m')	|
     Magenta	: CtrlSeq ('[35m')	|
     Cyan	: CtrlSeq ('[36m')
   ELSE
     CtrlSeq ('[37m')
   END
END Foreground;


PROCEDURE Background (colour 	: Colours);

BEGIN
   CASE  colour  OF
     Black	: CtrlSeq ('[40m')	|
     Red	: CtrlSeq ('[41m')	|
     Green	: CtrlSeq ('[42m')	|
     Yellow	: CtrlSeq ('[43m')	|
     Blue	: CtrlSeq ('[44m')	|
     Magenta	: CtrlSeq ('[45m')	|
     Cyan	: CtrlSeq ('[46m')
   ELSE
     CtrlSeq ('[47m')
   END
END Background;


PROCEDURE Colour (fg, bg     : Colours; attr	: AttrMode);

BEGIN
   Foreground (fg);
   Background (bg);
   Attribute (attr)
END Colour;


PROCEDURE Fill (lin, col, len	: CARDINAL);

VAR   n	       : CARDINAL;

BEGIN
   Cursor (lin, col);
   FOR  n := 1 TO len  DO  InOut.Write (' ')  END
END Fill;

END ANSI.
   
If you defined a TYPE or a CONST, you do not need to define it again in the implementation module.

Testing the new library

I modified the Try1 program and renamed it Try2. It ought to be identical to Try1. You be the judge of that. Here is the source for Try2.mod:

MODULE Try2;

IMPORT	InOut, ANSI;

FROM  ANSI  IMPORT  AttrMode, Colours;

BEGIN
   ANSI.Cls;
   ANSI.Cursor (0, 0);

   ANSI.Background (Red);	InOut.Write (' ');
   ANSI.Background (Black);	ANSI.Attribute (Bright);	ANSI.Foreground (Yellow);
   InOut.WriteString ('                                       ');
   ANSI.Attribute (Normal);	ANSI.Background (Red);		ANSI.Foreground (Cyan);
   InOut.Write (' ');

   InOut.WriteString ("This is line 1");	InOut.WriteLn;		InOut.WriteBf;
   ANSI.Foreground (Red);
   ANSI.Background (Black);
   InOut.WriteString ("This is line 2");	InOut.WriteLn;		InOut.WriteBf;
   ANSI.Foreground (Green);
   InOut.WriteString ("This is line 3");	InOut.WriteLn;		InOut.WriteBf;
   ANSI.Foreground (Yellow);
   ANSI.Attribute (Bright);
   InOut.WriteString ("This is line 4");	InOut.WriteLn;		InOut.WriteBf;
   ANSI.Foreground (White);
   ANSI.Background (Black);
   ANSI.Attribute (Normal);
END Try2.
   
As you see, I used a mix of qualified and unqualified imports. I could have refrained from the unqualified imports if I would have used qualified names for the colours and attributes (like ANSI.red, ANSI.Bright) but I was too lazy for that.
I compiled the source and ran it. And it did what it was supposed to do.

Page created 16 June 2007,