Development/Howto/WritingWizard
From Mandriva Community Wiki
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.