Jason Purdy > CGI-MxScreen > CGI::MxScreen

Download:
CGI-MxScreen-0.103.tar.gz

Dependencies

Annotate this POD

Related Modules

HTML::Template
CGI::Application
Apache::PageKit
Apache::Session
HTML::Mason
HTML::Embperl
Apache::Request
CGI::Session
Data::Dumper
HTML::EP
more...
By perlmonks.org
View/Report Bugs
Module Version: 0.103   Source  

NAME ^

CGI::MxScreen - a multi-screen stateful CGI framework

SYNOPSIS ^

 require CGI::MxScreen;

 my $manager = CGI::MxScreen->make(
     -bgcolor    => "#dedeef",
     -screens    =>
         {
             "state_1"   =>
                 [-class => "STATE_1", -title => "Hello"],
             "state_2"   =>
                 [-class => "STATE_2", -title => "Hello #2"],
         },
     -initial    => "state_1",
     -version    => "1.0",
 );

 $manager->play();

DESCRIPTION ^

CGI::MxScreen is a framework for building multi-screen stateful CGI programs. It is rather object-oriented, with some peculiarities brought by persistency constraints: all objects must be handled by Storable.

CGI::MxScreen is based on the CGI module, and co-operates with it, meaning you are able to use most CGI calls normally. The few places where you should not is where CGI::MxScreen supersedes the CGI functionalities: for instance, there's no need to propagate hidden values when you use CGI::MxScreen.

CGI::MxScreen is architected around the concept of screens. Among the set of defined screens within the same script, only one is visible at a time. One moves around the various screens by pressing buttons, which submit data to the server and possibly move you to a different screen. The state machine is handled by CGI::MxScreen, the user only defines which state (screen) a button shall move the application to

CGI::MxScreen is stateful in the sense that many of the runtime objects created to operate (and screens are among those) are made persistent. This is a very interesting property, because you do not have to worry too much about the underlying stateless nature of the CGI protocol. The CGI module brought the statefulness to the level of form controls, but CGI::MxScreen raises it to the level of the application itself.

CGI::MxScreen is not meant to be used for so-called quick and dirty scripts, or for scripts which do not require some fair amount of round trips between the browser and the server. You'll be better off with using the good old CGI module. However, for more complex web applications, where there is a fair amount of processing required on the server side, and where each script involves several states, CGI::MxScreen is for you.

OK, enough talking.

FRAMEWORK ^

This section describes the CGI::MxScreen framework. If you wish to read about the interface of the CGI::MxScreen managing object, please skip down to "INTERFACE".

Features

Here are the main features of CGI::MxScreen:

Flow

Here is a high-level description of the processing flow when issuing requests to a CGI::MxScreen script:

Example

The following example demonstrates the various common operations that need to be performed with CGI::MxScreen.

An important comment first: if we forget about the fact that you need an object per screen (which has some code overhead compared to using plain CGI), you will need to write more declarative code with CGI::MxScreen than you would with CGI, but this buys you more persistent state for fields, and lets you define the state transitions and associated processing for buttons.

Moreover, please note that this example could be written in less code by using the CGI module only. But CGI::MxScreen is not aimed at simple scripts.

