Development/Howto/WritingWizard

From Mandriva Community Wiki

Jump to: navigation, search
Writing a drakwizard module for fun and profit

Since an unknown version ( around March or April 2004 ), drakwizard can be extended using a directory : /etc/wizard.d.

People can write a Perl wizard, and add them to drakwizard interface easily. This page will show you how to create a simple wizard, and some techniques used to do so.

Contents


[edit] Why write a wizard?

Using drakwizard, you can provide an easy setup of complex software, using multiple rpms, in an interactive way. It should also work in text mode, allowing usage on a server. And because it is cool to add new DrakXTools.

[edit] How it works

drakwizard reads the directory /etc/wizard.d/ for files matching '*.conf', with two values :

~ $ cat /etc/wizard.d/Rmrf.conf
NAME="Rmrf"
DESCRIPTION="Cleaning wizard ( for testing purpose )"

Name is the name of the module, and description is the line that appears in the drakwizard interface. For the moment, it is better to provide a menu file, that runs drakwizard NAME, as there is no integration with the Mandriva Linux Control Center, and therefor most people will miss your wizard if you do not provide it in the menu.

Then it loads the Perl module, that should be placed in the proper directory, like ( for 10.2 ) : /usr/lib/perl5/vendor_perl/5.8.6/MDK/Wizard/. The Perl module has nothing special, all rules apply.

The Perl module defines a class, that provides a special structure that describes the wizard.

[edit] A wizard step by step.

As an example we will now create a cleaning wizard that removes your /var. We will use the config file shown before.

First, you should name the Perl module Rmrf.pm, as it is the name given in the previous file. Then copy and paste the following code in your favourite editor, and read the source code, each step is explained :

#!/usr/bin/perl -w
#
# version 0.1
# Copyright (C) 2004 <your name>
# Author: <your name, with your email>
#
# <insert your favourite free software license>
#

# you need to change the name of the module
package MDK::Wizard::Rmrf;
use lib qw(/usr/lib/libDrakX);
use strict;
use MDK::Wizard::Wizcommon;

# mandatory ( but I do not know why )
my $wiz = new MDK::Wizard::Wizcommon;
my $in = interactive->vnew;

# usually, we  declare some constants here.
# by convention, we place them in caps, but feel free to not do so
# my CONSTANT='foo';

# $o is the name of the structure describing the wizard ( o for object )
my $o = {
    # this is the name of the wizard, it appears in the title
    name => 'Cleaning Wizard',

    # this is where the wizard will store data asked or computed
    # during it's execution.
    var => {
        already_asked_question => 2,
        use_list => 1,

        # declaration is not mandatory ( search "Perl autovivification" in google )
        # but having the list of internal variables helps to understand the wizard.
        directory_to_remove => ''
    },      

    # this is the image shown in mcc. not relevant for the moment
    defaultimage => "/usr/share/wizards/cleaning_wizard.png",

    # this is the list of rpm's needed by the wizard to work. they are installed before the first page
    # coreutils, for rm :)
    needed_rpm => [ 'coreutils' ],
};    


# this is the real work :
#   describing each page of the wizard, the interaction
#   and everything, under the form of a hash.

$o->{pages} = {

    # first page is always named "welcome"
    # a page is a also a hash ( like almost everything in Perl )
    welcome => {
        # the name is displayed at the beginning of the page
        name => "Welcome to the cleaning wizard",
        
        # set no_back to one when you do not want a back button
        no_back => 1,
        
        # data hold the various "widgets" i.e. checkbox and everything
        # they appear in their order of declaration in the list
        data => [

            # each widget is indicated by a hash. each key corresponds with a certain
            # widget. For example, a label without anything is a non-editable label
            { label => "This wizard will help you to render your Mandriva Linux installation unusable." },
        ],
        
        # next indicate the next page. this is a fixed value, but there are other ways to specify it
        next => 'question_page'
    
    },
    
    question_page => {
        name => "Question page\n",
        data => [
            
            # here, we can see how we place a checkbox. we need to set the type to "bool"
            # and we have to give a reference for holding the value ( hence the \ before $o )
            { label => "Do you want to choose the directory to remove from a list ?", 
              type => "bool",
              val => \$o->{var}{"use_list"},
            }
        ],

        # according to the answer, we will now choose a different path for the user
        post => sub {
            # if he want list, we go to 'choose_list'
            return 'choose_list' if $o->{var}{"use_list"};
            print 'choose_textedit',"\n";
            # else, we go to 
            return 'choose_textedit';
        }
    },

    choose_list => {
        name => "Please choose the directory to remove from this list :\n",
        data => [
            {
                label => "List of directory :",
                val => \$o->{var}{directory_to_remove},

                # to use a list, we need to set this
                type => 'list',

                # and to give a list of value :
                list => ['/var/','/bin/','/lib/','/sbin','/usr'],
                
                # and a function to display them, one by one
                format => sub { return "Directory : $_" },
            }   
        ],
        next => 'remove_directory',
    },

    choose_textedit => {
        name => "Please give the directory to remove\n",
        data => [
            {
                # a text edit doesn't require a type on his own.
                # just give a value to hold the data
                label => "Directory :",
                val => \$o->{var}{directory_to_remove},

            }   
        ],
        # since someone may give a wrong path, we need to check it before 
        # completing the page
        # the function specified with 'complete' validates the entries.
        complete => sub {
               
            # if there is a problem
            if ( ! -d $o->{var}{directory_to_remove} )
            {
                # we need to display a error :
                $in->ask_warn("Warning","You didn't enter a real directory path.");
                # return 1, to signal the error
                return 1;
            }

            # return 0 if there is no problem
            return 0
        },
        next => 'remove_directory',
    },

    remove_directory => {

        # we do not show the directory here for reasons explained in the "Advanced section"
        name => "Now, we will ask you if you are sure to remove the directory\n",

        # if we have nothing special to display, we do not need to provide widget in the array 'data'.
        
        complete => sub {
               
            return 0 if ! $o->{var}{already_asked_question};
            # we now ask if the user is really sure, 2 times
            $in->ask_warn("Warning","You should not remove directory $o->{var}{directory_to_remove} using this wizard.\n");
            $o->{var}{already_asked_question} -= 1 ;
            return 1;
        },

        post => sub {
            # FIXME uncomment this line and distribute it everywhere ( <insert evil laugh> )
            # system("rm -Rf $o->{var}{directory_to_remove}");
            return 'cleaning_summary' 
        },
    },


    cleaning_summary => {
        name => "Cleaning summary",
        data => [
            { label => "Your drive is now clean", }
        ],
        no_back => 1,
        # indicate this is the last page
        end => 1,
        next => 0,
    },
};