Our example defines a two-state script, where one choose a color in the first screen, and then a week day in the second screen. The script reminds you about the choice made in the other screen, if any. It is possible to "redraw" the first screen to prove that the selection made is sticky. First, the whole script:

  1 #!/usr/local/bin/perl -T
  2 
  3 package Color; use base qw(CGI::MxScreen::Screen);
  4 
  5 use CGI qw/:standard/;
  6 
  7 sub init {
  8     my $self = shift;
  9     $self->vars->{color} = "";
 10 }
 11 
 12 sub display {
 13     my $self = shift;
 14     print h1($self->screen_title);
 15 
 16     my $color = $self->record_field(
 17         -name       => "color",
 18         -storage    => "color",
 19         -default    => $self->vars->{color} || "Green",
 20         -override   => 1,
 21         -values     => [qw(Red Green Blue White Black Yellow Orange Cyan)],
 22     );
 23 
 24     print p("You told me your favorite weekday was", $self->vars->{weekday})
 25         if exists $self->vars->{weekday};
 26 
 27     print p("Your favorite color is", popup_menu($color->properties));
 28 
 29     my $ok = $self->record_button(
 30         -name   => "Next",
 31         -target => "Weekday");
 32 
 33     my $redraw = $self->record_button(
 34         -name   => "Redraw",
 35         -target => $self->current_screen);
 36 
 37     print submit($ok->properties), submit($redraw->properties);
 38 }
 39 
 40 package Weekday; use base qw(CGI::MxScreen::Screen);
 41 
 42 use CGI qw/:standard/;
 43 
 44 sub init {
 45     my $self = shift;
 46     $self->vars->{weekday} = "";
 47 }
 48 
 49 sub display {
 50     my $self = shift;
 51     print h1($self->screen_title);
 52 
 53     print p("You told me your favorite color was", $self->vars->{color});
 54 
 55     my $weekday = $self->record_field(
 56         -name       => "day",
 57         -storage    => "weekday",
 58         -default    => $self->vars->{weekday} || "Mon",
 59         -override   => 1,
 60         -values     => [qw(Mon Tue Wed Thu Fri Sat Sun)],
 61     );
 62 
 63     print p("Your favorite weekday is", popup_menu($weekday->properties));
 64 
 65     my $back = $self->record_button(
 66         -name       => "Back",
 67         -target     => $self->spring_screen,
 68     );
 69 
 70     print submit($back->properties);
 71 }
 72 
 73 package main;
 74 
 75 require CGI::MxScreen;
 76 
 77 my $manager = CGI::MxScreen->make(
 78     -screens    =>
 79         {
 80             'Color'     => [-class => 'Color',   -title => "Choose Color" ],
 81             'Weekday'   => [-class => 'Weekday', -title => "Choose Day" ],
 82         },
 83     -initial    => ['Color'],
 84 );
 85 
 86 $manager->play();
 87 

Let's study this a piece at a time:

  1 #!/usr/local/bin/perl -T
  2 

The classical declaration for a CGI script, in taint mode.

  3 package Color; use base qw(CGI::MxScreen::Screen);
  4 

This defines the first state, Color. It inherits from CGI::MxScreen::Screen, as it should.

  5 use CGI qw/:standard/;
  6 

We're going to use CGI routines. We could do with less than what is exported by the :standard tag, but I did not bothered.

  7 sub init {
  8     my $self = shift;
  9     $self->vars->{color} = "";
 10 }
 11 

The init() routine is called on the screen the first time it is created. Upon further invocations, the same screen object will be used and re-used each time we need to access the Color state.

To differentiate from a plain CGI script which would use hidden parameters to propagate the information, we store the application variable in the persistent hash table, which every screen can access through $self->vars. Here, we initialize the "color" key, because any access to an unknown key is an error at runtime (to avoid malicious typos).

 12 sub display {
 13     my $self = shift;

The display() routine is invoked by the state manager on the screen selected for displaying.

 14     print h1($self->screen_title);
 15 

Prints screen title. This refers to the defined title in the manager, which are declared for each known screen further down on lines 78-82.

 16     my $color = $self->record_field(
 17         -name       => "color",
 18         -storage    => "color",
 19         -default    => $self->vars->{color} || "Green",
 20         -override   => 1,
 21         -values     => [qw(Red Green Blue White Black Yellow Orange Cyan)],
 22     );
 23 

This declaration is very important. It tells CGI::MxScreen that the screen makes use of a field named "color", and whose value should be stored in the global persistent hash under the key "color" (as per the -storage indication).

The remaining attributes are simply collected to be passed to the popup_menu() routine via $color-properties> below. They could be omitted, and added inline when popup_menu() is called, but it's best to regroup common things together.

The underlying object created by record_field() will be serialized and included in the CGI::MxScreen context (only the relevant attributes are serialized, i.e. CGI parameters such as -values are not). This will allow the processing engine to honour some meaningful actions, such as validation, storage, or on-the-fly patching.

Another important property of those objects is that CGI::MxScreen will update the value attribute, which would be noticeable if there was no -default line: you could query $color-value> to get the current CGI parameter value, as submitted.

 24     print p("You told me your favorite weekday was", $self->vars->{weekday})
 25         if exists $self->vars->{weekday};
 26 

If we have been in the Weekday screen, then the key "weekday" will be existing in the global hash $self->vars, because it is created by the init() routine of that object, at line 46. If we tried to access the key without protecting by the exists test on line 25, we'd get a fatal error saying:

    access to unknown key 'weekday'

This protection can be disabled if you want it so, but it is on by default. It will probably save you one day, but unfortunately this is a runtime check.

 27     print p("Your favorite color is", popup_menu($color->properties));
 28 

The above is generating the sole input of this screen, i.e. a popup menu so that you can select your favorite color. Note that we're passing popup_menu(), which is a routine from the CGI module, a list of arguments derived from the recorded field $color, created at line 16.

 29     my $ok = $self->record_button(
 30         -name   => "Next",
 31         -target => "Weekday");
 32 

This declaration is also very important. We're using record_button() to declare a state transition: we wish to move to the Weekday screen when the button Next is pressed.

 33     my $redraw = $self->record_button(
 34         -name   => "Redraw",
 35         -target => $self->current_screen);
 36 

The Redraw button simply redisplays the current screen, i.e. there is no transition to another screen (state). The current_screen routine returns the name of the current screen we're in, along with all the parameters we were called with, so that the transition is indeed towards the exact same state.

 37     print submit($ok->properties), submit($redraw->properties);
 38 }
 39 