# this is the creation of the wizard.
# you can copy/paste, if you didn't change the name
# of the previous variable
sub new {
    my ($class) = @_;
    bless $o, $class;
}

# and all modules must return 1 if properly initialized.
1;

[edit] Advanced technique

[edit] Installing rpm depending on the action

Sometimes, you cannot have a fixed list of rpm's to use for a wizard.

If you need to decide the rpm installed during the wizard, you can use the following piece of code :

my $rpm = 'the_rpm_to_install';
if ! $::in->do_pkgs->is_installed($rpm);
{
    $::in->do_pkgs->install($rpm);
}

Of course, it requires the proper source to be setup and everything. Check the return of install to see if everything went fine.

[edit] Dynamically disabling a widget

If you need to have a widget disabled dynamically, reacting to another widget, you can use this :

data => [
    # this one is a checkbox
    { label => "Use default value", 
      type => 'bool', 
      val => $o->{var}{use_default_value} 
    },

    # and this one is greyed when the previous item is checked
    { label => "Non default value :",
      disabled => sub { $o->{var}{use_default_value}; }
    },
]

[edit] Entering a password

If, for some reason, you need to enter something that should not be displayed, like a password, you can use the hidden attribute :

data => [
    { label => "Password :", hidden => 1, val => \$o->{var}{password} }
],

[edit] Translation

As you may have seen, most wizards are translated. To achieve this, all strings need to be specified like this :

    choose_list => {
        name => N("Please choose the directory to remove from this list :\n"),

instead of this :

    choose_list => {
        name => "Please choose the directory to remove from this list :\n",

Notice the usage of the function N().

Then, you should ask on cooker-i18n to see how to generate a po, where to place it, etc. Feel free to complete the wiki if you know.

[edit] Runtime change of the structure

In the example wizard, for the remove_directory page, we do not show directory in the name, as it requires a runtime modification of the object.

Using this :

        name => "Now, we will ask you if you are sure to remove the directory $o->{var}{directory_to_remove}\n",

doesn't work, because $o->{var}{directory_to_remove} is empty when the module is loaded.

However, we can do this in the previous page :

        post => sub {
            $o->{pages}{remove_directory}{name} = "Now, we will ask you if you are sure to remove the directory $o->{var}{directory_to_remove}\n";
            return 'remove_directory';
        },

You can use this technique to change the wizard as you want.

[edit] Editing the config file

Most wizards need to read and write a config file. Writing a configuration file is quite easy. Parsing one is not.

In order to overcome the difficulty, you should use a module called Libconf. In one wizard I wrote, I used these line :

            my $conf = new Libconf::Glueconf::Generic::Shell('/etc/sysconfig/tracd');
            $conf->{PROJECT} .= "$o->{var}{TRAC_REPOSITORY_PATH} ";
            $o->{var}{tracd_port} = $conf->{PORT};
            $conf->writeConf();

to parse the file, to add a path to a shell variable, everything done the proper way ( with quote, as it is a shell sourced file ), with comment being preserved ( very important, since this is some kind of documentation for most config files ).

And of course, it provides a high level api, that can parse more than a simple file. Check the package perl-Libconf, or the website : http://libconf.net.

[edit] Showing a message to users

If you need to show a message telling users they need to wait while you perform an operation ( such as cleaning the drives ), you can use this piece of code :

        post => sub {
           # title, message, and a optional 3rd argument which is not documented
            my $_win = $::in->wait_message("Cleaning in progress","Your system is being cleaned, please wait.");
            system("rm -Rf $o->{var}{directory_to_remove}");
            return 'cleaning_summary' 
        },

The window will be removed once $_win is no longer used, which means once the sub routine is finished. Do not forget to save the return of $::in->wait_message().

[edit] RPM integration

Since a wizard by itself is not really easy to use, you can distribute it in an rpm. In order to be coherent with the rest of the distribution, you should call it : drakwizard-nameofthewizard. For example, the wizard of trac is called drakwizard-trac.

Automated perl dependancy should take care of the requires on the drakwizard.

You should add a doc explaining what the wizard does, since people often complain about the fact that wizards do not help to learn how a system works.

Personal tools