We're finishing the display routine by calling the submit() routine from the CGI module to generate the submit buttons. Here again, we're calling properties() on each button object to expand the CGI parameters, just like we did for the field on line 27.

 40 package Weekday; use base qw(CGI::MxScreen::Screen);
 41 
 42 use CGI qw/:standard/;
 43 

This defines the second state, Weekday. It inherits from CGI::MxScreen::Screen, as it should. We also import the CGI functions in that new package.

Note that the name of the class need not be the name of the state. The association between state name and classes is done during the creation of the manager object (see lines 78-82).

 44 sub init {
 45     my $self = shift;
 46     $self->vars->{weekday} = "";
 47 }
 48 

Recall that init() is called when the screen is created. Since screen objects are made persistent for the duration of the whole session (i.e. while the user is interacting with the script's forms), that means the routine is called once for every screen that gets created.

Here, we initialize the "weekday" key, which is necessary because we're going to use it line 58 below...

 49 sub display {
 50     my $self = shift;
 51     print h1($self->screen_title);
 52 

This is the display() routine for the screen Weekday. It will be called by the CGI::MxScreen manager when the selected state is "Weekday" (name determined line 81 below).

 53     print p("You told me your favorite color was", $self->vars->{color});
 54 

We remind them about the color they have chosen in the previous screen. Note that we don't rely on a hidden parameter to propagate that value: because it is held in the global persistent hash, it gets part of the session context and is there for the duration of the session.

 55     my $weekday = $self->record_field(
 56         -name       => "day",
 57         -storage    => "weekday",
 58         -default    => $self->vars->{weekday} || "Mon",
 59         -override   => 1,
 60         -values     => [qw(Mon Tue Wed Thu Fri Sat Sun)],
 61     );
 62 

The declaration of the field used to ask them about their preferred week day. It looks a lot like the one we did for the color, on lines 16-22, with the exception that the field name is "day" but the storage in the context is "weekday" (we used the same string "color" previously).

 63     print p("Your favorite weekday is", popup_menu($weekday->properties));
 64 

The above line generates the popup. This will create a selection list whose CGI name is "day". However, upon reception of that parameter, CGI::MxScreen will immediately save the value to the location identified by the -storage line, thereby making the value available to the application via the $self->vars hash.

 65     my $back = $self->record_button(
 66         -name       => "Back",
 67         -target     => $self->spring_screen,
 68     );
 69 

We declare a button named Back, which will bring us back to the screen we were when we sprang into the current screen. That's what spring_screen is about: it refers to the previous stable screen. Here, since there is no possibility to remain in the current screen, it will be the previous screen. But if we had a redraw button like we had in the Color screen, which would make a transition to the same state, then spring_screen will still correctly point to Color, whereas previous_screen would be Weekday in that case.

 70     print submit($back->properties);
 71 }
 72 

This closes the display() routine by generating the sole submit button for that screen.

 73 package main;
 74 

We now leave the screen definition and enter the main part, where the CGI::MxScreen manager gets created and invoked. In real life, the code for screens would not be inlined but stored in a dedicated file, one file for each class, and the CGI script would only contain the following code, plus some additional configuration.

 75 require CGI::MxScreen;
 76 

We're not "using" it, only "requiring" since we're creating an object, not using any exported routine.

 77 my $manager = CGI::MxScreen->make(
 78     -screens    =>
 79         {
 80             'Color'     => [-class => 'Color',   -title => "Choose Color" ],
 81             'Weekday'   => [-class => 'Weekday', -title => "Choose Day" ],
 82         },
 83     -initial    => ['Color'],
 84 );
 85 

The states of our state machine are described above. The keys of the -screens argument are the valid state names, and each state name is associated with a class, and a screen title. This screen title will be available to each screen with $self->title, but there's no obligation for screens to display that information. However, the manager needs to know because when the display() routine for the script is called, the HTML header has already been generated, and that includes the title.

The act of creating the manager object raises some underlying processing: the session context is retrieved, incoming parameters are processed and silently validated.

 86 $manager->play();
 87 

This finally launches the state machine: the next state is computed, action callbacks are fired, and the target screen is displayed.

More Readings

To learn about the interface of the CGI::MxScreen manager object, see "INTERFACE" below.

To learn about the screen interface, i.e. what you must implement when you derive your own objects, what you can redefine, what you should not override (the other features that you cannot redefine, so to speak), please read CGI::MxScreen::Screen.

To learn more about the configuration options, see CGI::MxScreen::Config.

For information on the processing done on recorded fields, read CGI::MxScreen::Form::Field and CGI::MxScreen::Form::Utils.

For information on the state transitions that can be recorded, and the associated actions, see CGI::MxScreen::Form::Button.

The various session management schemes offered are described in CGI::MxScreen::Session::Medium.

The layering hooks allowing you to control where the generated HTML for the current screen goes in your grand formatting scheme are described in CGI::MxScreen::Layout.

Finally, the extra HTML-generating routines that are not implemented by the CGI module are presented in CGI::MxScreen::HMTL.

SPECIFIC DATA TYPES ^

This sections documents in a central place the state and callback representations that can be used throughout the CGI::MxScreen framework.

Those specifications must be serializable, therefore all callbacks are expressed in various symbolic forms, avoiding code references.

Do not forget that all the arguments you specify in callbacks and screens get serialized into the context. Therefore, you must make sure your objects are indeed serializable by the serializer (which is Storable by default, well, actually CGI::MxScreen::Serializer::Storable, which is wrapping the Storable interface to something CGI::MxScreen understands). See CGI::MxScreen::Config to learn how to change the serializer, and CGI::MxScreen::Serializer for the interface it must follow.

States

A state is a screen name plus all the arguments that are given to its display() routine. However, the language used throughout this documentation is not too strict, and we tend to blurr the distinction between a state and a screen by forgetting about the parameters. That is because, in practice, the parameters are simply there to offer a slight variation of the overall screen dispay, but it is fundamentally the same screen.

Anyway, a state can be either given as:

Callbacks

When an argument expects a callback, you may provide it under the foloowing forms.

INTERFACE ^

The public interface with the manager object is quite limited. The main entry points are the creation routine, which configures the overall operating mode, and the play() routine, which launches the state machine resolution.

Creation Routine

As usual, the creation routine is called make(). It takes a list of named arguments, some of which are optional:

-bgcolor => color

Optional, sets the default background color to be used for all screens. If unspecified, the value is gray75, aka "#bfbfbf", which is the default background in Netscape on Unix. The value you supply will be used in the BGCOLOR HTML tag, so any legal value there can be used. For instance:

    -bgcolor    => "beige"

You may override the default background on a screen basis, as explained in "Creation Routine" in CGI::MxScreen::Screen.

-initial => scalar | array_ref

Mandatory, defines the initial state. See States above for the actual format details.

The following two forms have identical effects:

    -initial    => ["Color"]
    -initial    => "Color"

and both define a state Color whose display() routine is called without arguments.

-layout => layout_object

Optional, provides a CGI::MxScreen::Layout object to be used for laying out the screen's HTML generated by display(). See CGI::MxScreen::Layout for details.

-screens => hash_ref

Mandatory, defines the list of valid states, whose class will handle it, and what the title of the page should be in that state. Usually, there is identity between a screen and a state, but via the display() parameters, you can have the same screen object used in two different states, with a slightly different mode of operation.

The hash reference given here is indexed by state names. The values must be array references, and their content is the list of arguments to supply to the screen's creation routine, plus a -class argument defining the class to use. See "Creation Routine" in CGI::MxScreen::Screen.

Example of hash_ref:

    {
        'Color'     => [-class => 'Color',   -title => "Choose Color" ],
        'Weekday'   => [-class => 'Weekday', -title => "Choose Day" ],
    }

The above sequence defines two states, each implemented by its own class.

-timeout => seconds

Optional, defines a session timeout, which will be enforced by CGI::MxScreen when retrieving the session context. It must be smaller than the session cleaning timout, if sessions are not stored within the browser.

When the session is expired, there is an error message stating so and the user is invited to restart a new session.

-version => string

Defines the script's version. This is your versioning scheme, which has nothing to do with the one used by CGI::MxScreen.

You should use this to track changes in the screen objects that would make deserialization of previous ones (from an old session) improper. For instance, if you add attributes to your screen objects and depend on them being set up, an old screen will not bear them, and your application will fail in mysterious ways.

By upgrading -version each time such an incompatibility is introduced, you let CGI::MxScreen trap the error and produce an error message.

Features

internal_error string

Immediately abort current processing and emit the error message string. If a layout is defined, it is honoured during the generation of the error message.

If you buffer STDOUT (which is the case by default), then all the output currently generated will be discarded cleanly. Otherwise, users might have to scroll down to see the error message.

log

Gives you access to the Log::Agent::Logger logging object. There is always an object, whether or not you enabled logging, if only to redirect all the logs to /dev/null. This is the same object used by CGI::MxScreen to do its hardwired logging.

See Log::Agent::Logger to learn what can be done with such objects.

play

The entry point that dispatches the state machine handling. Upon return, the whole HTML has been generated and sent back to the browser.

Utility Path

The concept of utility path stems from the need to keep all callback specification serializable. Since Storable cannot handle CODE references, CGI::MxScreen uses function names. In some cases, we have a default object to call the method on (e.g. during action callbacks), or one can specify an object. In some other case, a plain name must be used, and you must tell CGI::MxScreen in which packages it should look to find that name.

This is analogous to the PATH search done by the shell. Unless you specify an absolute path, the shell looks throughout your defined PATH directories, stopping at the first match.

Here, we're looking through package namespaces. For instance, given the name "is_num", we could check main::is_num, then Your::Module::is_num, etc... That's what the utility path is.

The routine CGI::MxScreen::add_utils_path must be used before the creation of the CGI::MxScreen manager, and takes a list of strings, which define the package namespaces to look through for field validation callbacks and patching routines. The reason it must be done before is that incoming CGI parameters are currently processed during the manager's creation routine.

LOGGING ^

During its operation, CGI::MxScreen can emit application logs. The amount emitted depends on the configuration, as described in CGI::MxScreen::Config.

Logs are emitted with the session number prefixed, for instance:

    (192.168.0.3-29592) t=0.13s usr=0.12s sys=0.01s [screen computation]

The logged session number is the IP address of the remote machine, and the PID of the script when the session started. It remains constant throughout all the session.

There is also some timestamping and process pre-fixing done by the underlying logging channel. See Log::Agent::Stamping for details. The so-called "own" date stamping format is used by CGI::MxScreen, and it looks like this:

    01/04/18 12:08:22 script:

showing the date in yy/mm/dd format, and the time in HH::MM::SS format. The script: part is the process name, here the name of your CGI script.

At the "debug" logging level, you'll get this whole list of logs for every intial script invocation:

    [main/0] t=0s u="ram" q="id=4"
    using "Mozilla/4.75 [en] (X11; U; Linux 2.4.3-ac4 i686)"
    t=0.20s usr=0.17s sys=0.01s [context restore + log init]
    t=1.15s usr=0.86s sys=0.05s [parameter init]
    t=1.71s usr=0.61s sys=0.07s [outside CGI::MxScreen]
    main()
    t=0.13s usr=0.12s sys=0.01s [screen computation]
    t=46.46s usr=43.42s sys=1.67s ["main" display]
    t=0.30s usr=0.29s sys=0.02s [context save]
    t=50.01s usr=45.53s sys=1.83s [total time] T=52.45s

The t=0s indicates the start of a new session, and u="ram" signals that the request is made for an HTTP-authenticated user named ram. The [main/0] indicates that we're in the state called main, and 0 is the interaction counter (incremented at each roundtrip). The q="id=4" traces the query string.

The next line traces the user agent, and is only emitted at the start of a new session. May be useful if something goes wrong later on, so that you can suspect the user's browser.

Then follows a bunch of timing lines, each indicating what was timed in trailing square brackets. The final total summs up all the other lines, and also provides a precious T=52.45s priece of statistics, measuring the total wallclock time since the script startup. This helps you evaluate the overhead of loading the various modules.

The single main() line traces the state information. Here, since this is the start of a new session, we enter the initial state and there's no state transition.

Note the very large time spent by the display() routine for that screen. This is because Carp::Datum was on, and there was a lot of activity to trace.

Compare this to the following log, where the user pressed a button called refresh, which simply re-displays the same screen, and where Carp::Datum was turned off:

    [main/1] t=1m11s d=19s u="ram"
    t=0.90s usr=0.83s sys=0.08s [context restore + log init]
    t=0.01s usr=0.00s sys=0.00s [parameter init]
    t=0.02s usr=0.02s sys=0.00s [outside CGI::MxScreen]
    main() -> main() on "refresh" pressed
    t=0.02s usr=0.01s sys=0.00s [screen computation]
    t=0.56s usr=0.58s sys=0.00s ["main" display]
    t=0.05s usr=0.05s sys=0.00s [context save]
    t=1.56s usr=1.50s sys=0.08s [total time] T=3.24s

The new d=19s item on the first line indicates the elapsed time since the end of the first invocation of the script, and this new one. It is the time the user contemplated the screen before pressing a button.

Note that there is no q="id=4" shown: CGI::MxScreen uses POST requests between its invocations, and does not propagate the initial query string. It is up to you to save any relevant information into the context.

The following table indicates the logging level used to emit each of the logging lines outlined above:

   Level    Logging Line Exerpt
   -------  --------------------------------
   warning  [main/1] ...
   info     using "Mozilla/4.75...
   debug    ... [context restore + log init]
   debug    ... [parameter init]
   debug    ... [outside CGI::MxScreen]
   notice   main() -> main() on "refresh"...
   debug    ... [screen computation]
   debug    ... ["main" display]
   debug    ... [context save]
   info     ... [total time] T=3.24s

All timing logs but the last one summarizing the total time are made at the debug level. All state transitions (button press, or even bounce exceptions) are logged at the notice level. Invocations are logged at the warning level, in order to trace them more systematically.

BUGS ^

There are still some rough edges. Time will certainly help polishing them.

If you find any bug, please contact both authors with the same message.

HISTORY AND CREDITS ^

CGI::MxScreen began when Raphael Manfredi, who knew next to nothing about CGI programming, stumbled on the wonderful MxScreen program, by Tom Christiansen, circa 1998. It was a graphical query compiler for his Magic: The Gathering database. I confess I learned eveything there was to learn about by studying this program. I owed so much to that MxScreen script that I decided to keep the name in the module.

However, MxScreen was a single application, very well written, but not reusable without doing massive cut-and-paste, and rather monolithic. The first CGI::MxScreen version was written by Raphael Manfredi to modularize the various concepts in late 1998 and early 1999. It was never published, and was too procedural.

In late 1999, I introduced my CGI::MxScreen to Christophe Dehaudt. After studying it for a while, he bought the overall concept, but proposed to drop the procedural approach and switch to a pure object-oriented design, to make the framework easier to work with. I agreed.

The current version of CGI::MxScreen is the result of a joint work between us. Christophe did the initial experimenting with the new ideas, and Raphael consolidated the work, then wrote the whole documentation and regression test suite. We discussed the various implementation decisions together, and although the result is necessarily a compromise, I (Raphael) believe it is a good compromise.

We managed to use CGI::MxScreen in the industrial development of a web-based project time tracking system. The source was well over 20000 lines of pure Perl code (comments and blank lines stripped), and we reused more than 50000 lines of CPAN code. I don't think we would have succeeded without CGI::MxScreen, and without CPAN.

The public release of CGI::MxScreen was delayed more than a year because the dependencies of the module needed to be released first, and also we were lacking CGI::Test which was developped only recently. Without it, writing the regression test suite of CGI::MxScreen would have been a real pain, due to its context-sensitive nature. See CGI::Test if you're curious.

AUTHORS ^

The original authors are Raphael Manfredi <Raphael_Manfredi@pobox.com> and Christophe Dehaudt <Christophe.Dehaudt@teamlog.fr>.

Send bug reports, suggestions, problems or questions to Jason Purdy <Jason@Purdy.INFO>

SEE ALSO ^

CGI::MxScreen::Config(3), CGI::MxScreen::Screen(3), CGI::MxScreen::Layout(3).

syntax highlighting: