Copyright © 2005-2006 Novell Inc.
This document is not to be construed as a promise by any participating company to develop, deliver, or market a feature or a product.
Novell Inc. makes no representations or warranties with respect to the contents of this document, and specifically disclaims any express or implied warranties of merchantability or fitness or any particular purpose. Further, Novell Inc. reserves the right to revise this document and to make changes to its content, at any time, without obligation to notify any person or entity of such revisions or changes.
You are allowed to distribute unchanged copies of this document.
Table of Contents
@deprecated@descr@example@param@ref@return@short@since@stable@struct@unstablecheck_ycp and Emacscheck_ycpsSTRINGTERM.TERM.Table of Contents
The most important thing reads: [DON'T PANIC!]
Administering a Linux system at the lowest level is sometimes not an easy task. If it should be done manually, it requires a very experienced and knowledgeable person, willing to browse and edit very many configuration files. As a result there have been many efforts to create intelligent tools that provide rather automatic means to accomplish this challenging and (at the same time) tedious task.
One of these tools is YaST, the SUSE Linux™ installer. Being the result of a
rather long period of development, it is by now a very large and capable
system, well suited to install and administer a SUSE Linux™ system. While the
internal functionality of YaST is quite multifaceted and therefore not
exactly easy to understand, it should not be kept as a secret. Rather the
world shall be encouraged to make use of the mechanisms YaST can
provide.
This goal can be achieved because YaST is not a closed monolithic
system but has a high degree of modularity. In fact it consists largely
of modules that could as well be created by people not related to YaST
development. For example hardware vendors could provide a YaST module
for customizing specific system settings related to their particular
piece of hardware. From the user's point of view this would be much more
comfortable than editing configuration files by hand.
Of course this can't be done without some knowledge of the YaST
internal functionality. So this document tries to lighten things up by
advancing from the unsubtle connections in the beginning to more and more
detailed explanations towards the end. However, describing the
particularities of this matter in full detail would easily fill several
heavy books which in turn would contradict the goal of introductory
simplification. Furthermore some of these “details” are
subject to moderate change service, which would render
“static” documentation like this one outdated rather
quickly.
To alleviate these problems, this text very often refers to the
“official” YaST developers documentation that can be found
in /usr/share/doc/packages/yast2* (especially towards
the end). Aside from the references to be found in the following text,
this location provides very valuable information regarding the whole
YaST environment. To have access to these files the following packages
must be installed:
yast2-devel
yast2-core-devel
liby2util-devel
yast2-packagemanager-devel
This document is subdivided into the following chapters:
Introduction. A brief explanation of the intention and nature of this document.
YaST - The Big Picture.
This is a short depiction of the YaST installer and the YaST
environment as such. The architectural peculiarities of YaST are
explained as far as it is necessary to understand the elucidations
that follow thereafter.
The YaST Language - YCP.
This chapter is dedicated to the YaST language that constitutes
most of the high-level functionality of YaST. Sections covering
the basic language elements are accompanied by others that deal with
user interface creation and program structure.
SCR Details.
In this chapter the YaST System Configuration
Repository (SCR) is explained in some concise detail. It
shows how to access configuration data and hardware data from within
YaST modules in a consistent way.
YaST Modules.
Some explanations regarding the different types of
YaST modules as well as some rules for writing
them.
Appendix A. References.
Throughout this document there are numerous references to the YaST
developers documentation. To ease access to these links this appendix
is mostly a dense index of those references.
Throughout this document some conventions regarding the typeface of printed text are used:
Emphasized text is used to denote important parts of the text.
Product names, file names and paths are printed using
literal typeface. Furthermore cut-outs from
programs that are embedded in the normal text flow are printed this
way.
Commands and command lines are printed using the command typeface.
Keyboard keys are denoted as in CTRL-C.
The description of programming language elements is displayed as shown below.
Synopsis: while (condition) loop_body
The parts of the language construct are printed like commands while any arguments are printed emphasized.
YaST is the installation program used by SUSE Linux™ to install Linux on a
system and to administer this installed system thereafter. The notation
“program” is a bit misleading here because in fact YaST
consists of many components and layers. Therefore one may as well regard
YaST as an installation and administering environment.
Among the most important components in this environment are the
YaST-modules which are usually written in a YaST-specific language
called YaST Control Language (YCP). With
exception of some rare cases the whole high-level functionality of YaST
is formulated in YCP. When YaST is running, the YCP-modules are
interpreted by the low-level YaST-components and the YCP-code makes
use of the infrastructure provided by them. The possibility to add such
modules at any time realizes the concept of extensibility that is
inherent in YaST.
This concept of extensibility by means of modules has been designed into
YaST from the very beginning. In fact YaST-modules are the layer of
YaST the user comes in contact with. Nearly every dialog on screen
during the installation is realized as a YaST-module and there are also
modules that act behind the scenes to care of specific pieces of
hardware, e.g. the keyboard.
The YaST-modules mentioned so far come ready-made with the distribution
and provide the core functionality to install and administer a
SUSE Linux™-system, but this need not and should not be the end. The extension
facility is intended to be also used by “third party
people”, e.g. hardware vendors, who want to contribute
YaST-functionality in some way.
To pave the way for this intention to come alive this document will
provide some insight into the inner mechanics of YaST. The primary goal
of the following elucidations is to make available the information that
is needed to write YaST-modules that conform to the programming
paradigm imposed by YaST.
What This Document Is.
This document explains in some detail how to extend the functionality
of YaST by means of modules. Of course every module of more than
trivial functionality will have to make use of the features that are
provided by the YaST-core-components and other YaST-modules. While
the official YaST developers documentation is the primary knowledge
base for all YaST related information, it is a bit overwhelming for
everyone who tries to get into the matter for the first time. Therefore
this document tries to provide a gentle introduction by explaining
things rather explicitly in the beginning and getting more and more
concise towards the end. By providing very many references to the
developers documentation this document can be thought of as a
“guided index” to this voluminous material.
What This Document Is Not.
This document will not explain the “binary”
particularities related to YaST, i.e. there will be no implementation
notes on how the low-level machinery of YaST is realized. Because
YaST implies the module concept for extensibility, only this approach
is promoted here. The “engine” that executes these modules
and how it is assembled is subject to the following explanations only
in so far as it is necessary to understand the interaction of the
various components.
The Audience Of This Document.
So this document is intended for all people who want to make use of the
YaST-functionality by providing modules for a specific task.
Additionally the information presented herein might be interesting for
all those who want to adept something about the whys and wherefores
related to YaST as such. Furthermore, to get most out of this
reading, the reader is supposed to have some programming experience in
a structured programming language, ideally C/C++. Some expertise in
functional programming could also be helpful but is not really
necessary.
Table of Contents
List of Figures
To be able to create a YaST-module it is necessary to have understood
how the extensive YaST-world is structured, which components there are,
what they do and how they do it. Therefore prior to going into closer
detail we'll step back from the blackboard and have a look at the big
picture first. By doing so you will have the opportunity to get an
overview of the ample terrain YaST is living on. While you don't have
to understand each and every byte YaST consists of, having seen the
whole issue will ease your understanding of the details we will come
across.
Table of Contents
YaST has been invented to have an extensible and fairly standardized
means to install and manage SUSE Linux™ on a system. Basically YaST serves
three main purposes:
Installation of SUSE Linux™ on a system
Configuration of the installed system
Administration of the installed system
To provide a solution to the resulting demands that has a lifetime
extensible well into the future this solution had to be flexible and
maintainable. Consequently some key concepts determined the design of
YaST. In particular it was the strict separation of:
The user interface
The functional code doing the job
The data representing the current state of the system
Furthermore YaST has some very specific attributes that make it
unique for the user as well as for those people who are developing it
or contributing to it. The following sections outline some of the
features of the YaST installer that should be seen as a guiding line
for module development.
Managing a SUSE Linux™ system requires direct low-level access to the system
which generally means reading and writing configuration data. Of
course this could be done manually by a knowledgeable person using a
conventional editor. A more comfortable and in most cases safer way
is to use YaST. Consequently YaST must be able to handle this
configuration data on the system level. By handling the
original data YaST activities take into
account manual editing that might also occur. Thus nobody is
forced to use YaST exclusively for
configuration tasks.
In YaST the access to system configuration data is realized by
means of a special component (or layer if you prefer), the
System Configuration Repository (SCR) (see below
and Access to the System (SCR in General)). The SCR component basically
consists of a number of so-called agents that
have been created to accomplish a specific kind of access. For
example there is an agent to run shell-commands and there is another
one that reads and writes ASCII-files of a specific format.
Additionally there are agents that provide access to the system
hardware e.g. by taking hold on the proc-file-system.
All these agents are gathered together under a common hood, the
SCR-API that can be used from within the YaST-modules in a
consistent way. In summary the SCR provides kind of a
view on all kinds of data, either YaST2 internal
data, original system configuration files or hardware data.
YaST implies lots of artificial intelligence to provide reasonable
suggestions for the various tasks. During installation the target
system is thoroughly analyzed with respect to its hardware components
and in most cases YaST succeeds in suggesting a proper
configuration for them.
These suggestions are presented in an overview dialog that shows the
main characteristics of the system to be installed and how YaST
would handle them. If you are satisfied with these automatically
generated settings you can simply accept them. If not, each of the
system configuration categories can be “activated” to be
changed manually. This is where the workflows
come into their own.
If you decide to change a specific configuration category this is usually being done in a workflow. Workflows are used to lead you through the steps necessary to accomplish a specific task. The steps are generally small to avoid an information “overflow”. At the end of the sequence the task has been accomplished and the changes are made permanent in the system.
As was stated above, you are not forced to do it
this way. You could as well edit configuration files by hand but
YaST can offer as much help as possible for this. Sometimes a
workflow has multiple branches for “novice” and
“expert” modes. The novice mode fills in the default
values and tries to determine as much as possible automatically. The
expert mode offers full control and allows to enter even unreasonable
values.
By providing pre-configured workflows and configuration data, it is
possible to automate almost arbitrary configuration tasks with
YaST. From adding a user, to installing a completely configured
SUSE Linux™ on specific hardware, nearly everything is possible.
Every workflow is assembled from rather small steps, implemented by
means of YaST modules written in a
YaST-specific scripting language, the YaST Control
Language (YCP). These YaST-modules are then called in
a predefined sequence to complete a specific task.
In fact it is possible to even write modules in
bash and Perl as long as
the module need not have a user interface, i.e. it is not
interactive. Such non-interactive modules typically handle specific
problems like controlling a particular piece of hardware and can be
called from within YCP-modules. This building block approach makes
constructing complex workflows easy and maintainable.
The YCP language is also used to control the user interface (UI)
presented on screen. The UI displays the information already known by
the system and retrieves the information entered by the user.
There are two modes of operation:
Text mode for console-based service
In text mode the user interface is presented in the NCurses environment that provides windowing capabilities and entry forms on a text-based console. Mouse support is neither possible nor necessary here because all dialogs can be operated using only the keyboard.
Graphics mode for X11-based service
In graphics mode the well-known Qt-system is used to present the dialogs in a graphical way using a running X11-server. Operating these dialogs follows the common habits of graphical user interfaces.
It is important to notice here that both methods principally use the
same YaST-specific YCP-API to build the dialogs. While there are
some (rare) cases where the YCP-code has to distinguish these modes,
the dialogs are usually programed for both worlds in in one single
source with the same code.
In summary YaST provides the following features, some of them
having already been mentioned above:
System access
YaST provides thorough probing of the system hardware and
presents the information gathered thereby via the SCR-API. The
SCR is also the means for reading and writing configuration
files.
Reasonable Suggestions
Based on the system analysis and predefined configuration data,
YaST is able to provide reasonable suggestions for almost any
configuration task.
Workflows
Management of particular configuration categories is usually realized in form of workflows that split up the whole task into small steps.
Modules and YCP
The steps constituting a workflow are usually realized as
YaST-modules that are written in the YaST Control
Language (YCP)
User interface
The user interface of YaST is realized by means of a specific
API from within the YCP-modules. This API supports a text-based
console-mode as well as a graphical X11-mode.
Internationalization
YaST provides support for various languages.
Multi-platform support
YaST provides support for various platforms like
Intel™ (x86), Apple™,
IBM™ (s390) etc.
Table of Contents
YaST2 is a modular system for Linux installation and system administration. The design goals include:
Flexibility
Extensibility
Maintainability
Network transparency
support administration of remote hosts or virtual machines on mainframes, machines without CD/DVD drives, rack-mounted machines
User interface independence
must run in graphical and text-only environments and serial consoles
Cover the whole range from novice users to expert system administrators
To achieve the above design goals, YaST2 is split up into a number of components for each individual task:
There is the core engine and to run scripts written in YCP (YaST2's own scripting language), Perl or (in future releases) other scripting languages.
The engine and scripts together form a YaST2 Module for the user.
Even though in most scenarios there is only one single machine, it is important to distinguish between the installation source machine and the installation target machine:
The installation source machine is the machine that holds the installation media - usually CDs or DVDs - and a mini-Linux called "inst-sys" that is copied from one of those installation media to that machine's RAM disk to have a basic operating system to work with on a "bare metal" machine (a machine that doesn't have an operating system installed yet). Most of that inst-sys is read-only, there is only limited disk space for temporary files, and since everything runs from a RAM disk the writable part of it is very volatile.
The installation target on the other hand is the machine that is to be installed or administered. That may be the same machine as the installation source machine (in fact, this is very common for PC installation or administration tasks), but it might as well be two distinct machines - a virtual machine on a mainframe computer or a remote rack-mounted machine without any display adapter or CD/DVD drives.
All communication with the installation target is handled via the System Configuration Repository (SCR) to guarantee the network abstraction design goal. This is much easier said than done, however: YaST2 module developers always have to keep in mind that it is strictly forbidden to access system files (or any other system resources, for that matter) directly, even if there may be very convenient CPAN Perl modules to do that. Rather, SCR is to be used instead - always. Otherwise everything might run fine if installation source and target are the same machine, but break horribly if they are not.
SCR in itself is also modularized: All calls are handled by "agents" that each
know how to handle a particular configuration "path" like "/etc/fstab" or
"/etc/passwd". That may be a simple file, but it may also be a directory
hierarchy like "probe" - this particular agent handles all kinds of hardware
probing, from mouse and display adapters to storage device controllers (like
SCSI or IDE controllers), disks attached to each individual controller or
partitions on those disks. Paths are denoted like ".etc.fstab" for SCR. YCP
even has a special data type "path" for just this case (a special kind of
string with some special operations).
SCR agents handle no more than three calls:
SCR::Read()
SCR::Write()
SCR::Execute()
The first argument is always the path to handle, but there may be any number of additional parameters, depending on the agent.
While Read() and Write() are obvious, Execute() may not be: This is intended for some kinds of agents that actually run a program on the installation target. In particular, the ".target.bash" agent does that - it runs a "bash" shell on the target machine and accepts a shell command as an argument. This is the tool of choice for tasks such as creating backup copies of configuration files or running any special command on the target machine - and again, the distinction between installation source and installation target machine becomes very important: You want run these commands on the (possibly remote) target machine, not on the machine that happens to hold the installation media.
SCR agents can easily added when needed. There are frameworks available to write SCR agents in C++, in Perl, or as Bash shell scripts as well as several ready-made parsers for different file formats like the ".ini" file parser that can handle files with "key = value" pairs or the "anyagent" that generalizes that concept even more using regular expressions. Those parsers return YCP lists and maps ready for further processing.
Typically, a YaST2 module for a specific installation or administration task includes a set of YCP or Perl scripts as well as some SCR agents to handle its particular configuration files.
Given the wide variety of machines that can possibly be handled with YaST2, it is important to keep the user interface (UI) abstraction in mind - very much like the SCR, the UI does not necessarily run on the installation target machine. It doesn't even need to run on the same machine as the WFM.
The UI provides dialogs with "widgets" - user interface elements such as input fields, selection lists or buttons. It is transparent to the calling application if those widgets are part of a graphical toolkit such as Qt, or text based (using the NCurses library) or something completely else. An input field for example only guarantees that the user can enter and edit some value with it. A button only provides means to notify the application when the user activated it - by mouse click (if the UI supports using pointing devices such as a mouse), by key press or however else.
The UI has a small number of built-in functions - for example:
UI::OpenDialog() accepts a widget hierarchy as an argument and opens a dialog with those widgets
UI::CloseDialog() closes a dialog
UI::QueryWidget() returns a widget's property such as the current value of an input field or selection box
UI::ChangeWidget() changes a widget's property
UI::UserInput() waits until the user has taken some action such as activate a button - after which the application can call UI::QueryWidget() for each widget in the dialog to get the current values the user entered. The application does not have to handle every key press in each input field directly - the widgets are self-sufficient to a large degree.
There is virtually no low-level control for the widgets - nor is it necessary or even desired to have that. You don't specify a button's width or height - you specify its label to be "Continue", for example, and it will adapt its dimensions accordingly. If desired, more specific layout constraints can be specified: For example, buttons can be arranged in a row with equal width each. The UI will resize them as needed, giving them additional margins if necessary.
The existing UIs provide another layer of network abstraction: The graphical UI uses the Qt toolkit which is based on the X Window System's Xlib which in turn uses the X protocol (usually) running on top of TCP/IP. X Terminals can be used as well as a Linux console (that may be the installation source machine or the installation target machine or another machine connected via the network) running the X Window System or even X servers running on top of other operating systems.
The NCurses (text based) UI requires no more than a shell session - on a text terminal (serial console or other), on a Linux console, in an XTerm session, via ssh or whatever.
Currently, there is no web UI, but YaST2's concepts would easily allow for that if it proves useful or necessary.
The component broker is the central piece of YaST. It acts as a dispatcher for all other components: When a (YCP, Perl or whatever) script calls a function, the broker determines what component handles that function call based on the respective namespace identifier. It is transparent to the caller what programming language a function is written in; the component broker handles that kind of dispatching. The caller only needs to know the function name, its namespace and (or course) the required parameters.
For example, calls like UI::OpenDialog() go to the UI (the user interface), SCR::Read() to the SCR (the system configuration repository). Even scripts can provide namespaces via modules in YCP or Perl.
All communication between the different parts of YaST core is done via a predefined set of YCP data types - simple data types like string, integer, boolean etc., but also compound data types like maps (key / value pairs, also known as "hashes" in other programming languages) or lists (like arrays or vectors in other programming languages). For complex data structures, maps, lists and simple data types can be nested to any degree.
The core-engine of YaST consists of some binary components
(modules) that are interconnected via YaST-specific protocols.
There are clients as well as
servers that are responsible for specific tasks
that may have to be accomplished during a YaST-session. According
to the well-known client-server-paradigm often used in software
technology, YaST-servers are program modules that
passively await connections from certain clients
to process their requests. Clients on the other hand are
active components that send requests to the
servers thereby initiating certain actions.
For example the SCR and the UI act as server components that process
client-requests on demand. An example for a client module is the
stdio-component that can be used to connect the
YaST-internal communication with a terminal.
Because this architectural specialty is meant to be used only by the
YaST core developers to establish and maintain the low-level
machinery we will not go into more detail here. Instead we will focus
on the advocated method of extending YaST at the “open
end” by creating YCP-modules.
Table of Contents
List of Figures
List of Examples
else if"
The YaST-language YCP is a scripting language to be interpreted by the
YCP-engine (YCP interpreter) specially designed for
manipulation with a system configuration. Its syntax is very similar
to C programming language.
Because YCP can make use of the whole infrastructure that
YaST provides, the actions that can be accomplished with YCP are
very powerful.
YCP has the usual features of procedural languages and
some more, partially originating from the functional
programming paradigm:
Control structures like if/then/else, foreach-loops.
Compound data types like strings, lists and maps.
Function definition (procedures)
Variable scopes
Name spaces
Include files
UNIX command execution (via the YaST infrastructure)
On the following pages we will explore the YCP language definition and
find out how to use YCP to write “programs” that can be
executed by YaST.
Table of Contents
Probably the best way to get into the matter is by means of a simple example.
The following little program opens a window that displays the string “Hello, World!” and provides a push button for termination.
Example 1.1. “Hello World” in YCP
{
string message = "Hello, World!";
UI::OpenDialog(
`VBox(
`Label( message ),
`PushButton("&OK")
)
);
UI::UserInput();
UI::CloseDialog();
}
In the following this code will be explained shortly in a line-by-line manner thereby touching some topics we will examine in detail later on.
{
The opening curly opens a so-called
block in YCP. Blocks are used to
“glue” several YCP-statements together to form an
entity that can be handled just like a single statement.
string message = "Hello, World!";
In this line we define a variable named “message” that
is of type string. In YCP any variable definition
must imply a value assignment to avoid all
errors that might occur due to uninitialized variables. Here we
assign the constant string “Hello, World!”.
Furthermore the terminating semicolon is mandatory in YCP to
indicate the end of a statement (just like C).
UI::OpenDialog(
This command opens a dialog on screen.
Because we want to display something, the code describing our
dialog has to be sent to the UI. This is being done by the leading
name space identifier UI::. The (single) parameter that
is supplied here determines the content of the dialog.
`VBox(
This is a UI-statement related to the geometry of the dialog to be defined. As the name indicates it opens a (virtual) vertical box that displays all content in a column-wise manner. (Geometry management is described in more detail in Chapter 9, Controlling The User Interface).
The leading back-quote introduces a YCP-feature that stems from
the functional programming paradigm. In YCP-speak the
`VBox() is a term. In YCP,
terms are used as a structured constants and are typically
passed to functions provided by YaST infrastructure
as parameters as is done here with
OpenDialog().
`Label( message ),
Displaying strings in YCP is done by means of
Labels. This statement gets one parameter, the
string variable we defined in the beginning. Because it is the
first of two parameters passed to `VBox() this
line is not terminated with a semicolon but with a comma. As in
most programming languages commas are used to separate parameters
in YCP.
`PushButton("&OK")
This statement displays a labeled push button. Since it is the
next element in the enclosing `VBox(), it is
displayed immediately below the preceding label. The & in the
label string is a YaST feature declaring the subsequent character
to be a key-shortcut. As a result the button can not only be
clicked with the mouse but also be activated by typing
ALT-O.
) and );
The next two lines first close the open
`VBox() and then the open
OpenDialog(). Because
`VBox() is passed as a parameter to
OpenDialog() there is no need to terminate
the statement with a semicolon. OpenDialog()
on the other hand is a statement in the UI and
hence must be terminated with a semicolon.
UI::UserInput();
Here we hand over control to the UI which then awaits some sort of user input. In this case it simply waits for the push button to be pressed by the user. Consequently our program blocks at this point until the user really does it.
UI::CloseDialog();
After all the UI-related action has finished, i.e. when
UI::UserInput() returns, we want to remove the
dialog we just created. This is done here.
}
Indicating the end of the block, the closing curly
bracket ends our little YCP-program.
Now we can start the program using YaST. For this,
we will use a script /sbin/yast2.
It is an envelope for easier setup of a running
YaST environment.
So if you are reading this document with a browser, you could
copy-and-paste the program listed above into a file
hello.ycp, and then run
/sbin/yast2 hello.ycp
which should render the following
“spectacular” result.
Starting off with this simple example we will now explore the more
subtle details of YCP. Since all programming is about handling of data
there must be a way to hold it in variables of different types. In the
next section you will get to know the various data types that YCP
knows about.
Table of Contents
Just like any other high-level programming language YCP has typed variables to hold data of different kinds:
This is the most simple data type. It has only one possible value:
nil. Declaring variables of this type doesn't make
much sense but it is very useful to declare functions that need not
return any useful value. nil is often also
returned as an error flag if functions fail in doing their job
somehow.
A symbol is a literal constant. It is denoted by a single backquote and a letter or underscore optionally followed by further letters, underscores or digits.
In contrast to C/C++, a YCP boolean is a real data type with the
dedicated values true and
false. Comparison operations like
< or == evaluate to a
boolean value. The if (...) statement expects a
boolean value as the result of the decision clause.
This is a machine independent signed integer value that is
represented internally by a 64 bit value. The valid range is from
-2^63 through 2^63-1. Integer
constants are written just as you would expect. You can write them
either decimal or hexadecimal by prefixing them with
0x, or octal by prefixing them with
0 (just like in C/C++).
Floating point numbers. Because they are represented via the C
datatype double the valid range is machine
dependent. Constants are written just as you would expect. The
decimal point is mandatory only if no exponent follows. Then there
must be at least one digit leading the decimal point. The exponent
symbol may be e or E.
Represents a character string of almost arbitrary length (limited only by memory restrictions). String constants consist of UNICODE characters encoded in UTF8. They are enclosed in double quotes.
The backslash in string can be used to mark special characters:
| Representation | Meaning |
|---|---|
\n | Newline (ASCII 10) |
\t | Tabulator |
\r | Carriage Return (ASCII 13) |
\b | Backspace |
\f | Form Feed |
\abc | ASCII character represented by the octal value abc. Note that unlike in C, there must be exactly 3 octal digits! |
\X | The character X itself. |
A backslash followed by a newline makes both the backslash and the
newline being ignored. Thus you can split a string constant over
multiple lines in the YCP code.
Example 2.4. String constants
“This string ends with a newline character.\n” “This is also a newline: \012”
A byteblock simply is a sequence of zero or more bytes. The ASCII
syntax for a byteblock is #[hexstring]. The
hexstring is a sequence of hexadecimal values,
lower and upper case letters are both allowed. A byte block
consisting of the three bytes 1, 17 and 254 can thus be written as
#[0111fE].
In most cases, however, you will not write a byteblock constant
directly into the YCP code. You can use the SCR to read
and write byteblocks.
A list is a finite sequence of values. These values need not
necessarily have the same data type. List constants are denoted
by square brackets. In contrast to C it is possible to use complex
expressions as list members when defining a list constant. The
empty list is denoted by [].
Example 2.6. List constants
[ ] [ 1, 2, true ] [ variable, 17 + 38, some_function(x, y) ] [ "list", "of", "strings" ]
Accessing the list elements is done by means of the index operator as
in my_list[1]:"error". The list elements are
numbered starting with 0, so index 1 returns the second element.
After the index operator there must be a colon
denoting a following default value that is
returned if the list access fails somehow. The default value should
have the type that is expected for the current list access, in this
case the string “error”.
Note 1: A list preserves order of its elements when iterating over them.
Note 2: There is also another method for accessing lists, originating
from the early days of YaST. The command select(my_list,
1, "error") also returns the second element of
my_list. While this still works, it is deprecated
by now and may be dropped in the future.
A YCP-map is an associative array. It is a list of key-value-pairs
with the keys being non-ambiguous, i.e. there are no two keys being
exactly equal. While you can use values of any type for keys and
values, you should restrict the keys to be of
type string because from experience other types
tend to complicate the code. Values of arbitrary
type on the other hand make the map a very flexible data container.
Maps are denoted with $[ key_0:value_0, key_1:value_1,
...]. The empty map is denoted by $[].
Note: A map does not reserve order of its elements when iterating over them.
Example 2.7. Map constants
$[ ] $[ "/usr": 560, "/home" : 3200 ] $[ "first": true, "2": [ true, false ], "number" : 8+9 ]
Accessing the map elements is done by means of the index operator as
in my_map["os_type"]:"linux". This returns the
value associated with the key "os_type". As
with lists, a default value must be appended
(after a colon) that is returned if the given key does not
exist. Again it should have the type that is expected for the
current access, in this case the string “linux”.
You may have noticed that the syntax for accessing maps kind of resembles that of accessing lists. This is due to the fact that lists are realized as maps internally with constant keys 0, 1, 2, and so on.
Note: There is also another method for accessing maps, originating
from the early days of YaST. The command lookup(my_map,
"os_type", "linux") also returns the value associated
with the given key. While this still works, it is deprecated by now
and may be dropped in the future.
A path is something special to YCP and similar to paths in TCL. It
is a sequence of path elements separated by dots. A path element can
contain any characters except \x00. If it contains
something else than a-zA-Z0-9_- it must be
enclosed in double quotes. The root path, i.e.
the root of the tree is denoted by a single dot. Paths can be used
for multiple purposes. One of their main tasks is the selection of
data from complex data structures like the SCR-tree (see Chapter 2, SCR Tree).
The backslash in paths can be used to mark a special characters:
| Representation | Meaning |
|---|---|
\n | Newline (ASCII 10) |
\t | Tabulator |
\r | Carriage Return (ASCII 13) |
\b | Backspace |
\f | Form Feed |
\xXX | ASCII character represented by the hexadecimal value XX. |
\X | The character X itself. |
Example 2.8. Path constants
. .17 .etc.fstab ."\nHello !\n".World ."\xff" == ."\xFF" ."\x41" == ."A" ."" != .
A term is something you won't find in C,
Perl, Pascal or
Lisp but you will find it in functional programming
languages like Prolog for
example. It is a list plus a symbol, with the list written between
normal brackets. The term `alpha(17, true) denotes
a symbol `alpha and the list [ 17, true
] as parameters for that symbol.
This looks pretty much like a function call.
You can also use the term as a parameter in another function call, for example to specify a user dialog.
Example 2.9. Term constants
`like_function_call(17, true) `HBox(`Pushbutton(`Id(`ok), "OK"), `TextEntry(`Id(`name), "Name"))
In the previous sections you have seen the data types YCP knows
about. In most cases you will (and should) assign a certain data type
to every variable you declare. However, there might be cases when the
type of a variable is not really clear at coding time, e.g. (in some
rare cases) if you access the SCR to get some hardware data. While
you should try very hard to avoid this situation, there might be
cases where you can't.
To solve this problem, you may assign the type
any to your variable which makes it accept
assignments of any other valid type. However, because variables of
type any are highly deprecated in YaST by now,
this “feature” will eventually be dropped in the near
future.
Table of Contents
Section not written yet...
Basically a block is a sequence of YCP statements enclosed in curly
brackets. It can be a whole YCP program as was the case with the
outermost block in hello.ycp from Section 1.1, “YCP Source”. What is special about blocks in YCP is
that they represent a value and therefore can be assigned to a
variable. It is sometimes really useful to have those blocks as YCP
values because this makes it possible to use them as parameters to
function calls. Of course the syntactical structure of blocks can
become rather complex which leads to a description of the whole
language itself. Therefore we put this into a section of its own:
Chapter 8, YCP Program Structure.
For now the following examples should suffice.
Table of Contents
Section not written yet...
In Data type any you have seen the data type
any. Because the value of type
any can not be assigned to
a variable of any other type. FIXME.
So it is important to check its type with is(...) and
then re-assigning it to a variable of the correct type. The following
example shows how this should be done.
Example 4.1. Type checking and data type any
//
// Hypothetical example:
// ---------------------
// We don't know whether the SCR will return integers or floats...
//
any any_var = 0;
integer int_var = 0;
float float_var = 0.0;
boolean int_case = false;
any_var = SCR::Read(...);
if ( is( any_var, integer ) )
{
int_var = any_var;
int_case = true;
}
else if ( is( any_var, float ) )
{
float_var = any_var;
int_case = false;
}
else
{
// Error...
}
if ( int_case )
{
// Use int_var...
}
else
{
// Use float_var...
}
As this is very cumbersome, you should try to
avoid this oddity in any case. If it is ineluctable, do it as shown
above to stay compatible with future YaST behavior.
Table of Contents
From the interpreters point of view any YCP value is an expression and
thus can be evaluated. How the evaluation is done
in a particular case depends on the data type of the expression.
Because the block data type is somewhat special with respect to evaluation it will be explained first. The other basic data types will follow thereafter.
A YCP block is a sequence of
statements enclosed in curly brackets. Upon
evaluation (execution), all the statements in the block are evaluated
one by one. Because blocks are also a valid data type in YCP, they
can have a value (see Data type block). If a
block contains the special statement return(...),
then the returned value replaces the block upon evaluation.
The following code sample shows a block with some statements.
{
integer n = 1;
while (n <= 10)
{
y2milestone("Number: %1", n);
n = n + 1;
}
y2milestone("Returned number: %1", n);
return n;
}
It calculates the numbers 1 through 10 and prints these numbers into
the log file. The statement y2milestone(...) used
for this is explained in YaST2 Logging along
with YCP-logging as such. For now we are interested in the output
that is written to the log file. As can be seen below the loop is
executed 10 times and the counter has the value 11 after the loop.
Finally the last statement return(...) determines
the value of the whole block, in this case 11.
...ycp/block_01.ycp:6 Number: 1
...ycp/block_01.ycp:6 Number: 2
...ycp/block_01.ycp:6 Number: 3
...ycp/block_01.ycp:6 Number: 4
...ycp/block_01.ycp:6 Number: 5
...ycp/block_01.ycp:6 Number: 6
...ycp/block_01.ycp:6 Number: 7
...ycp/block_01.ycp:6 Number: 8
...ycp/block_01.ycp:6 Number: 9
...ycp/block_01.ycp:6 Number: 10
...ycp/block_01.ycp:10 Returned number : 11
The basic YCP data types we got to know in Chapter 2, YCP Data Types are evaluated in a rather straight
forward way as will be shown in the following list.
Evaluation of basic data types
Simple data types
Most of the YCP data types can't be “evaluated” at
all, as they simply evaluate to themselves. This holds for the
simple types void, boolean,
integer, float,
string, symbol and
path.
list
When evaluating a list, the interpreter evaluates all the list elements thereby forming a new list.
{
list list_var = [ 1 + 1, true || false, "foo" + "bar" ];
y2milestone("list_var: %1", list_var );
}
yields the log file entry
...ycp/list_eval.ycp:4 list_var: [2, true, "foobar"]
map
A map is handled similar to a list. The values (but not the keys) are evaluated to form a new map.
{
map map_var = $[ "one" : `one, `two : "one" + "one" ];
y2milestone("map_var: %1", map_var );
}
yields the log file entry
...ycp/map_eval.ycp:4 map_var: $["one":`one, `two:"oneone"]
term
Upon evaluation, term parameters are evaluated to form a new term.
{
term term_var = `val ( 1 + 1, true || false, "foo" + "bar" );
y2milestone("term_var: %1", term_var );
}
yields the log file entry
...ycp/term_eval.ycp:4 term_var: `val(2, true, "foobar")
All the evaluations we have seen above are closely related to
operators that may be used within an expression to act on the
variables. The next section will give an overview of the operators
that can be used in YCP.
Table of Contents
As any other programming language YCP knows a lot of operators that
can be used to act on data.
These are binary operators for comparison of two values. The result is always boolean.
| Operator | Datatype | Description |
|---|---|---|
| == | almost all | True if operands are equal, otherwise false. |
| < | almost all | True if left operand is smaller than the right one, otherwise false. |
| > | almost all | True if left operand is greater than the right one, otherwise false. |
| <= | almost all | True if left operand is smaller or equal to the right one, otherwise false. |
| >= | almost all | True if left operand is greater or equal to the right one, otherwise false. |
| != | almost all | True if operands are not equal, otherwise false. |
These are logical operators, that works with boolean datatype, two are binary one is unary. The result is always boolean.
| Operator | Datatype | Description |
|---|---|---|
| && | boolean | True if both operands are true, otherwise false (logical and). |
| || | boolean | True if at least one of the operands is true, otherwise false (logical or). |
| ! | boolean | True if the operand if false, otherwise false (logical not). |
These are bit operators that works with integer, two are binary one is unary. The result is always integer.
| Operator | Datatype | Description |
|---|---|---|
| & | integer | Bits of the result number are product of the bits of the operands (bit and). |
| | | integer | Bits of the result number are count of the bits of the operands (bit or). |
| ~ | integer | Bits of the result number are reverted bits of operand (bit not). |
| << | integer | Bits of the result number are left shifted bits of the operands (bit shift left). |
| >> | integer | Bits of the result number are right shifted bits of the operands (bit shift right). |
There math operators works with numeric data types (integer and float) and also with string. All are binary (except unary minus).
| Operator | Datatype | Description |
|---|---|---|
| + | integer, float, string | The result is sum of the numbers or concatenation of the strings. |
| - | integer, float | The result is difference of the numbers. |
| * | integer, float | The result is product of the numbers. |
| / | integer, float | The result is quotient of the numbers (number class is preserved, thus quotient of integers produce integer, etc). |
| % | integer | The result is modulo. |
| unary - | integer, float | The result is negative number. |
This is the operator known from C language ( condition ? expression : expression). The first operand is expression that can evaluate to boolean, types of second and third operands are code dependent. The result of the triple operator expression is the second operand in the case the first operand (condition) evaluates to true, the third one otherwise.
| Code | Result | Comment |
|---|---|---|
| (3 > 2) ? true : false | true | The expression (3 > 2) evaluates to true, the result is true |
| contains ([1, 2, 3], 5) ? "yes" : "no" | "no" | The expression contains ([1, 2, 3], 5) evaluates to false, the result is "no" |
| (size ([]) > 0) ? 1 : -1 | -1 | The expression size ([]) > 0 evaluates to false, the result is -1 |
![]() | Note |
|---|---|
Using brackets makes code cleaner, but is not necessary (according to operators precedence). |
![]() | Note |
|---|---|
With the introduction of the index operator ( a = mapvar["key"]:default ), the sequence "]:" became a lexical token usable only for indexing, so watch out when using the triple operator with lists and maps. Use parentheses or white space. |
The table of operators precedence (from lowest to highest).
| Direction | Operators |
|---|---|
| right | = |
| left | ?: |
| left | || |
| left | && |
| left | == != |
| left | < <= > >= |
| left | + - |
| left | * / % |
| left | << >> |
| left | | |
| left | & |
| prefix | ! ~ - |
The bracket operator is the use of '[' and ']' like accessing arrays in C.
In YCP, this operator is used to ease handling with (possibly nested) lists and maps.
The bracket operator can be applied to any list or map variable and should be used in favour of (deeply) nested lookup() and select() cascades.
The access variant of the bracket operator is used for accessing
elements of a list or a map. It effectively replaces
select for lists and lookup
for maps.
General syntax:
for simple lists:
<list-var>[<index>]:<default-value>
for nested lists:
<list-var>[<index>,<index> <, ...>]:<default-value>
index must be an integer and counts from 0 up to the number of elements-1.
It will return the default-value if you try to access an out-of-bounds element.
![]() | Important |
|---|---|
Note that there must be no space between the closing bracket and the colon. |
Examples:
{
list list_of_numbers = [1, 2, 3];
// value of element with index 2 is 3
integer three = list_of_numbers[2]:0;
// evaluates to the default value 0,
// because list element with index 42 doesn't exist
integer zero = list_of_numbers[42]:0;
// evaluates to 4
integer number = [3, 4, 5][1]:8;
list list_of_lists = [[1,2], [3,4], [5,6]];
// list_of_lists[1] -> [3,4]
// [3,4][0] -> 3
//
// returns true because the left side evaluates to 3
// just as the right side does (three == 3)
return (list_of_lists[1,0]:0 == three);
}
General syntax:
for simple maps:
<map-var>[<key>]:<default-value>
Examples:
{
// map with string as a key, integer as a value
map simple_map = $["a":1, "b":2, "c":3];
// evaluates to 3
integer three = simple_map["c"]:0;
// evaluates to 0
integer zero = simple_map["notthere"]:0;
}{
// map with string as a key, integer as a value
// this example also defines the data-types
map <string, integer> simple_map = $["a":1, "b":2, "c":3];
// evaluates to 3
integer three = simple_map["c"]:0;
// evaluates to 0
integer zero = simple_map["notthere"]:0;
}for nested lists:
<map-var>[<key>,<key> <, ...>]:<default-value>
key must have an allowed type for maps, integer, string, or symbol.
It will return default-value if you try to access an non existing key.
![]() | Important |
|---|---|
Note that there must be no space between the closing bracket and the colon. |
Examples:
{
// map with string as a key and another map as a value
map nested_map = $[
"a":$[1:2],
"b":$[3:4],
"c":$[5:6]
];
// nested_map["b"] -> $[3:4] ("b" is a key of the map)
// $[3:4][3] -> 4 ( 3 is a key of the map)
//
// returns true
return (nested_map["b",3]:0 == 4);
}{
// map with string as a key and another map as a value
// this map has a defined data-type
map <string, map <integer, integer> > nested_map = $[
"a":$[1:2],
"b":$[3:4],
"c":$[5:6]
];
// returns true
return (nested_map["b", 3]:0 == 4);
}Since the bracket operator applies to list and maps, you can use it to access nested lists and maps. But be careful not to mix up the index/key types.
Examples:
{
map <string, list <integer> > map_of_lists = $[
"a":[1, 2, 3, 4],
"b":[5, 6],
"c":[7, 8, 9]
];
// evaluates to 3
integer three = map_of_lists["a", 2]:0;
list <map <integer, integer> > list_of_maps = [
$[1:2],
$[3:4],
$[5:6]
];
// returns true
return (list_of_maps[1,0]:0 == three);
}
The bracket operator can also be used on the left side of an assignment (lvalue). This changes the list or map element in place (!!) and must be used with care.
If the map or list element does not exist, it will be created.
The bracket operator can therefore replace add
and change.
Creating a new list element will extend the size of the list.
Holes will be filled with nil. See the examples
below.
If used as an lvalue, the default value is not allowed.
Examples:
{
list numbers = [1, 2, 3];
// changes the second element
// numbers == [1, 25, 3] now
numbers[1] = 25;
// extends the list to 7 elements (0-6)
// numbers == [1, 25, 3, nil, nil, nil, 6] now
numbers[6] = 6;
// remove an element item with index 2
// numbers == [1, 25, nil, nil, nil, 6] now
numbers = remove (numbers, 2);
}{
map <string, integer> new_map = $["a":1, "b":2, "c":3];
// changes the "c" element
// new_map == $["a":1, "b":2, "c":42] now
new_map["c"] = 42;
// add a new element to m
new_map["zz"] = 13;
// remove an elment "a"
// new_map == $["b":2, "c":42, "zz":13] now
new_map = remove (new_map, "a");
// returns $["b":2, "c":42, "zz":13]
return (new_map);
}
A string that is localized by YaST2 via the
gettext mechanism (see gettext and ngettext in
libc for more informations). Basically a string to be translated via
gettext must be enclosed in _(...) which causes
gettext to look up the translated string. It is even possible to
distinguish singular and plural verbalizations depending on a
parameter that denotes the actual number of something.
For a simple number-independent string you write
_(some_string_constant) which causes its
translation.
If the string to be translated needs to be different depending on the
multiplicity of something then you write
_(singular_string_constant1, plural_string_constant2,
actual_number). If actual_number
equals 1 then the translation of the first string is used. Otherwise
the translation of the second string is used.
Note 1: There are languages that distinguish more than the two cases singular and plural. Principally gettext can handle even those cases as it allows more than two strings for selection, but that is beyond the scope of this document (see the gettext documentation).
Note 2: It is not possible to put something other than string constants between the brackets.
Example 7.1. Locale constants
_("Everybody likes Linux!")
_("An error has occurred.", "Some errors have occurred", error_count)Table of Contents
Now that we have learned how data can be stored and evaluated in YCP,
we will take a look at the surrounding code structure that can be
realized. Code structure is created by means of
blocks and statements.
Despite not being “really”
statements, comments do (and should) belong to
the overall structure of a YCP program. There are two kinds of
comments:
Single-line comments
Single-line comments may start at any position on the line and reach up to the end of this line. They are introduced with “//”.
Multi-line comments
Multi-line comments may also start at any position on the line
but they may end on another line below the starting line.
Consequently there must be a start tag
(“/*”) and an end tag
(“*/”) as is shown below.
Example 8.1. Comments
{
// A single-line comment ends at the end of the line.
/*
Multi-line comments
may span several lines.
*/
y2milestone("This program runs without error");
}Synopsis: data_type variable_name = initial_value;
Variable declarations in YCP are similar to C. Before you can use a
variable, you must declare it. With the declaration you appoint the
new variable to be of a certain data_type which
means you can assign only values of that specific type. To avoid any
errors caused by uninitialized variables, a declaration
must imply a suitable value assignment.
Note: A variable declaration may occur at several points in the code which determines its validity (accessibility) in certain program regions (see Section 8.15, “Variable Scopes and blocks”).
Example 8.2. Variable Declaration
{
integer int_num = 42;
float float_num = 42.0;
string TipOfTheDay = "Linux is the best!";
integer sum = 4 * (int_num + 8);
}Synopsis: variable_name = value;
An assignment statement is almost the same as a declaration statement. Just leave out the declaration. It is an error to assign a value to a variable that has not already been declared or to a variable of different data type.
Example 8.3. Variable Assignment
{
integer number = 0;
number = number + 1;
number = 2 * number;
number = "Don't assign me to integers!"; // This will cause an error!!!
}Synopsis: if (condition) then_part [ else else_part ]
Depending on condition only one of the code
branches then_part and
else_part is executed. The
else_part is optional and may be omitted. Both
then_part and
else_part may either be single statements or a
sequence of statements enclosed in curly brackets, i.e. a block. The
then_part is executed if and only if
condition evaluates to true,
the else_part otherwise. It is an error if
condition evaluates to something other than
true or false.
Example 8.4. Conditional branch
{
integer a = 10;
if ( a > 10 )
y2milestone("a is greater than 10");
else
{
// Multiple statements require a block...
y2milestone("a is less than or equal to 10");
a = a * 10;
}
}Example 8.5. Conditional branch with "else if"
{
list &string& new_list = ["a", "new", "string"];
if (contains(new_list, "test"))
y2milestone("this is a test");
else if (size(new_list) > 100)
y2error("Too big list!");
else if (contains(new_list, "string"))
y2milestone("this is a new list");
else
y2error("Undefined behavior for list %1", new_list);
}Synopsis: while (condition) loop_body
The while() loop executes the attached
loop_body again and again as long as
condition evaluates to true.
The loop_body may be either a single statement
or a block of statements.
Because condition is checked at the top of
loop_body, it may not be executed at all if
condition is false right from
start.
Example 8.6. while() Loop
{
integer a = 0;
while (a < 10) a = a + 1;
while (a >= 0)
{
y2milestone("Current a: %1", a);
a = a - 1;
}
}Synopsis: do loop_body while (condition);
The do...while() loop executes the attached
loop_body again and again as long as
condition evaluates to true.
The loop_body may be either a single statement
or a block of statements.
Because condition is checked at the bottom of
loop_body, it is executed at least once, even if
condition is false right from
start.
Example 8.7. do...while() Loop
{
integer a = 0;
do
{
y2milestone("Current a: %1", a);
a = a + 1;
} while (a <= 10);
}Synopsis: repeat loop_body until (condition);
The repeat...until() loop executes the attached
loop_body again and again as long as
condition evaluates to false.
The loop_body may be either a single statement
or a block of statements.
Because condition is checked at the bottom of
loop_body, it is executed at least once, even if
condition is true right from
start.
repeat...until() is similar to do...while() except that condition is logically inverted. The example below has been converted from the do...while() example.
Example 8.8. repeat...until() Loop
{
integer a = 0;
repeat
{
y2milestone("Current a: %1", a);
a = a + 1;
} until (a > 10);
}
Synopsis: break;
The break statement is used within loops to exit immediately. The execution is continued at the first statement after the loop.
Example 8.9. break statement
{
integer a = 0;
repeat
{
y2milestone("Current a: %1", a);
a = a + 1;
if (a == 7) break; // Exit the loop here, if a equals 7.
} until (a > 10); // Value 10 will never be reached.
y2milestone("Final a: %1", a); // This prints 7.
}Example 8.10. break statement in foreach
{
foreach (string text, ["a", "new", "string", "break"], {
// finishes the foreach loop
if (text == "string") break;
// "string" and "break" will never get here
y2milestone("Current text is '%1'", text);
});
}Example 8.11. Nice break statement in foreach
{
// list of found prime-number
list <integer> found = [];
// start with number
integer number = 2;
// finish with number
integer max_number = 20000;
while (number < max_number) {
boolean not_found = true;
// try all already found numbers
foreach (integer try, found, {
if (number % try == 0) {
not_found = false;
break;
}
});
if (not_found)
found = add (found, number);
number = number + 1;
}
y2milestone("Sum: %1", size(found));
y2milestone("Found: %1", found);
}Example 8.12. break statement in listmap
{
integer counter = 0;
// goes through the list and returns a map
// made of this list
map new_map = listmap (string text,
["a", "new", "string", "break"],
{
// finishes the listmap loop
if (text == "string") break;
counter = counter + 1;
// returns one "key : value" pair of the new map
return $[text : counter];
});
y2milestone("Returned map: %1", new_map);
}Synopsis: continue;
The continue statement is used within loops to abandon the current loop cycle immediately. In contrast to break it doesnt exit the loop but jumps to the conditional clause that controls the loop. So for a while() loop, it jumps to the beginning of the loop and checks the condition. For a do...while() loop or repeat...until() loop, it jumps to the end of the loop end checks the condition.
Example 8.13. continue statement in while
{
integer a = 0;
while (a < 10)
{
a = a + 1;
if (a % 2 == 1) continue; // % is the modulo operator.
y2milestone("This is an even number: %1", a);
}
}Example 8.14. continue statement in foreach
{
// lists all files in /tmp directory
map cmd = (map) SCR::Execute(.target.bash_output, "ls -1a /tmp");
list <string> files = splitstring (
(string) cmd["stdout"]:"", "\n"
);
// clearing memory
cmd = nil;
foreach (string filename, files, {
if (regexpmatch(filename, "x")) {
y2warning("Filename '%1' contains 'x'", filename);
}
if (size(filename) > 20) {
y2error("Filename '%1' is longer than 20 chars", filename);
// we don't work with files with longer names
continue;
}
if (regexpmatch(filename, "^\.")) {
y2milestone("Filename '%1' starts with a dot", filename);
} else {
y2milestone("Filename '%1' is what we are looking for", filename);
}
});
}Example 8.15. continue statement in listmap
{
list <integer> random_integers = [];
// filling up with random numbers
while (size(random_integers) < 10) {
random_integers = add (random_integers, random(32768));
}
random_integers = toset(random_integers);
map <integer, integer> new_map = listmap (integer number, random_integers, {
// do not proccess huge numbers
// actually, this finished the listmap and returns nil
if (number > 32000) {
y2error("A huge number has been found");
continue;
}
if (number % 3 == 0) {
return $[number : number / 3];
} else {
return $[number : number * 3];
}
});
y2milestone("New map: %1", new_map);
}![]() | Note |
|---|---|
| The usage is similar to the break's one. |
Synopsis: return [ return_value ];
The return statement immediately leaves the current function or a current top level block (that contains it) and optionally assigns a return_value to this block. If blocks are nested, i.e. if the current block is contained in another block, the return statement leaves all nested blocks and defines the value of the outermost block.
However, if a block is used in an expression other than a block, and that expression is contained in an outer block, the return statement of the inner block won't leave the outer block but define the value of the inner block. This behavior is a as one would expect. For example in the iteration builtins in Section 8.16, “Applying Expressions To Lists And Maps”,
Example 8.16. return statement 1
{
// This block evaluates to 42.
return 42;
y2milestone("This command will never be executed");
}Example 8.18. return statement 3
{
// This program evaluates to 3:
integer a = 1 + { return 2; };
return a;
}Synopsis: data_type function_name ( [ typed_parameters ] ) function_body
A function definition creates a new function in the current namespace named function_name with a parameter list typed_parameters that has function_body attached for evaluation. The function_body must return a value of type data_type and the arguments passed upon function call must match the type definitions in typed_parameters.
Example 8.19. Function definition
{
void nothing()
{
y2milestone("doing nothing, returning nothing");
}
integer half( integer value )
{
return value / 2;
}
map <string, integer> get_some_map () {
return listmap (string key, ["a", "b", "c"], {
return $[key : random(999)];
});
}
// This renders: ...nothing: nil - half: 21...
y2milestone("nothing: %1 - half: %2", nothing(), half(42) );
// This renders something like: new half-random map: $["a":839, "b":393, "c":782]
y2milestone("new half-random map: %1", get_some_map());
}Synopsis: data_type function_name ( [ typed_parameters ] );
A function declaration allows you to declare only a header of a function without its body. It's main purpose is for indirect recursion etc. You have to provide a function definition with exactly the same arguments later in the same file. A new function will be declared in the current namespace named function_name with a parameter list typed_parameters.
Example 8.20. Function declaration
{
// declares the function - it is defined later
void nothing();
integer half( integer value )
{
return value / 2;
}
// This renders: ...nothing: nil - half: 21...
// uses the function nothing
y2milestone("nothing: %1 - half: %2", nothing(), half(42) );
// defines the function
void nothing()
{
y2milestone("doing nothing, returning nothing");
}
}Synopsis: include " included file";
The include statement allows you to insert contents of a file at the given place in the current file. If the current file is a module, the contents of the included file will become a part of the module.
This is useful for dividing a large file into number of pieces. However, if a file is included more than once in a single block, the 2nd, 3rd etc. include statements are ignored.
The included file can be
a relative or an absolute file name. Relative
names are looked up with /usr/share/YaST2/include
as a base.
Example 8.21. Include a file
// this will include /usr/share/YaST2/include/program/definitions.ycp include "program/definitions.ycp";
Synopsis: import "name_space";
The import statement allows you to import another namespace (module) into the one you are just running in. Then you can access the global functions and variables of that namespace using the double-colon.
Namespaces, to be imported, are looked up in the
/usr/share/YaST2/modules/ directory.
{
import "Hostname";
import "IP";
// writes: Check FQDN: true
y2milestone("Check FQDN: %1", Hostname::Check("www.example.com"));
// writes: Check IP4: false
y2milestone("Check IP: %1", IP::Check4("192.168.0.356"));
}This is often used for clients using some global API of modules but also for modules using global API of another ones. Please, be careful on creating cross-dependencies when creating an RPM package from sources.
In contrast to many other programming languages, YCP variables can
be defined at (almost) any point in the code, namely between other
statements. Given that, there must be some rules regarding the
creation, destruction and validity of variables. Generally variables
are valid (accessible) within the block they are declared in. This
also covers nested blocks that may exist in this current block. The
valid program region for a variable is called a
scope.
Example 8.22. Variable scopes and blocks
{
// Declared in the outer block
integer outer = 42;
{
// Declared in the inner block
integer inner = 84;
// This is OK.
// Log: ...IN: inner: 84 - outer: 42
y2milestone("IN: inner: %1 - outer: %2", inner, outer);
}
// This yields an error because "inner" is not defined any more.
y2milestone("OUT: inner: %1 - outer: %2", inner, outer);
}
Additionally to the structural language elements described so far,
there are special commands that apply to lists and
maps. What is special about these commands is that
they apply an expression to the single elements of a list or
map. This is done in a functional manner,
i.e. the expression to be applied is passed as a parameter. Generally
this executes faster than a procedural loop
because the internal functionality is realized in a very effective
way.
Furthermore some of these commands create lists from maps, maps from maps, maps from lists etc., so that they can be used to avoid the cumbersome assembling of these compound data types in a procedural loop.
Synopsis (list): any foreach (
type variable, list<type>, { expression }
);
Synopsis (map): any foreach(
type_key variable_key, type_value variable_value,
map<type_key, type_value>, { expression }
);
This statement is a means to process the content of list or map in a sequential manner. It establishes an implicit loop over all entries of the list or map thereby executing the given expression with the respective entries. With lists the variable is a placeholder for the current entry. With maps, variable_key and variable_value are substituted for the respective key-value-pair.
![]() | Note |
|---|---|
The return value of the last execution of expression determines the value of the whole foreach() statement. |
Example 8.23. foreach() Loop
{
// Exemplary foreach for list
// This yields 3
foreach(integer value, [1, 2, 3], { return value; });
// Exemplary foreach for map
// This yields 9
foreach(integer key, integer value, $[1:1, 2:4, 3:9], {
y2milestone("value: %1", value);
return value;
});
}Example 8.24. Sophisticated foreach() Loop
{
list <string> codes = ["X1", "D", "vT", "o", "T5h8"];
list <string> one_letter_codes = [];
list <string> other_codes = [];
foreach (string code, codes, {
// all one-letter codes
if (size(code) == 1) {
one_letter_codes = add (one_letter_codes, code);
// other ones
} else {
other_codes = add (other_codes, code);
}
});
// Results in: ["D", "o"]
y2milestone("One-letter codes: %1", one_letter_codes);
// Results in: ["X1", "vT", "T5h8"]
y2milestone("Other codes: %1", other_codes);
}
Synopsis: map<type1, type2> listmap(
type3 variable, list<type3>, { expression returning
map<type1, type2> }
);
This statement is a means to process the content of list in a sequential manner. It establishes an implicit loop over all entries in list thereby executing the given expression with the respective entry. During execution variable is a placeholder for the current entry. For each element in list the expression is evaluated in a new context. The result of each evaluation MUST be a map with a single pair of key-value. All the returned key-value-pairs are assembled to form the new map that is returned.
Example 8.25. listmap() statement
{
// This results in $[1:"xy", 2:"xy", 3:"xy"]
map <integer, string> m1 = listmap (integer s, [1, 2, 3], {
return $[s: "xy"]
});
// This results in $[11:2, 12:4, 13:6]
map <integer, integer> m2 = listmap (integer s, [1, 2, 3], {
integer a = s+10;
integer b = s*2;
list ret = [a, b];
return ret;
});
y2milestone("map 1: %1 - map 2: %2", m1, m2);
}
Synopsis (map): list<type1> maplist(
type2 key, type3 value, map<type2, type3>,
{ block returning type1 }
);
Synopsis (list): list<type1>
maplist(
type2 variable, list<type2>,
{ block returning type1 }
);
This statement is a means to process the content of map or list in a sequential manner. It establishes an implicit loop over all entries in map or list thereby executing the given expression with the respective entries. With lists the variable is a placeholder for the current entry. With maps, key and value are substituted for the respective key-value-pair. For each element the expression is evaluated in a new context. All return values are assembled to form the new list that is returned.
![]() | Note |
|---|---|
To exit the loop before it ends, use the break. See the usage in the break statement description. To skip to the next loop step, use the continue. See the usage in the continue statement description. |
Example 8.26. maplist() statement
{
// This results in [2, 4, 6]
list<integer> l1 = maplist (integer s, [1, 2, 3], {
reuturn s*2;
});
// This results in [2, 6, 12]
list<integer> l2 = maplist (integer k, integer v, $[1:2, 2:3, 3:4], {
return k*v;
});
y2milestone("list 1: %1 - list 2: %2", l1, l2);
}
Synopsis: map<type1, type2> mapmap(
type3 key, type4 value, map<type3, type4>,
{ expression returning map<type1, type2> }
);
This statement is a means to process the content of map in a sequential manner. It establishes an implicit loop over all entries in map thereby executing the given expression with key and value substituted for the respective key-value-pair. For each map element the expression is evaluated in a new context. The result of each evaluation MUST be a map with a single pair of key-value. All the returned key-value-pairs are assembled to form the new map that is returned.
Example 8.27. mapmap() statement
{
// This results in $[11:"ax", 12:"bx"]
map<integer,string> m = mapmap (integer k, string v, $[1:"a", 2:"b"], {
return [k+10, v+"x"];
});
y2milestone("map: %1", m);
}
In the previous sections we have already seen some YCP code that
dealt with the creation and handling of on-screen dialogs. These
examples were rather simple to show the basic
strategy of creating dialogs. Of course designing “real”
dialogs that do useful things is is bit more complicated and requires a
rather good knowledge of the instruments provided by the UI.
Because the UI has been designed to be most flexible, the possibilities
for creating and managing dialogs are quite versatile. Consequently the
instruments for doing this are rather diverse. In fact the UI extends
the basic YCP language to a large extent, thereby providing the means
to create and manage on-screen dialogs.
For more information about the User Interface, handling events see the Layout HOWTO.
In the previous section the basic mechanisms suitable for managing
on-screen dialogs were presented. However, creating dialogs for each and
every application from scratch would be cumbersome and moreover is
unnecessary. For example the well-known layout that is presented with
nearly every YaST dialog during installation was not
programmed anew for each dialog. Rather it is kind
of imported from a special YCP module, the Wizard
that provides all the functionality necessary to create uniform
dialogs.
Furthermore, as time went by, during the development of the YaST
installer, the developers encountered situations where the same (or
similar) tasks ofttimes had to be accomplished at different locations
in the overall program flow. For example opening a popup to ask the
user a question with the predefined buttons “Yes” and
“No” is a procedure used very often.
This led to the development of predefined dialog elements that can (and
should) be included in the current YCP source. Displaying the
Yes-No-popup from the example above is then reduced to calling a
function with respective parameters. Aside from avoiding the need to
redevelop such things again and again, another benefit is the ever same
visual appearance that adds up to the well-known YaST look-and-feel.
Meanwhile there are also many functions that are not UI-related but
nonetheless very useful.
The omnium gatherum of all these elements has been collected to form
the so-called “YaST Wizard”. In short the YaST Wizard
consists of one YCP module that provides the layout framework used in
the installation dialogs and some additional YCP modules that provide
access to several common dialog elements needed rather often. Many
“generic” functions are at hand as well.
The following two sections cover these topics mostly by means of
references to the YaST developers documentation.
In the previous section you got to know how to do a
YaST program. Well, normally YCP-scripts are
executed involving the whole YaST-machinery, e.g. during
installation, which requires correct embedding of the script into the
surrounding YCP environment. Fortunately there is a way to let run
YCP-scripts isolated, i.e. stand-alone.
To do so we make use of the architectural separation of components
featured by YaST. The “command line version” of YaST
is called y2base and can usually be found in
/usr/lib/YaST2/bin. You could set the
PATH to include this location to avoid typing in the
full path every time.
$> y2base -h
Usage: y2base [LogOpts] Client [ClientOpts] Server [Generic ServerOpts] [Specific ServerOpts]
LogOptions are:
-l | --logfile LogFile : Set logfile
ClientOptions are:
-s : Get options as one YCPList from stdin
-f FileName : Get YCPValue(s) from file
'(any YCPValue)' : Parameter _IS_ a YCPValue
Generic ServerOptions are:
-p FileName : Evaluate YCPValue(s) from file (preload)
'(any YCPValue)' : Parameter _IS_ a YCPValue to be evaluated
Specific ServerOptions are any options passed on unevaluated.
Examples:
y2base installation qt
Start binary y2base with intallation.ycp as client and qt as server
y2base installation '("test")' qt
Provide YCPValue '"test"' as parameter for client installation
y2base installation qt -geometry 800x600
Provide geometry information as specific server options
This help page, showing the possible options in a call of
y2base, is rather self-explaining. For the moment
the interesting parameters are Client and
Server. In Section 2.4, “External Programs” we learned that YaST consists
of several modules, some of them being
client-components and some others being
server-components. By invoking YaST in the way
displayed above we can connect any client-component with any
server-component.
Because a YCP-program (also called YCP-module) can act as a
client-component, it is possible to connect it with a server-component
suitable of executing it. Since our “Hello,
World!”-program displays something on screen, we need to use
the UI as server-component in this case. As already said the UI is able
to use a text-based console environment as well as a graphical X11
environment which leads to the following two methods of running a
YCP-script.
y2base file.ycp qt
This will execute file.ycp in the graphical
Qt-UI.
y2base file.ycp ncurses
This will execute file.ycp in the text-based
NCurses-UI.
Table of Contents
List of Figures
List of Tables
List of Examples
In the introductory chapter we have already heard something about
accessing the system with SCR. Because manipulating the system at the
lowest layer is all YaST is about, we now want to take a closer look at
this topic.
Basically the SCR creates a consistent view of the system hardware and its configuration files. There are many dependencies between the different entities among those data and these dependencies have to be taken into consideration when manipulating them. For this being possible in a convenient way for the higher-level modules there must be an easy and consistent accessing method. This method is provided by the SCR as it presents kind of an abstraction of the various types of data to be handled.
Now the data “landscape” that must be covered here is rather heterogeneous. Hardware data and configuration data, both of most multifaceted type can hardly be handled by one single monolithic program. Therefore the SCR consists of a “head” that is accompanied by various helper programs, the so-called agents, each of them being specialized on a specific task.
For each category of system data there is a corresponding SCR-agent.
Their job is to map the real system data to YCP-data structures so
that YCP-modules can access them in a convenient way. In fact the
SCR-agents provide the YCP data structures.
They come into existence with the presence of an SCR-agent that
provides them. Otherwise they wouldn't be there.
For example there is an agent that reads and writes the
/etc/sysconfig files. The YCP-representation of a
sysconfig-variable is a single YCP-string. When
reading, the agent reads the variable in the corresponding file and
creates a YCP-string from it. When writing, the agent gets the new
value as YCP-string and changes the variable in the corresponding file
accordingly.
It must be said here, that the set of agents may change over time. New agents may be created in the future and other ones might be abandoned if their functionality is obsolete or taken over by another agent. Generally this is no problem because for module development it is not (and should not be) necessary to know exactly which agent does what. As already said, the SCR provides an abstraction of the data to be handled and this abstraction comes into being in form of a tree, the SCR-tree.
As a computer's hardware and software configuration is quite complex, the SCR organizes all data in form of a tree. It resembles very much a file system with its folders, sub-folders and files whereby the tree structure reflects the thematic separation of the various configuration categories.
The SCR-tree consists of two different kinds of nodes:
Table 2.1. SCR Node Types
| Data nodes |
Data nodes represent single pieces of data, for example a
sysconfig-entry or a mountpoint of a
file system in /etc/fstab. They are the
leaves of the tree and stand for actual
data to be handled.
|
| Map nodes | Map nodes allow for navigation to the leaves just like the path components in the directory structure of a file system. This way map nodes are used to structure the data in a suitable manner. |
The names of the nodes in the SCR-tree can be concatenated resulting in
the creation of an SCR-path. An SCR-path is a
description were to find a node in the SCR-tree. It is a sequence of
path components each of them being a string. As we have seen in the
section Data type path YCP-paths are prepended by
dots (.) which act as separators in compound paths. So
.foo.bar is a valid YCP-path. If
bar is an SCR data node, then
SCR::Read(.foo.bar) would render some data. If
bar is a map node, then
SCR::Dir(.foo.bar) would reveal the immediate
sub-nodes in the SCR-tree, e.g. ["big", "brown",
"fox"]. The single dot (.) is also valid and denotes the
root of the whole SCR-tree. Consequently SCR::Dir(.)
will return a list of all the top-nodes in the SCR-tree.
In the figure below we see a (very small) cut-out of the SCR-tree that is related to hardware-specific information.
The light gray nodes are SCR-map-nodes denoting the path to the data.
They can be used with SCR::Dir(...) to find out what
is below. So in the figure above SCR::Dir(.probe)
would return a list as [..."has_smp", "boot_arch",
"has_apm"...].
The dark grey nodes are SCR-data-nodes that stand for the actual data.
What can be done with them depends on the actual node (reading,
writing, executing), but usually SCR::Read(...) is
possible. As is shown above
SCR::Read(.probe.boot_arch) would return
"grub".
Now that we know how the SCR-landscape can be navigated we will take a look at how the data that is dug in there can be accessed. The accessing methods already implied above shall now be defined more precisely.
There are four accessing methods.
Reading
We speak of reading, when the agent reads some configuration file or scans the system hardware and produces a YCP data structure representing this information.
Writing
We speak of writing, when the agent gets some YCP data structure and creates or modifies some system configuration file according to these data.
Executing
We speak of executing, when the agent gets
some YCP data that can be interpreted as instruction and executes
it. Usually this is being done by means of another program, e.g.
the bash.
Dir
Compared to the other accessing methods listed above, the
Dir-command is somewhat special. It takes as
argument an SCR-path that points to a specific node in the
SCR-tree. It returns a list of all the sub-paths that are
immediately below this node. This way it works just like a
dir-command in a file system. For example if
you apply this to the root of the SCR-tree (.), the answer would be
a list with all the top nodes known by the SCR , e.g.
["audio", ... , "yast2"].
[1]
As a rule all SCR-agents implement some of the four accessing methods listed above. However depending on the task the agent was made for, not all of them may be provided.
For convenient use from YCP the accessing methods are realized by
means of an API, i.e. a defined set of
YCP-functions that are understood. You can call these functions from
YCP if you prepend the commands with the name space identifier
SCR:: which causes redirection to the SCR.
Table 3.1. The SCR-commands
| Function | What it does |
|---|---|
Read(path p) -> any
|
Reads the data represented by the node at path
p. The value returned can be any YCP data
type but it is always one single value.
|
Write(path p, any v) -> boolean
|
Writes the value v to the node at path
p. The boolean return value is
true on success. On error the return value
is false and a log entry is generated in the
log file. Reasons for errors can be a mistyped value
v or some problem with the periphery that
lies behind the data-node.
|
Execute(path p) -> boolean
|
This command is mostly used with the
system-agent (see Chapter 6, Useful SCR Agents). Usually the return value
indicates success or failure of the executed command.
|
Dir(path p) -> list(string)
|
Returns a list of all subtree nodes immediately below the node
p. For each such node the list contains a
string denoting its name. If p does not
point to a map node, i.e. the last path component is a leaf,
the command will return an empty list or
nil.
|
[1]
Unfortunately not all SCR-agents do support this command
properly. There may be agents that wrongly return an empty list
or even nil when queried this way.
In the last section we saw some examples of how the SCR can be used
from YCP. However if you only want to test or explore different
SCR-paths, writing a YCP-script for every access can be cumbersome.
Fortunately the SCR-component can be run individually on the command
line of a terminal using a method very similar to the one we saw in
Section 1.4, “Advanced YaST2 command line parsing”.
In contrast to the method demonstrated there, this time we don't feed
a YCP-script into YaST. Instead we make another use of the
architectural separation of components featured by YaST in that we
connect the so-called stdio-component with the
SCR-component. By doing so we can feed everything
we type on the command line into the SCR.
However, because of the “raw” nature of the
YaST-internal communication paths, this method is not very
comfortable. You can't correct typos with
Backspace or Del here (the
SCR is not meant to be operated in this way). By
doing so we kind of “simulate” YaST-internal
communication which normally forecloses any misspelling.
Furthermore, if you play around with the SCR in this manner you will be able to initiate privileged actions only if you are running the commands under the root-account.
![]() | Caution |
|---|---|
If you run “manual” SCR-commands under the root-account, the SCR will “gracefully” fulfill all your wishes. So be careful with Write and Execute!!! |
Now operating the SCR this way can be shown best with some examples.
Example 5.1. Operating the SCR from the command line
$> /usr/lib/YaST2/bin/y2base stdio scr
([])
`Read(.probe.boot_arch)
("grub")
`Read(.probe.version)
("Oct 7 2002, 15:05:08")
`Read(.probe.has_smp)
(false)
`Read(.probe.has_apm)
(true)
`Read(.probe.boot_arch)
(nil)
As is shown above, the command y2base stdio scr
starts YaST in a specific way. It connects the YaST
client-component stdio with the server-component
scr. After that the SCR is running and awaits any
input on stdio which in this case is the console. To explore the
content of the SCR-tree you can now enter any SCR-commands just as you
would do in YCP. The only difference is the absence of the
SCR:: name space identifier which must not be given
here as can be seen in the last line.
The SCR-world knows many agents for all sorts of tasks. Unfortunately this matter is subject to a rather high change service and not (yet) well documented. Therefore it is not easily possible to explain the details in a manner of “Which agent provides which paths for what reason?”. As a result only the most helpful agents are mentioned here along with references to the respective developers documentation.
Please note that even the developers documentation might be outdated to
some extent. Consequently the most reliable source of information are
the “real” files below
/usr/share/YaST2/.
System Agent
This agent realizes access to the target system during installation.
/usr/share/doc/packages/yast2-core/agent-system/ag_system-builtins.html /usr/share/doc/packages/yast2-core/agent-system/ag_system-builtins.html
Background Agent
This agent runs shell commands in the background.
/usr/share/doc/packages/yast2-core/agents-perl/ag_background.html /usr/share/doc/packages/yast2-core/agents-perl/ag_background.html
Hardware Probe Agent
The agent being responsible for hardware probing.
/usr/share/doc/packages/yast2-core/agent-probe/hwprobe.html /usr/share/doc/packages/yast2-core/agent-probe/hwprobe.html
Any-Agent
This agent handles the access to configuration files of (almost) arbitrary syntax. The syntax to be understood must be specified in a configuration file.
/usr/share/doc/packages/yast2-core/agent-any/anyagent.html /usr/share/doc/packages/yast2-core/agent-any/anyagent.html
Ini-Agent
The Ini-agent is suitable for accessing configuration files with the well-known ini-file syntax.
/usr/share/doc/packages/yast2-core/agent-ini/ini.html /usr/share/doc/packages/yast2-core/agent-ini/ini.html
Modules Agent
This agent is the interface to the
/etc/modules.conf file.
/usr/share/doc/packages/yast2-core/agent-modules/modules.html /usr/share/doc/packages/yast2-core/agent-modules/modules.html
Perl Agent
This agent is a means to call Perl scripts from within YCP.
/usr/share/doc/packages/yast2-core/agents-perl/ycp-pm.html file:///usr/share/doc/packages/yast2-core/agents-perl/ycp-pm.html
Table of Contents
Creating modules for YaST means extending its functionality. For this
being possible it is necessary to follow the infrastructural and
functional particularities of YaST as well as some guidelines regarding
the interaction of the module with the user and the rest of the system.
In the following we'll have a closer look at these topics .
Throughout this document the term “YCP module” was
mentioned repeatedly without providing a sharp definition. In fact the
term “module” is used quite loosely in the YaST world,
because there are several kinds of modules
involved in different contexts. The following text shall lighten
this topic.
Different kinds of YCP modules
Generic YCP modules
In principle every YCP file that provides a distinct functionality
can be seen as a module. Typical representatives of this kind of
module are the inst_xxx.ycp files that are part
of the YaST installer. Modules of this kind mostly represent
rather self-contained functionality, e.g like
inst_keyboard.ycp that provides the user dialog
for selecting a keyboard during installation. These modules are
usually called via CallFunction().
Library modules
This kind of module can be seen as what is called a library in
other programming languages. Usually these modules are a collection
of functions that must be included to be used.
As with other programming languages, including
in YCP means merely text insertion that takes place each and every
time an include is stated. This is often
adverse with respect to speed and memory consumption.
True YCP modules
This kind of module is the most interesting one. True modules represent an “object oriented” approach to module design. Because the mechanisms associated with them deserve some special mention, the next section will cover this topic in more detail.
Table of Contents
True modules are rather new in the YaST world and it is planned that
they will replace the old method of including
modules completely (with exception of some rare cases perhaps). The
following sections will outline the differences between these concepts.
YCP, originally planned as a functional language, always did dynamic
(i.e. runtime) binding of variables. Although useful in many cases,
it's quite puzzling for someone used to “imperative”
languages. So you could well program the following block and get an
unexpected result.
{
integer x = 42;
define f() ``{ return x; }
... // lots of lines
x = 55;
return f(); // will return 55 because of runtime binding of x!
}
Another widely misused feature is to include global definitions.
While there was no alternative as long as include
was the only referencing instrument, this is certainly not a good
programming practice in view of speed and memory considerations.
In contrast to included modules, true modules have some distinct properties that are shown in the list below.
Definition-time bindings
Definitions are evaluated in the sequence of the program flow.
One-time inclusion
In contrast to include the
import statement includes the module only once
even if there are more than one import
statement in the program flow. Later imports are silently ignored.
Proprietary global namespace
The module definition implies a module declaration that determines the namespace of the module's global variable scope.
Local environment
Aside from the data located in the module's global namespace all other data defined in the module is purely local, i.e. is invisible from the outside.
Module constructor function
Each true module may have a constructor function that is automatically executed upon first import.
The following listing is a brief sample of a true module.
{
// This is a module called "Sample".
// Therefore the file name MUST be Sample.ycp
// The "module" statement makes the module accessible for 'import'.
//
module "Sample";
// This is a local declaration.
// It can only be 'seen' inside the module.
//
integer local_var = 42;
// This is a global declaration.
// It can be accessed from outside with the name space identifier 'Sample::'.
//
global integer global_var = 27;
// This is a global function.
// It has access to global_var *and* local_var.
//
global define sample_f () ``{ return local_var + global_var; }
}
The module above can be used with the import
statement. The syntax for file inclusion with
import is similar to include.
The interpreter automatically appends “.ycp” to the
filename and searches below
/usr/lib/YaST2/modules. If the filename starts
with “./”, the file is loaded from the local directory.
The global declarations of the module can then be accessed with the
name space identifier Sample::.
![]() | Note |
|---|---|
The file name must match the module declaration! Inside modules, only variable or function declarations are allowed. Stand-alone blocks or any kind of evaluation statements are forbidden. |
{
// This imports the 'Sample'-module.
//
import "Sample";
// The global function is called with the respective name space identifier.
//
integer i = Sample::sample_f(); // == 69
// No access to local module variables.
//
i = Sample::local_var; // ERROR, no access possible !
// No problem with global variables.
//
i = Sample::global_var; // == 27
Sample::global_var = 0; // This variable is writable !!
return Sample::sample_f(); // == 42, since global_var is 0
}
![]() | Note |
|---|---|
The first encounter of the statement |
If a global function with the same name as the module is defined, it is treated as a constructor. The constructor is called after the module has been loaded and evaluated for the first time. Because of this the constructor could (and should) be defined at the beginning of the module. Despite being located “on top” it can make use of the functions declared later in the file.
Module constructors are used mostly for initialization purposes, e.g. for setting local variables to proper values. However, the actions within a constructor can be arbitrarily complex.
![]() | Note |
|---|---|
Constructors can't have any arguments. The result of calling a constructor from the outside is ignored. |
{
// This is a module called "Class" with a constructor function.
//
module "Class";
// A globally accessible variable.
//
global integer class_var = 42;
// This is the constructor (same name as the module).
//
global define Class() ``{ class_var = 12345; }
}
{
// The usage of the "Class"-module.
//
import "Class";
return Class::class_var; // will be 12345 !
}
Table of Contents
Most often when a YaST module shall be created, this module will have
some interaction with the user. This usually implies the creation of
dialogs to be displayed on screen. As you might have noticed the
dialogs that come ready-made with YaST follow a distinct “look
and feel” which is due to the fact that the YaST developers
follow some rules regarding the visual appearance as well as the
functional behavior of a dialog. The keywords here are usability and
GUI-consistency.
When it comes to user-interaction one concept that is stressed very often is usability or - more speaking - user-friendliness. If you have ever heard s.th. about ergonomics you may also know the term Human Computer Interaction (HCI). For us regular folks usability is probably the best notation because it best summarizes what's it all about. It means that the program in question is good “usable” by the user. In general that means that operating a screen dialog should enjoin as low a burden as possible on the user.
In order to have a good usability a system should satisfy the following criteria:
Users must be able to accomplish their goal with minimal effort and maximum results.
The system must not treat the user in a hostile fashion or treat the user as if they do not matter.
The system can not crash or produce any unexpected results at any point in the process.
There must be constraints on the users actions.
Users should not suffer from information overload.
The system must be consistent at every point in the process.
The system must always provide feedback to the user so that they know and understand what is happening at every point in the process.
![]() | Important |
|---|---|
If you want to create an interactive |
All that said above in essence is an outline from a very good article by Todd Burgess. If you are interested in a more elaborate discussion of usability you may have a look at http://www.osOpinion.com/Opinions/ToddBurgess/ToddBurgess1.html
Table of Contents
FIXME: To be done...
Table of Contents
@deprecated@descr@example@param@ref@return@short@since@stable@struct@unstableList of Examples
A lot of YaST documentation is generated from the source code. That's the reason why we should follow some rules when documenting the source code (especially functions and variables) to have the possibility of auto-generated documentation.
Why should we have it? It's not needed to create your own modules, functions or agents when such things alread exist. You can just use something that already exist. But the most important information is that it exists, where it exists and how to use it. All these pieces of information can be in the auto-generated documentation.
Additionally, if you develop some universal module with, e.g., functionality offering abstraction layer for reading and writing records from and into the OpenLDAP server, you should document the functionality itself to offer it to others.
Table of Contents
@deprecated@descr@example@param@ref@return@short@since@stable@struct@unstableMost of the modules and clients are written in the YCP programming language.
Although YCP itself offers more possibilities howto write comments, there are some quite strict rules that needs to be followed. Don't be afraid, they are easy to remember.
Every YaST package, you have installed on your system, has its
auto-generated HTML-based documentation in the
/usr/share/doc/packages/_package_name_/.../autodocs/
directory. This documentation is generated by the
ycpdoc script — part of
the yast2-devtools package.
![]() | Important |
|---|---|
To recognize the comment that should be processed for the autodocs, ycpdoc needs this syntax: /** * * slash and two asterisks identify the comment * for file headers, functions and variables * */
/*** * * slash and three asterisks identify the initial comment * of file (intro) * */ If you don't use two (or three) asterisks, the comment doesn't get processed at all. |
Here you can see all available options and arguments of the ycpdoc script. This is the /usr/lib/YaST2/bin/ycpdoc --help command output:
Usage:
ycpdoc -h|--help|--man
ycpdoc [-d <dir>] [-s <dir>] [-f html|xml] [-i] [-] [-o] [-wr] files.ycp...
Options and Arguments:
-h Show this help screen
-d dir, --outputdir=dir
Output files are placed to directory dir
-s dir, --strip=dir
Strip only dir when generating output files (default all). Remaining
slashes are converted to double underscores.
-f format, --format=format
Produce output in given format, html or xml. The option may be
repeated. HTML is the default. XML produces ycpdoc.xml, the DTD is
not stable yet.
-O file, --xml-output=file
For xml output, write to file. The default is "ycpdoc.xml", not
respecting outputdir.
-i, --noindex
Do not generate index.html (intro.html, files.html)
- Write output to stdout. Do not generate indexes. If there are more
input files, generate only one output html file
-o, --oldindex
Old style of index: creates only index.html
-wr Do not warn about undeclared return types
--state
For debugging, prints the parsing state for each line.
This is the example of generated HTML-based documentation. Run the
command /usr/lib/YaST2/bin/ycpdoc
/usr/share/YaST2/modules/FileUtils.ycp and open the just
generated HTML files in some HTML browser. First of all, see
the one that contains FileUtils.html
in its name.
You can see all global functions and variables documented there. Functions also with their parameters, return types, examples, etc.
This example of generated XML-based documentation. Run the
command /usr/lib/YaST2/bin/ycpdoc --format=xml
/usr/share/YaST2/modules/FileUtils.ycp and see the just
generated ycpdoc.xml XML file.
![]() | Note |
|---|---|
XML is not for humans, it's optimized for computers. That's also why the XML output of ycpdoc doesn't so look nice but it's very valuable for text-processing tools. |
Example 1.1. Examplary XML output reformated and shortened by hand
<?xml version="1.0" encoding="UTF-8"?>
<ycpdoc>
<files>
<file_item key="usr/share/YaST2/modules/FileUtils.ycp">
<header>
<authors>
<ITEM>author</ITEM>
</authors>
<file>modules/FileUtils.ycp</file>
<module>YaST2</module>
<summary>summary</summary>
</header>
<provides>
<provides_item>
<descr></descr>
<example_file></example_file>
<file>usr/share/YaST2/modules/FileUtils.ycp</file>
<global>1</global>
<kind>function</kind>
<name>Exists</name>
<parameters>
<parameters_item>
<name>target</name>
<type>string</type>
</parameters_item>
</parameters>
<return>true if exists</return>
<scruple></scruple>
<see></see>
<short>short description</short>
<type>boolean</type>
</provides_item>
...
</provides>
<requires>
<requires_item>
<kind>import</kind>
<name>SCR</name>
</requires_item>
...
</requires>
</file_item>
</files>
</ycpdoc>
The file header should show general information about the file name and location, author, and a general purpose.
Example 1.2. Exemplary file header
{
/**
* File: modules/CRONConfig.ycp
* Package: General YaST Modules
* Authors: John The Fish <john@thesmallfish.net>
* Flags: Stable
*
* $Id: $
*
* This module offers to read, change and write the current
* CRON setup for all system users.
*/
module "CRONConfig";
...
}File header format:
Attribute: value
Possible Attributes are:
Authors
Author
Depends
File
Flags
Module
Package
Summary
Defines the author(s) of the file.
Example 1.4. Multi-line Authors tag
Authors: Wendy Graceful <grace@example.com>
Lars Kralewski <lars@example.com>What does this file / module depend on.
Relative path where is this file located under the base YaST
/usr/share/YaST2/ path.
Defines the default value for the module stability (read: stability of module API).
Possible values are:
Stable
Unstable
Textual description of the package (YaST module) that this file belongs to. This tag is obsolete.
Defines the purpose of this file, such as what does do, how it does that, what it uses for that, etc.
Example 1.9. Multi-line Summary tag
Summary: This is a multiline summary for the exemplary YaST module
that does nothing and has no valuable content at all.Summary tag is something special —. You don't need to define the tag itself, just write a multiline text after at the bottom of the comment after the $Id: $ entry.
Example 1.10. Other Multi-line Summary tag
/** * File: modules/DoNothingAtAll.ycp * * ... * * $Id: $ * * This is a multiline summary for the exemplary YaST module * that does nothing and has no valuable content at all. */
The function header should describe all needed pieces of information about the function. Such as parameters, return value, function's textual description, examples, etc.
Example 1.11. Simple Example of Function Description
/**
* Returns number of just connected users to the server.
* You can filter these users by defining the filtering parameter.
*
* @param part_of_ip ("192.168.") that users are connected from
* @return integer number of connected users
*
* @see AnotherFunction()
*/
global integer GetCountOfConnectedUsers (string part_of_ip) {
...
}Function description tags:
@deprecated
@descr
@example
@param
@ref
@return
@short
@since
@stable
@struct
@unstable
Defines that the function has been deprecated. Additionaly defines another function as the replacement.
Complete description of the function. It might describe the environment or detailed behavior.
Example 1.14. Automatic description
/** * This first line is identified as @short * * This multi-line @descr (after a newline) is automatically * taken as the function description. * * Additionally, this multi-line text is automatically taken as another paragraph * of the function @descr (the number of paragraphs is unlimited). */
Block of examples of usage, they are as exported as they are written (like <pre> in HTML).
Example 1.15. Multiline @example block
@example
list <string> servers = GetListOfNSServers("example.com.");
boolean success = AddNewNSServer("ns4.example.com.", "example.com.");
Describes the parameter of the function. The order
of @param tags is significant.
Example 1.16. @param tag
/**
* Adds a new NS Server into list of domain NS servers.
*
* @param new_ns_server FQDN
* @param domain
* @return boolean successfull
*/
global boolean AddNewNSServer (string new_ns_server, string domain) {
...
return success;
}
Single-line version of @see tag.
Description of the return value.
Example 1.18. @return tag
/**
* Removes the NS Server from list of domain NS servers.
*
* @param ns_server FQDN
* @param domain
* @return boolean successfull
*/
global boolean RemoveNSServer (string ns_server, string domain) {
...
boolean success = true;
return success;
}Short single-line description of the function.
Example 1.20. Automatic description
/** * This first line is identified as @short * * This multi-line @descr (after a newline) is automatically * taken as the function description. */
See the @descr tag.
Since which version thix function exists. This is not a commonly used tag.
Defines that the function has a stable API and we don't plan to change it for years.
See tag @unstable.
This tag might changes the default value of the file's stability of the API (just for the current function).
This tag is used to represent the used data-structure closer. It's as represented as it's written (such as the <pre> HTML tag).
Example 1.23. @struct tag
@struct returns $[
// example.com.
"domain" : "domain name",
// list of NS servers assigned to the domain
"ns_servers" : [ "ns1", "ns2", ... ],
// map of MX servers $[ "server_name" : priority ]
"mx_servers" : $[ "mx1" : 10, "mx2" : 5 ],
]
The YCP variable description can contain almost all pieces
of information as the YCP
function
description
except @param and @return
tags that don't make sense here. You can, for instance, describe
the variable's textual description, examples, structures, etc.
However, it's usual that only the description is given.
Example 1.25. Simple Example of Variable Description
/** * Defines the number of connected users for each IP. * There might be more connected users from one IP. * * @struct $[ * "192.168.0.1" : 5, * "192.168.0.12" : 1, * "192.168.0.35" : 3, * ] * * @see GetNumberOfConnectedUsers() */ global map <string, integer> number_of_connected_users = nil;
Example 1.26. The Most Usual Usage
/** * Stores the currently selected domain */ global string current_domain = "";
Function description tags:
@deprecated
@descr
@example
@ref
@short
@since
@stable
@struct
@unstable
See the YCP function description for usage and parameters of tags mentioned above.
The file header should show what is the SCR Agent good for, supported acces types (read/write/execute) and examples how to use it.
Example 3.1. Exemplary SCR Agent Definition
/**
* File:
* sshd.scr
* Summary:
* SCR Agent for reading/writing /etc/ssh/sshd_config
* using the ini-agent
* Access:
* read/write
* Authors:
* John The Fish <john@thesmallfish.net>
* Example:
* Dir(.sshd)
* (["Port", "X11Forwarding", "Compression", "MaxAuthTries", "PermitRootLogin"])
*
* Read(.sshd.Compression)
* (["yes"])
*
* Write(.sshd.Compression, "no")
* (true)
*
* $Id: sshd.scr 11113 2005-10-20 14:15:16Z jtf $
*
* Fore more information about possible keys and values
* consult with the sshd_config man pages `man sshd_config`.
*/
.sshd
`ag_ini(
...
)Table of Contents
List of Examples
Table of Contents
This is a set of basic routines for manipulating packages.
The module Package is the module for handling package installation , it works on the target system and correctly differentiates between normal and autoinstallaton mode.
PackageSystem is for situations when you need immediate action (for example to start the client).
Package
PackageSystem
The function names should be self-descriptive, so there are no comments here. Feel free to ask if you are in doubts. (FIXME)
boolean Package::InstallMsg(<string> package, <string> message);
boolean Package::InstallAllMsg(list<string> packages, <string> message);
boolean Package::InstallAnyMsg(list<string> packages, <string> message);
boolean Package::RemoveMsg(<string> package, <string> message);
boolean Package::RemoveAllMsg(list<string> packages, <string> message);
![]() | Note |
|---|---|
GUI based, do not install if Mode::config is defined, only in PackageSystem) |
This is a short introduction into using the package manager interface from within YCP code.
It describes the required Pkg calls and how to use them in actual code.
The new package manager does all package related handling inside C++ code and uses callbacks for user interaction.
This reduces the amount of ycp code needed for package installation (or removal) to a few lines.
The package manager keeps a list of installed (on the system) and available (on the CD or other installation media) packages and their status (install, delete, no change). YCP code can query the package manager for installed or available packages and set their status. The actual package installation or deletion takes place with a final commit call.
This call goes through the package list and deletes all packages marked as such. Then it orderes the packages marked for installation according to their "PreRequires" rpm tag and the media number (so you usually install CDs in ascending order).
All user interaction is done via callbacks from inside the package manager. If no callback is defined, the user doesn't see anything and also isn't requested to change CDs. Especially the latter case makes it important to set up appropriate callbacks.
Pkg::IsProvided
checks if a rpm tag (a package name or any other symbolic name provided
by a package) is installed.
It does a 'rpm -q' and if this fails a 'rpm -q --whatprovides' for the given tag.
It does not start the complete package manager.
Pkg::IsAvailable
checks if a rpm package is available for installation.
(It currently does not check for other tags provided, this will be
implemented in a future version.)
It starts the package manager if it isn't running already. Starting the package manager may take some time since it must read all it's caches about the installation sources and set up the internal data structures.
Pkg::PkgDelete
marks an installed package for deletion. It does not
immediately run 'rpm -e' but just sets the internal
status of the package in the package manager.
The actual deletion must be triggered with Pkg::PkgCommit().
Pkg::PkgInstall
marks an available package for installation.
It does not immediately run
'rpm -Uhv' but just sets the internal status
of the package in the package manager.
The actual installation must be triggered with Pkg::PkgCommit().
Pkg::PkgSolve()
tries to solve open package dependencies and marks other (dependant)
packages for installation/deletion.
(It currently does an automatic solving without user interaction. Callbacks for user interaction will be added later.)
Pkg::PkgCommit
does the 'rpm' calls needed to install or delete
packages.
It sorts the packages according to their pre-requires and their location on the media (CD order), handles mouting/unmounting of the media, does the package download (if the media is an ftp or http server), and calls rpm.
All these actions might trigger callbacks for user information or user actions (i.e. media change).
The module PackageCallbacks from yast2-packager.rpm defined default callbacks for all these actions and should be imported by any code using Pkg::PkgCommit(0).
The integer parameter given to Pkg::PkgCommit(0) must be zero for the normal usage. Non-zero values are only allowed during system installation when no media change is possible.
{
import "PackageCallbacks";
// installing netscape and cups
Pkg::PkgInstall ("netscape");
Pkg::PkgInstall ("cups");
// deleting lprng if it is installed
if (Pkg::IsProvided ("lprng"))
Pkg::PkgDelete ("lprng");
// fill open dependencies
Pkg::PkgSolve();
// do the rpm calls
Pkg::PkgCommit();
}
The module Popup.ycp contains commonly used popup dialogs
for general usage. Use those dialogs rather than creating your own custom
dialogs whenever possible.
If you have your own definitions of equivalent popups (which is very likely),
please remove them as soon as possible and use the popups from
Popup.ycp. The new popups usually require fewer
parameters, e.g., you no longer need to explicitly pass standard button
labels like Yes or No etc.
as parameters (we had to do that because of technical limitations with the
translator module, but we found a workaround for that).
There are several versions for each type of popup, a simple version with a minimum number of parameters and one or more "expert" versions where you can pass lots of parameters to fine-tune many details.
Use the simplest version one whenever you can. It's normally the version that makes most sense for most cases.
If you use an expert version, try to stick to the default behaviour (i.e. the behaviour of the simple version) as closely as possible. Change only parameters you really need to change. In particular, only change the default button for very good reasons.
If there is a specialized simple version, use it whenever you can.
For example, use Popup::Warning
rather than Popup::AnyMessage
with the same message if you want to display a warning. This way we can make
sure all warnings look the same and make them easily recognizable
as warnings.
Sense or nonsense of providing headlines for each popup is a cause for endless discussion - we've been through that. Sometimes headlines make sense, sometimes they don't.
As a general rule of thumb, don't provide a headline that is more or less the same as the message itself, i.e. don't

// Example how NOT to use popups:
//
// - The headline is redundant
//
// - The text is too verbose
//
// - The text contains a reference to "YaST2"
// (the user shouldn't need to know what that is)
{
import "Popup";
boolean answer = Popup::YesNoHeadline( "Create Backup?", // superfluous headline
"Should YaST2 create a backup of the configuration files?" );
}
this is plainly redundant. This could be done much better like this:

// Improved version of ask_create_backup_bad.ycp:
//
// Note there is no superfluous headline any more,
// the text is concise, and there is no more reference
// to "YaST2" (a user shouldn't need to know what that is)
{
import "Popup";
boolean answer = Popup::YesNo( "Create a backup of the configuration files?" );
}
i.e. concise question, no lyrics around it, clear, plain and simple.
If you need to provide more background information to the users so they can understand what tragedy could befall their machine should they chose either alternative, a headline makes perfect sense for the more experienced user who gets to this kind of question quite frequently:

// Example when to use a headline:
//
// There is lengthy text that advanced users might have read several times
// before, so we give him a concise headline that identifies that dialog so he
// can keep on working without having to read everything again.
{
import "Popup";
string long_text =
"Resizing the windows partition works well in most cases,\n"
+ "but there are pathological cases where this might fail.\n"
+ "\n"
+ "You might lose all data on that disk. So please make sure\n"
+ "you have an up-to-date backup of all relevant data\n"
+ "for disaster recovery.\n"
+ "\n"
+ "If you are unsure, it might be a good idea to abort the installation\n"
+ "right now and make a backup."
;
boolean answer = Popup::YesNoHeadline( "Resize Windows Partition?", long_text );
}
![]() | Note |
|---|---|
This example might be a good candidate for ContinueCancel() - see below |
If you use headlines, please use them to either
classify the type of popup (Error, Warning, ...)
summarize the question.
Please DON'T use headlines like Question etc.
- that doesn't have any informative value, yet it forces the user
to read this useless headline.
For all those reasons, most popups come in a generic version where you can pass a headline ("Headline" is included in the names of those) and a simple version for general usage.
There are predefined messages for commonly used texts for the dialogs.
Use them when you use the expert version of a dialog
- don't pass your own messages if you can avoid it! Not only makes
this life easier for our translators (they need to translate stuff like
Cancel over and over again), it also gives us a chance
to provide consistent keyboard shortcuts throughout the entire program.
Example 1.1.
Use
`OpenDialog(
...
`HBox(
:-) `PushButton( `id(`ok ), `opt(`default), Label::OKButton() ),
`PushButton( `id(`retry ), Label::RetryButton() ),
`PushButton( `id(`cancel), Label::CancelButton() )
)
)Do not use
`OpenDialog(
...
`HBox(
:-( `PushButton( `id(`ok ), `opt(`default), _("&OK") ),
`PushButton( `id(`retry ), "&Retry" ),
`PushButton( `id(`cancel), "&Cancel" )
)
)- even whenever you create your own dialogs.
The first part of the name always is the message itself literally
Retry, the suffix indicates the type
Button to indicate whether or not it has a keyboard
shortcut. Currently we have Label::xxxButton (with
keyboard shortcut) and Label::xxxMsg (without shortcut).
For confirmation of possibly dangerous things, use Popup::ContinueCancel.
Only when there are two really distinct paths of decision use
Popup::YesNo.
And no, cancelling the entire process doesn't count here -
this is no equivalent decision.
The positive case (Yes or
Continue) is the default. If you don't like that,
use the generic Popup::AnyQuestion
directly and pass `focus_no for the focus parameter.
![]() | Important |
|---|---|
Remember: Only do that for very good reasons - i.e. when it's a really dangerous decision that typically results in loss of data that can't easily be restored. |
If you need to pass other button labels, think twice.
If you really need this, use Popup::AnyQuestion.
But NEVER use it so simply change the order of default buttons - i.e. NEVER create dialogs like this one:

// Bad Popup design: Default button order exchanged
//
// DON'T DO THIS!
{
import "Popup";
import "Label";
boolean dont_do_it =
Popup::AnyQuestion( Popup::NoHeadline(),
"Format hard disk?",
Label::CancelButton(),
Label::ContinueButton(),
`yes ); // initial focus - "Cancel" in this case
}

// Bad Popup design: Default button order exchanged
//
// DON'T DO THIS!
{
import "Popup";
import "Label";
boolean dont_do_it =
Popup::AnyQuestion( Popup::NoHeadline(),
"Show installation log?",
Label::NoButton(),
Label::YesButton(),
`no ); // button role reversed - "Yes"
}
simple specification in the YaST module
automatic help
automatic checking of arguments (types, format)
interactive session without UI
The aim of the module is to provide as automatic interface as possible for controlling the module. To support interactive sessions, the YaST module needs to provide command-handling loop similar to concept of event-handling in GUIs.
If the module does not need to do any special handling of the actions, it can use the "commandline" include (a wrapper for CommandLine). The include defines a single function "CommandLineRun()", which implements a standard event-loop and returns true on success. The module just needs to specify handlers for actions, user-interface, initialization and finishing.
Example 1.2. Simple CommandLine definition
{
define void deleteHandler( map options ) ``{
string dev = options["device"]:"";
CommandLine::Print("Deleting: "+dev);
if(Lan::Delete(dev) && Lan::Commit())
CommandLine::Print("Success");
else
CommandLine::Print("Error");
}
...
map cmdline = $[
"help" : "Configuration of network cards",
"id" : "lan",
"guihandler": ``(LanSequence()),
"initialize": ``(Lan::Read()),
"finish" : ``(Lan::Finish()),
"actions" : $[
"list" : $[
"help" : "display configuration summary",
"example" : "lan list configured",
"handler" : ``(listHandler())
],
"add" : $[
"help" : "add a network card",
"handler" : ``(addHandler())
],
"delete" : $[
"help" : "delete a network card",
"handler" : ``(deleteHandler())
]
],
...
];
import "Lan";
include "commandline/commandline.ycp";
CommandLineRun(cmdline);
/* EOF */
}
The UI handler is specified in the "guihandler" key of the command line description map. It must take no arguments and return boolean, true on success.
The initialize resp. finish handler is specified by the "initialize" resp. "finish" key of the description map. They do not take any arguments and must return boolean, true on success. Notice that "initialize" and "finish" handlers are not used if a user asked for GUI (guihandler is used instead and therefore it must do initializing and finishing on its own). The handler for an action is specified in the "handler" key of the action description map. Each handler must take a single argument containing the options entered by the user and return a boolean, true on success. If the handler returns "false", the command line will abort for non-interactive handling. This is useful for handling error states. However, a handler must use CommandLine::Abort() to stop event-handling in the interactive mode.
The CommandLine module is stateful, i.e., it contains a current state of command line parsing/interactive console control of a YaST module. Therefore, the CommandLineRun() handles the commands as follows:
standard UI start of a module - CommandLine::StartGUI will return true in this case
command given as an argument - the inner while loop will be done only once
interactive controling of a module - the while loop will be done as long as the user does not enter "exit" or "quit"
shows the help text for the command
starts interactive session without UI
shows the command-specific help
option to supress the progress messages
These are available in interactive mode only:
![]() | Note |
|---|---|
If the map does not follow the following rules, an error will be emitted into log. |
The map describing the command line interface of a module must contain "id" entry containing the name of the module. The map can contain global help text, a list of actions, a list of options and a list for mapping of options to actions (which optionscan be used for which actions). Each action and option must have its help text.
Actions is a map with the action name as a key. For each action, it isnecessary to provide the help text. Optionally, action can have defined an example of usage.
A list of flags can be specified to change the default behavior of the parameter checker. It is a list with key "options". Currently known flags:
for unknown parameters, do not check their validity can be used for passing unknown options into the handler
Example 1.3. Actions map definition
"actions" : $[
"list" : $[
"help": "display configuration summary",
"example": "lan list configured",
"options": [ "non_strict" ]
],
"add" : $[
"help": "add a network card" ],
...
],
Options is a map with the option name as a key. For each option, it is necessary to provide a description text in "help" key, and a "type" key (for options with arguments only). Optionally, an option can contain an example.
There are two kinds of options: flags and options, which require an argument.For flags ommit type key, or specify type as "". A type is a string describing the type.basic types supported are string, boolean, integer.
Special types:
In this case, you need to specify "typespec" key containing the regular expression the argument should be matched against.
In this case, the typespec key must contain a list of possible values as strings
Example 1.4. Options map definition
"options" : $[
"propose" : $[
"help": "propose a configuration",
"example": "lan add propose",
"type": ""
],
"device": $[
"help": "device ID",
"type": "string",
"example": "lan add device=eth0"
],
"blem" : $[
"help": "other argument (without spaces)",
"type": "regex",
"typespec": "^[^ ]+$"
],
"atboot": $[
"help": "should be brought up at boot?",
"type": "enum",
"typespec": ["yes","no"]
}
The actions and options are grouped together using mappings. Currently, you can mapan action to a set of options. You can't specify required/optional options, all ofthem are optional.
Example 1.5. Mappings between actions and their options
"mappings" $[
"list" : [ "configured", "unconfigured" ],
"add" : [ "device", "ip", "netmask", "blem" ],
"delete": [ "device" ]
]
If you need to write your own event loop, this is a part of the CommandLine API useful for this:
returns true, if the user asked to start up the module GUI
reads a new command line in interactive mode, splits the arguments into a list
parse (and scan if needed) next command and return its map
lower-level function to parse the command line, check the validity
returns true, if the last command was already returned
returns true, if the user asked to cancel the changes
prints the string and then a message how to obtain the help
prints the string
same as CommandLine::Print, but the string is printed only in verbose mode
Example 1.6. Example of an event-loop
import "CommandLine"; if( ! CommandLine::Init( description_of_commands, Args() ) ) return; if( CommandLine::StartGUI() ) { <do standard GUI module> return; } <initialize call> while( ! CommandLine::Done() ) { map command = CommandLine::Command(); <handle commands here> } <finish call>
The CommandLine::Init() returns boolean whether the module has something to do. If the user only requested help or the arguments passed were not correct, the module should stop processing.
The CommandLine::Command() will do the user interaction if necessary. Also, it will handle all supported"system" commands, like "help", "exit" and "quit".
![]() | Note |
|---|---|
In interactive mode, the communication uses /dev/tty. In non-interactive commandline mode it prints everything to standard error output. |
For an example without using the event-loop provided by the
CommandLine module, see lan-simple.ycp.
/**
* File: clients/lan.ycp
* Package: Network configuration
* Summary: Network cards main file
* Authors: Michal Svec <msvec@suse.cz>
*
* $Id: lan-simple.ycp 10158 2003-06-23 12:48:40Z visnov $
*
* Main file for network card configuration.
* Uses all other files.
*/
{
/***
* <h3>Network configuration</h3>
*/
import "CommandLine";
include "network/lan/wizards.ycp";
/**
* Command line definition
*/
map cmdline = $[
"help" : "Configuration of network cards",
"id" : "lan",
"actions" : $[
"list" : $[
"help" : "display configuration summary",
"example" : "lan list configured"
],
"add" : $[
"help" : "add a network card" ],
"delete" : $[
"help" : "delete a network card" ]
],
"options" : $[
"propose" : $[
"help" : "propose a configuration",
"example" : "lan add propose",
"type" : ""
],
"configured" : $[
"help" : "list only configured cards"
],
"unconfigured" : $[
"help" : "list only not configured cards"
],
"device" : $[
"help" : "device ID",
"type" : "string",
"example" : "lan add device=eth0"
],
"ip" : $[
"help": "device address",
"type": "ip"
],
"netmask" : $[
"help": "network mask",
"type": "netmask"
],
],
"mappings" : $[
"list" : [ "configured", "unconfigured" ],
"add" : [ "device", "ip", "netmask" ],
"delete": [ "device" ],
]
];
/* The main () */
y2milestone("----------------------------------------");
y2milestone("Lan module started");
/* Initialize the arguments */
if(!CommandLine::Init(cmdline, Args())) {
y2error("Commandline init failed");
return false;
}
/* Start GUI */
if(CommandLine::StartGUI()) {
any ret = nil;
ret = LanSequence();
y2debug("ret=%1", ret);
y2milestone("Lan module finished");
y2milestone("----------------------------------------");
return ret;
}
/* Init */
CommandLine::Print("Initializing");
import "Lan";
import "Progress";
CommandLine::Print("Reading");
Progress::off();
Lan::Read();
/* Init variables */
string command = "";
list flags = [];
map options = $[];
string exit = "";
list l = [];
while(!CommandLine::Done()) {
map m = CommandLine::Command();
command = m["command"]:"exit";
options = m["options"]:$[];
/* list */
if(command == "list") {
CommandLine::Print("\nSummary\n");
string summary = sformat("%1\n", Lan::Summary(false));
CommandLine::Print(summary);
}
/* del */
else if(command == "delete") {
string dev = options["device"]:"";
CommandLine::Print("Deleting: "+dev);
if(Lan::Delete(dev) && Lan::Commit())
CommandLine::Print("Success");
else
CommandLine::Print("Error");
}
/* add */
else if(command == "add") {
string dev = options["device"]:"";
}
else {
/* maybe we got "exit" or "quit" */
if( !CommandLine::Done() ) {
CommandLine::Print("Unknown command (should not happen)");
continue;
}
}
}
if(!CommandLine::Aborted()) {
CommandLine::Print("Writing");
Lan::Write();
CommandLine::Print("Finish");
}
else {
CommandLine::Print("Quit (without saving)");
}
/* Finish */
y2milestone("Lan module finished");
y2milestone("----------------------------------------");
/* EOF */
}
For an example with the standard event-loop provided by the
commandline.ycp include, see lan-simpler.ycp.
/**
* File: clients/lan.ycp
* Package: Network configuration
* Summary: Network cards main file
* Authors: Michal Svec <msvec@suse.cz>
*
* $Id: lan-simpler.ycp 24831 2005-08-11 15:51:55Z visnov $
*
* Main file for network card configuration.
* Uses all other files.
*/
{
/***
* <h3>Network configuration</h3>
*/
import "CommandLine";
include "network/lan/wizards.ycp";
/**
* Command line definition
*/
map cmdline = $[
"help" : "Configuration of network cards",
"id" : "lan",
"guihandler": ``(LanSequence()),
"initialize": ``(Lan::Read()),
"finish" : ``(Lan::Write()),
"actions" : $[
"list" : $[
"help" : "display configuration summary",
"example" : "lan list configured",
"handler" : ``(listHandler())
],
"add" : $[
"help" : "add a network card",
"handler" : ``(addHandler())
],
"delete" : $[
"help" : "delete a network card",
"handler" : ``(deleteHandler())
]
],
"options" : $[
"propose" : $[
"help" : "propose a configuration",
"example" : "lan add propose",
"type" : ""
],
"configured" : $[
"help" : "list only configured cards"
],
"unconfigured" : $[
"help" : "list only not configured cards"
],
"device" : $[
"help" : "device ID",
"type" : "string",
"example" : "lan add device=eth0"
],
"ip" : $[
"help": "device address",
"type": "ip"
],
"netmask" : $[
"help": "network mask",
"type": "netmask"
],
],
"mappings" : $[
"list" : [ "configured", "unconfigured" ],
"add" : [ "device", "ip", "netmask" ],
"delete": [ "device" ],
]
];
/** handler for action "list" */
define void listHandler( map options ) ``{
CommandLine::Print("\nSummary\n");
string summary = CommandLine::Rich2Plain( sformat("%1\n", mergestring( Lan::Summary(false), "") ) );
CommandLine::Print(summary);
}
/** handler for action "add" */
define void addHandler( map options ) ``{
string dev = options["device"]:"";
}
/** handler for action "delete" */
define void deleteHandler( map options ) ``{
string dev = options["device"]:"";
CommandLine::Print("Deleting: "+dev);
if(Lan::Delete(dev) && Lan::Commit())
CommandLine::Print("Success");
else
CommandLine::Print("Error");
}
import "Lan";
CommandLineRun( cmdline );
/* Finish */
y2milestone("Lan module finished");
y2milestone("----------------------------------------");
/* EOF */
}
If you need to activate or de-activate services in YaST, use module Service. It is a replacement for the old client runlevel_adjust that you should have been using before You may also use it to obtain information about services. Following functions are self-containing.
Service::Adjust (string name,
string action) does some operation with init script.
name is the name of the init script.
action is one of:
"enable"
enables service, which means that if service is not set to run in any runlevel, it will set it to run in default runlevels. Otherwise it will not touch the service. Use it when you want the service to run, because it preserves user-defined state. If user sets service to run in some non-default runlevels, this will not destroy her own settings.
"disable"
disables service, which means that it sets it not to run in any runlevel.
"default"
sets service to run in its default runlevels, which are the runlevels listed in init script comment, fiels Default-Start.
"enable" and "disable" take no action when init script links are OK, but "default" always runs insserv.
Service::Adjust ("cups", "disable");
Service::Finetune
(string name, list rl)
sets service to run in list rl.
rl is list of strings.
Service::Finetune ("cups", [ "5" ]);
Service::Finetune ("cups", [ "S", "3", "5" ]);
Service::RunInitScript
(string name, string param)
runs init script name
with parameter param. Returns init scripts exit
value. I do not think it is worth to import module
Service only because of this one function when you
may simply call SCR::Execute (.target.bash, ...)
with the same result. But if you use Service for something else, this
function may increase readability of the code.
Service::RunInitScript ("cups", "restart");
Service::Status (string name),
Service::Info (string name) and
Service::FullInfo
(string name)
Service::Status tells whether service runs. It calls init script status and returns exit value.
Service::Info returns information about service. The map contains:
$[ "defstart" : [ ... ], // list of strings "defstop" : [ ... ], // list of default runlevels "start" : [ ... ], // list of strings "stop" : [ ... ], // list of real runlevels "reqstart" : [ ... ], // list of strings "reqstop" : [ ... ], // prerequisites "description": string,// description ]
Values "def{start|stop}", "req{start|stop}" and "description" are taken from init script comments. "start" and "stop" are taken from links. Service::FullInfo combines info from Service::Info and Service::Status into one map. It adds key "started" with integeral value of script status. Service is enabled if "start" is not empty.
Service::Enabled
(string name) returns true if service is set to run in any
runlevel. False otherwise.
import "Service";
boolean Read () {
if (0 != Service::RunInitScript ("foo", "restart"))
{
y2error ("Can not start service");
return false;
}
// ... your code ...
}
boolean Write () {
// ... write settings ...
// set service to correct state
if (start_it)
{
// enable service
Service::Adjust ("foo", "enable");
// reload changed settings
Service::RunInitScript ("foo", "reload");
}
else
{
// disable service
Service::Adjust ("foo", "disable");
// and stop it
Service::RunInitScript ("foo", "stop");
}
}It has sense to run some services only in case that some other service runs. Now the only case known to me is portmap that is needed by NFS, NIS and others. Runlevel UI cares about this case. But if you use functions like Service::Adjust, you must take care about these dependencies yourself. If you know about other dependencies, let me know, I will implement them in UI.
Just for your info: Runleve UI warns user who wants to stop service xdm. If you know about another cases where it is wise idea to warn user, please let us know...
Table of Contents
List of Examples
Table of Contents
This is both a tutorial and a reference on how to lay out YaST2 dialogs.
Since experience shows that most people begin this with only a vague perception of YaST2's concepts, it also includes some basics that might be covered in other YaST2 documentation as well - just enough to get started.
If you are in a hurry or - like most developers - you don't like to read docs, you can skip this section and move right on to the next section. That is, if you think you know what the next few headlines mean. You can always come back here later.
Just don't ask anything that is explained here on the yast2-hackers mailing list - you'll very likely just get a plain RTFM answer shot right back into your face. And this is the FM, so read it if you need explanations. ;-)
The UI (user interface) is that part of YaST2 that displays dialogs. It is a separate process which uses a separate interpreter. Always think of it as something running on a different machine: There is the machine you want to install with YaST2 (i.e. the machine where the disks will be formatted etc.) and there is the machine that displays the dialogs - the UI machine. In most cases, this will actually be the same machine. But it doesn't need to be. Both parts of YaST2 might as well run on different machines connected via a serial line, a network or by some other means of communication (telepathy? ;-) ).
The logical consequence of this is that the UI uses its own
separate set of function definitions and variables. You need to be
real careful not to mix that up. Always keep in mind what part of
YaST2 needs to do what and what variables need to be stored where.
You can easily tell by the UI prefix within the YCP code
what parts are getting executed by the UI.
A widget is the most basic building block of the UI. In short, each single dialog item like a PushButton, a SelectionBox or a TextEntry field is a widget. But there are more: Most static texts in dialogs are widgets, too. And there are a lot of widgets you can't see: Layout boxes like HBox or VBox and many more that don't actually display something but arrange other widgets in some way.
See the widget reference for details and a list of all available widgets.
There are several different UIs for YaST2. There is the Qt based UI (y2qt) as a graphical frontend which requires the X Window System; this is what most people know as the "normal" YaST2 UI. But there is also a NCurses based UI (y2ncurses) for text terminals or consoles.
That means, of course, that all YaST2 dialogs need to be written in a way that is compatible with each of those UIs. This is why libyui was introduced as an intermediate abstract layer between the YCP application and the UI. You do not communicate directly with either y2qt, y2ncurses - you communicate with the libyui.
Thus, YaST2 dialogs need to be described logically rather than in terms of pixel sizes and positions: You specify some buttons to be arranged next to each other rather than at positions (200, 50), (200, 150), (200, 200) etc. - whatever exactly this "next to each other" means to the specific UI.
Add to that the fact that there are several dialog languages to choose from: User messages or button labels have different lengths in different languages. Just compare the length of English messages to those in German or French, and you'll discover another good reason not to hard-code coordinates.
In addition to that, always keep in mind that the same dialog might require a different amount of space in a different UI. Overcrowded dialogs don't look good in the Qt UI. In the NCurses UI, they will very likely break completely: There simply isn't as much space available (80x25 characters vs. 640x480 pixels).
Each widget has a so-called nice size - this is the size the widget would like to have in order to look nice. E.g. for PushButtons that means the entire button label fits into the button. Likewise for labels.
Then there are widgets that don't have a natural nice size. For example, what size should a SelectionBox get? It can scroll anyway, so anything that makes at least one line of the list visible will satisfy the basic requirements. More space will make it look nicer; but how much is enough? The widget cannot tell that by itself.
Such widgets report a somewhat random size as their nice size. This is a number chosen for debugging purposes rather than for aesthetics. You almost always need to specify the size from the outside for that very reason. Always supply a weight for such widgets or surround them with spacings.
By default, all dialogs will be as large as they need to be - up to full screen size (which is UI dependent - 640x480 pixels for Qt, 80x25 characters for NCurses): The outermost widget is asked what size it would like to have, i.e. its nice size. If that outermost widget has any children, for example because it is a layout box, it will ask all of its children and sum up the individual sizes. Those in turn may have to ask their children and so on. The resulting size will be the dialog's initial size - unless, of course, this would exceed the screen size (UI dependent, see above).
You can force full screen size for any dialog by setting the
`defaultsize option when opening it:
OpenDialog(
`opt(`defaultsize ),
`VBox(...)
);
This will create a dialog of 640x480 pixels (y2qt) or 80x25 characters (y2ncurses) - regardless of its contents.
Use this for main windows or for popup dialogs with very much the same semantics - e.g. many of the YaST2 installation wizard's "expert" dialogs. Even though they are technically popup dialogs and they return to the main thread of dialog sequence they have main window semantics to the user.
Use your common sense when considering whether or not to use this feature for a particular dialog.
Note: Not every UI may be capable of this feature. This is only a hint to the UI; you cannot blindly rely on it being honored.
This section covers the widgets used for creating dialog layouts - the kind of widgets that are less obvious to the user. If you are interested in the "real" widgets, i.e. the kind you can actually see, please refer to the widget reference.
This is the most basic and also the most natural layout widget. The HBox widget arranges two or more widgets horizontally, i.e. left to right. The VBox arranges two or more widgets vertically, i.e. top to bottom.
The strategy used for doing this is the same, just the dimensions (horizontal / vertical) are different. Each child widget will be positioned logically next to its neighbor. You don't have to care about exact sizes and positions; the layout box will do that for you.
See the description of the layout algorithm for details.
For creating more complex layouts, nest HBox and VBox widgets into each other. Usually you will have a structure very much like this:
`VBox(
`HBox(...),
`HBox(...),
...
)
i.e. a VBox that has several HBoxes inside. Those in turn can have VBoxes inside etc. - nest as deep as you like.
Almost every kind of layout can be broken down into such columns (i.e. VBoxes) or rows (i.e. HBoxes). If you feel you can't do that with your special layout, try using weights.
By default, each widget in a layout box (i.e. in a HBox or a VBox) will get its nice size, no more and no less. If for any reason you don't want that, you can exactly specify the proportions of each widget in the layout box. You do that by supplying the widgets with a weight (to be more exact: by making it the child of a weight widget, a HWeight or a VWeight).
You can specify percentages for weights, or you can choose random numbers. The layout engine will add the weights of all children of a layout box and calculate percentages for each widget automatically. Specify a HWeight for HBox children and a VWeight for VBox children.
Example 1.1. Specifying Proportions 1
`HBox(
`HWeight( 20, `PushButton( "OK" ) ),
`HWeight( 50, `PushButton( "Cancel" ) ),
`HWeight( 30, `PushButton( "Help" ) )
)
In this example, the "OK" button will get 20%, the "Cancel" button 50% and the "Help" button 30% of the available space. In this example, the weights add up to 100, but they don't need to.
Note: This dialog looks extremely ugly - don't try this at home, kids ;-)
The weight ratios will be maintained at all times, even if that means violating nice size restrictions (i.e. a widget gets less space than it needs). You are the boss; if you specify weights, the layout engine assumes you know what you are doing.
Example 1.2. Specifying Proportions 2
(See also creating widgets of equal size in the common layout techniques section)
`HBox(
`HWeight( 1, `PushButton( "OK" ) ),
`HWeight( 1, `PushButton( "Cancel" ) ),
`HWeight( 1, `PushButton( "Help" ) )
)
Note: This is a very common technique.
In this example all buttons will get an equal size. The button with the largest label will determine the overall size and thus the size of each individual button.
Please note how the weights do not add up to 100 here. The value "1" is absolutely random; we might as well have specified "42" for each button to achieve that effect.
Example 1.3. Specifying Proportions 3
The YaST2 wizard layout reserves 30% of horizontal space for the help text (a RichText widget) and the remaining 70% for the rest of the dialog. The important part of that code (simplified for demonstration purposes) looks like that:
`HBox(
`HWeight( 30, `RichText( "Help text") ),
`HWeight( 70, `VBox(
... // the dialog contents
`HBox(
`PushButton( "Back"),
`HCenter( `PushButton( "Abort Installation") ),
`PushButton( "Next")
)
)
)
)
Specifying the size of the help text like that is important for most kinds of widgets that can scroll - like the RichText widget used here, for example. The RichText widget can take any amount of space available; it will wrap lines by itself as long as possible and provide scroll bars as necessary. Thus, it cannot supply any reasonable default size on its own - you must supply one. We chose 30% of the screen space - which of course is absolutely random but suits well for the purposes of YaST2.
Use this technique for widgets like the SelectionBox, the Table widget, the Tree widget, the RichText, but also for less obvious ones like the InputField.
Note: This list may be incomplete. Use your common sense.
When you don't want parts of a dialog to be resized just because some neighboring widget needs more space, you can insert stretch widgets to take any excess space. Insert a HStretch in a HBox or a VStretch in a VBox. Those will act as "rubber bands" and leave the other widgets in the corresponding layout box untouched.
You can also insert several stretches in one layout box; excess space will be evenly distributed among them.
If there is no excess space, stretch widgets will be invisible. They don't consume any space unless there is too much of it (or unless you explicitly told them to - e.g. by using weights).
Some widgets that are not stretchable by default can be made
stretchable by setting the `hstretch option. PushButtons are typical
candidates for this: They normally consume only as much space as
they really need, i.e. their nice size.
With the `hstretch option, however, they can grow and take
any extra space - very much like stretch widgets.
Please note, however, that all widgets for with a weight are implicitly stretchable anyway, so
specifying `opt(`hstretch) or `opt(`vstretch) for
them as well is absolutely redundant.
Use HSpacing or VSpacing to create some empty space within a layout. This is normally used for aesthetical reasons only - to make dialogs appear less cramped.
The size of a spacing is specified as a float number, measured in units roughly equivalent to the size of a character in the respective UI (1/80 of the full screen width horizontally , 1/25 of the full screen width vertically). Fractional numbers can be used here, but text based UIs may choose to round the number as appropriate - even if this means simply ignoring a spacing when its size becomes zero.
You can combine the effects of a spacing and a stretch if you specify a hstretch or a vstretch option for it: You will have a rubber band that will take at least the specified amount of space. Use this to create nicely spaced dialogs with a reasonable resize behaviour.
Example 1.4. Spacings
`HBox(
`PushButton( "OK" ),
`HSpacing( `opt(`hstretch), 0.5),
`PushButton( "Cancel" )
)
This will create two buttons with a spacing between them. When the dialog is resized, the spacing will grow.
Alignments are widgets that align their single child widget in some way.
HCenter centers horizontally, VCenter centers vertically, HVCenter centers both horizontally and vertically. The others align their child as the name implies.
More often than not, you could achieve the same effect with a clever combination of spacings, but sometimes this might require an additional HBox within a HBox or a VBox within a VBox, i.e. more overhead.
Sometimes you wish to squeeze any extra space from a part of a dialog. This might be necessary if you want to draw a frame around a RadioBox in a defaultsize dialog: You want the frame drawn as close as possible to the RadioButtons, not next to the window frame with lots of empty space between the frame and the RadioButtons. Use a squash widget for that purpose:
`HVCenter(
`HVSquash(
`Frame( "Select Software categories",
`VBox(
...
)
)
)
)
This is not exactly a layout-only widget - you can see it. It is being mentioned here more because like layout widgets it can have children.
Use a Frame to visually group widgets that logically belong together - such as the RadioButtons of a RadioBox or a group of CheckBoxes that have a meaning in common (e.g. individual file permissions, software categories to install, ...).
Note: Do not overuse frames. They have a nice visual effect, but only if used sparingly.
You may need to put a squash widget around the frame in order to avoid excessive empty space between the frame and its inner widgets.
The RadioButtonGroup is a widget go logically group individual RadioButton widgets. It does not have a visual effect or an effect on the layout. All it does is to manage the one-out-of-many logic of a RadioBox: When one RadioButton is selected, all the others in the same RadioBox (i.e. in the same RadioButtonGroup) must be unselected.
Please notice that this might not be as trivial as it seems to be at first glance: There might be some outer RadioBox that switches between several general settings, enabling or disabling the others as necessary. Any of those general settings might contain another RadioBox - which of course is independent of the outer one. This is why you really need to specify the RadioButtonGroup.
You usually just surround the VBox containing the RadioButtons with a The RadioButtonGroup.
Don't forget to include your RadioBox within a frame! RadioButtonGroup, Frame and HVSquash usually all come together.
Example 1.5. Grouping RadioButtons
`HVCenter(
`HVSquash(
`Frame( "Select Installation Type",
`RadioButtonGroup(
`VBox(
`RadioButton(...),
`RadioButton(...),
`RadioButton(...)
)
)
)
)
)
A ReplacePoint
is a "marker" within the widget hierarchy of a layout. You can
later refer to it with ReplaceWidget(). Use this to cut
out a part of the widget hierarchy and paste some other
sub-hierarchy to this point.
The YaST2 wizard dialogs use this a lot: The main window stays the same, just some parts are replaced as needed - usually the large part to the right of the help text, between the title bar and the "previous" and "next" buttons.
A ReplacePoint has no other visual or layout effect.
Use the case studies in this section as building blocks for your own dialogs.
Remember that even though most of the examples use a horizontal layout (a HBox), the same rules and techniques apply in the vertical dimension as well - just replace HBox with VBox, HWeight with VWeight etc.
Screen shot of the Layout-Buttons-Equal-Growing.ycp example
You can easily make several widgets the same size - like in this example. Just specify equal weights for all widgets:
`HBox(
`HWeight(1, `PushButton( "OK" ) ),
`HWeight(1, `PushButton( "Cancel everything" ) ),
`HWeight(1, `PushButton( "Help" ) )
)
The widgets will grow or shrink when resized. They will always retain equal sizes:
The same example, resized larger.
The same example, resized smaller.
Screen shot of the Layout-Buttons-Equal-Even-Spaced1.ycp example
Widgets with a weight (such as these buttons) are implicitly stretchable. If you don't want the widgets to grow, insert stretches without any weight between them. They will take all excess space - but only if there is no weight specified (otherwise, the stretches would always maintain a size according to the specified weight - not what is desired here).
`HBox(
`HWeight(1, `PushButton( "OK" ) ),
`HStretch(),
`HWeight(1, `PushButton( "Cancel everything" ) ),
`HStretch(),
`HWeight(1, `PushButton( "Help" ) )
)
The same example, resized larger. Notice how the stretches take the excess space.
The same example, resized smaller. The stretches don't need any space if there is not enough space anyway.
Screen shot of the Layout-Buttons-Equal-Even-Spaced2.ycp example. Notice the spacing between the buttons.
If you want some space between the individual widgets, insert a spacing. You could use both a spacing and a stretch, but specifying the stretchable option for the spacing will do the trick as well - and save some unnecessary widgets:
`HBox(
`HWeight(1, `PushButton( "OK" ) ),
`HSpacing(`opt(`hstretch), 3),
`HWeight(1, `PushButton( "Cancel everything" ) ),
`HSpacing(`opt(`hstretch), 3),
`HWeight(1, `PushButton( "Help" ) )
)
The value "3" used here for the spacing is absolutely random, chosen just for aesthetics. Use your own as appropriate.
The same example, resized larger. Notice how the spacings take the excess space.
The same example, resized smaller.
As you can see, the spacings have one disadvantage here: They need the space you specified even if that means that there is not enough space for the other widgets.
As mentioned before, most kinds of widgets that can scroll don't have a natural nice size. If the overall size of your layout is fixed by some other means (e.g. because it is a full screen dialog), you can have it take the remaining space or specify proportions with weights.
If this is not the case, create such "other means" yourself: Surround the scrollable widget with widgets of a well-defined size, e.g. with spacings.
Prevent the spacings from actually using precious screen space themselves by putting a VSpacing in a HBox or a HSpacing in a VBox - it will resize the corresponding layout box in its secondary dimension. It will take no space in its primary dimension.
Example 1.6. Specifying the Size of Scrollable Widgets
`VBox(
`HSpacing(40), // make the scrollable widget at least 40 units wide
`HBox(
`VSpacing(10), // make the scrollable widget at least 10 units high
`Table(...) // or any other scrollable widget
)
)
See also the Table2.ycp, Table3.ycp, Table4.ycp and Table5.ycp examples.
As a general rule of thumb, use this technique whenever you place a scrollable widget in a non-defaultsize dialog. Don't leave the size of such widgets to pure coincidence - always explicitly specify their sizes.
printf() is your best friend when debugging - every
seasoned programmer knows that. YaST2 has something very much like
that: y2log(), available both in YCP and in the C++ sources.
It is being used a lot, and you can add your own in your YCP code.
Thus, if something strange happens, check the log file - either in
your home directory (~/.y2log) or the system wide log file
(/var/log/y2log).
You can increase the level of verbosity by setting the
Y2DEBUG environment variable to 1 - both in your shell and
at the boot prompt (for debugging during an installation) - boot
with something like
linux Y2DEBUG=1
Log files will be wrapped when they reach a certain size - i.e.
the current log file is renamed to ~/.y2log-1,
~/.y2log-2 etc. or /var/log/y2log-1,
/var/log/y2log-2 etc., and a new log file is begun.
If the layout engine complains about widgets not getting their nice size and tell you to check the layout, please do that before you write a bug report. More often than not that just means that your dialog is overcrowded. That doesn't only raise technical problems: In that case your dialog most likely is too complex and not likely to be understood by novice users. In short, you very likely have a problem with your logical design, not with the layout engine. Consider making it easier or splitting it up into several dialogs - e.g. an easy-to-understand novice level base dialog and an advanced "expert" dialog. Use YaST2's partitioning, software selection and LILO configuration dialogs as examples for how to do this.
You might also consider replacing some widgets with others that don't use as much screen space - e.g. use a ComboBox rather than a SelectionBox, or a ComboBox rather than a RadioBox. But always keep in mind that this just reduces screen space usage, not complexity. Plus, widgets like the ComboBox frequently are harder to operate from a user's point of view because they require more mouse clicks or keys presses to get anything done. Use with caution.
When you created a new dialog or substantially changed an existing one always remember to check it with the other UIs, too. If it looks good with the Qt UI that doesn't mean it looks good with the NCurses UI as well - it might even break completely. There might be too many widgets or parts of widgets may be invisible because of insufficient screen space.
If you don't like that idea always remember some day you might be that poor guy who can't run YaST2 with Qt - maybe because of a brand new graphics card the X server doesn't support yet or maybe because you have to install a server system that just has a serial console.
The text based version may not need to look as good (but it would sure be nice if it did), but it needs to work. That means all widgets must be there and be visible. If they are not, you really need to rearrange or even redesign your dialog. Possibly before somebody from the support department finds it out the hard way - because a user complained badly about it.
Very much the same like the previous issue: Consider somebody who wants or needs to operate your dialog without a mouse. Maybe he doesn't have one or maybe it doesn't work - or maybe he uses the NCurses UI. There are even a lot of users who can work a whole lot quicker if they can use keyboard shortcuts for common tasks - e.g. activating buttons or jumping to text input fields. You can and should provide keyboard shortcuts for each of those kinds of widgets.
Of course this needs to be double-checked with each of the translated versions: Keyboard shortcuts not only are language dependent (so users can memorize them), they are even contained within messages files. The translators need to include their own in the respective language, and that means chances are some of the sort cuts are double used - e.g. Alt-K may be used twice in the same dialog, which renders the second use ineffective. Always check that, too.
You don't need to know the internals of the YaST2 UI layout engine in order to be able to create YaST2 dialogs. But this kind of background knowledge certainly helps a lot when you need to debug a layout - i.e. when a dialog you programmed behaves "strange" and doesn't look at all like you expected.
A HBox lays out its children horizontally, a VBox vertically. How they do that is very much the same except for the dimensions: The HBox uses horizontal as its primary dimension, the VBox vertical. The other dimension is called the secondary dimension (vertical for the HBox, horizontal for the VBox).
Calculating the nice size in the secondary dimension is easy: It is the maximum of the nice sizes of all children. Thus, for a HBox this is the nice height of the highest child, for a VBox this is the nice width of the widest child.
If any child is a layout box itself (or any other container widget), this process will become recursive for the children of that layout box etc. - this holds true for both the primary and the secondary dimension.
In the primary dimension things are a bit more complicated: First, the nice sizes of all children without weights are summed up.
Then the sizes of all children with weights are added to that sum - in such a way that each of those gets at least its nice size, yet all weights are maintained with respect to each other. I.e. when a button is supposed to get 30% he must get it, but its label must still be completely visible.
Maybe some of the children with weights need to be resized larger because of those restrictions. Exactly how large is calculated based on the so-called boss child. This is the one widget that commands the overall size of all children with weights, the one with
max ( nice size / weight )
The boss child's nice size and its weight determine the accumulated nice size of all children with weights. The other children with weights will be resized larger to get their share of that accumulated size according to their individual weights.
By the way this is why all children with weights are implicitly stretchable - most of them will be resized larger so the weights can be maintained at all times.
Each widget has a SetSize() method. This will be called recursively for all widgets from top (i.e. the outer dialog) to bottom. When a dialog is opened, the UI determines how large a dialog should become. The UI tries to use the dialog's nice size, if possible - unless the defaultsize option is set or the nice size exceeds the screen size, in which case the screen size is used.
After the dialog is opened, the SetSize() method will be called again when:
The user resizes a dialog.
A significant portion of the dialog changes - e.g. because of ReplaceWidget().
All of those cases will cause a re-layout of the entire dialog.
For layout boxes, the SetSize() method works like this:
If none of the children of a layout box has a weight, any extra space (i.e. space in excess of the nice size) is evenly distributed among the stretchable children. All non-stretchable children get their nice size, no more.
If there are not any stretchable children, there will be empty space at the end of the layout (i.e. to the right for a HBox and at the bottom of a VBox). If any child has a weight, all children without weights will get no more than their nice sizes - no matter whether or not they are stretchable.
The rest of the space will be distributed among the children with weights according to the individual weights.
There is one exception to that rule, however: If there is more space than the weighted childrens' nice size and there are any stretches or stretchable spacings without weights, the excess space will be evenly distributed among them.
This may sound like a very pathological case, but in fact only this gives the application programmer a chance to create equal sized widgets that don't grow, maybe with a little extra space between them. Simple popup dialogs with some buttons are typical examples for this, and this is quite common.
There should be enough space for any layout box: By default, the overall size of a dialog is calculated based on its nice size. But this might exceed the full screen size, or the user might manually have resized the dialog (some UIs are capable of that) - both of which cases will cause a dialog to get less than its nice size.
If there is not enough space, the layout engine will complain about that fact in the log file, asking you to "check the layout". Please do that if this message always appears when a certain dialog is opened - you may have to rearrange your dialog so all widgets properly fit into it.
Anyway, if it happens, some widgets will get less than their nice size and probably will not look good; some might even be completely invisible.
Even then, as long as there is enough space for all children without weights, those will get their nice sizes. Only the remaining space will be distributed among the children with weights.
If the space isn't even enough for the children without weights, each of them will have to spend some of its space to make up for the loss. The layout engine tries to treat each of them equally bad, i.e. each of them has to give some space.
This behaviour may be somewhat unexpected, but not only is this compatible with older versions of the YaST2 UI, it also comes very handy for simple layout tasks like this (taken from the Label1.ycp example):
`VBox(
`Label( "Hello, world" ),
`PushButton( "OK" )
)
This button will be centered horizontally - without the need for a HCenter around it.
Classic graphical user interface (GUI) programming is almost always event-driven: The application initializes, creates its dialog(s) and then spends most of its time in one central event loop.
When the user clicks on a button or enters text in an input field, he generates events. The underlying GUI toolkit provides mechanisms so the application can react to those events - perform an action upon button click, store the characters the user typed etc.; all this is done from callback functions of one kind or the other (whatever they may be called in the respective GUI toolkit).
In any case, it all comes down to one single event loop in the application from where small functions (let's call them callbacks for the sake of simplicity) are called when events occur. Those callbacks each contain a small amount of the application's GUI logic to do whatever is to be done when the respective event occurs. The overall application logic is scattered among them all.
This approach is called event-driven. Most GUI toolkits have adopted it.
Depending on the primary goal of a GUI application, this event-driven approach may or may not be appropriate. It is perfectly suitable for example for word processor applications, for web browsers or for most other GUI applications that have one central main window the user works with most of his time: The user is the driving force behind those kinds of applications; only he knows what he next wishes to do. The application has no workflow in itself.
Thus the event-driven application model fits perfectly here: The callbacks can easily be self-contained; there is little context information, and there are limited application-wide data.
Applications like YaST2 with all its installation and configuration workflows, however, are radically different. The driving force here is the application workflow, the sequence of dialogs the user is presented with.
Of course this can be modeled with a traditional event loop, but doing that considerably adds to the complexity of the application: Either the application needs a lot more callbacks, or the callbacks need to keep track of a lot of status information (workflow step etc.) - or both.
For the YaST2 UI, a different approach was chosen: Rather than having one central event loop and lots of callbacks, the flow control remains in the interpreted YCP code. User input is requested on demand - very much like in simplistic programming languages like the first versions of BASIC.
This of course means that there is no single one central "waiting point" in the program (like the event loop in the event-driven model), but rather lots of such waiting points spread all over the YCP code within each UserInput() or WaitForEvent() statement.
Side note: Of course a graphical UI like the YaST2 Qt UI still has to be prepared to perform screen redraws whenever the underlying window system requires that - i.e. whenever X11 sends an Expose (or similar) event. For this purpose the Qt UI is multi-threaded: One thread takes care of X event handling, one thread is the actual YCP UI interpreter. This instant screen redraw is what you lose when you invoke y2base with the "--nothreads" command line option.
YCP was meant to be an easy-to-understand programming language for developers who specialize in a particular aspect of system configuration or installation, not in GUI programming.
Practical experience with all the YaST2 modules developed so far has shown that application developers tend to adopt this concept of UserInput() very easily. On the other hand it is a widely known fact that event-driven GUI programming means a steep learning curve because (as mentioned before) it requires splitting up the application logic into tiny pieces for all the callbacks.
Thus, this design decision of YaST2 seems to have proven right much more often than there are problems with its downsides (which of course also exist).
The basic idea of YaST2 UI programming is to create a dialog asking the user for some data and then continue with the next such dialog - meaning that most of those dialogs are basically forms to be filled in with an "OK" (or "Next") and a "Cancel" (or "Back") button. The YCP application is usually interested only in those button presses, not in each individual keystroke the user performs.
This is why by default UserInput() and related functions react to little more than button presses - i.e. they ignore all other events, in particular low-level events the widgets handle all by themselves like keystrokes (this is the input fields' job) or selecting items in selection boxes, tables or similar. Most YCP applications simply don't need or even want to know anything about that.
This makes YCP UI programming pretty simple. The basic principle looks like this:
{
UI::OpenDialog(
`VBox(
... // Some input fields etc.
`HBox(
`PushButton(`id(`back ), "Back" ),
`PushButton(`id(`next ), "Next" )
)
)
);
symbol button_id = UI::UserInput();
if ( button_id == `next )
{
// Handle "Next" button
}
else if ( button_id == `back )
{
// Handle "Back" button
}
UI::CloseDialog();
}
Strictly spoken, you don't even require a loop around that - even though this is very useful and thus strongly advised.
All that can make UserInput() return in this example are the two buttons. Other widgets like input fields ( InputField), selection boxes etc. by do not do anything that makes UserInput() return - unless explicitly requested.
If a YCP application is interested in events that occur in a
widget other than a button, the notify widget option can be used
when creating it with UI::OpenDialog().
Example 1.7. The notify option
UI::OpenDialog(...
`SelectionBox(`id(`pizza ), `opt(`notify ), ... ),
...
`Table(`id(`toppings), `opt(`notify, `immediate ), ... ),
...
)
In general, the notify options makes UserInput() return when something "important" happens to that widget. The immediate option (always in combination with notify!) makes the widget even more "verbose".
Note: UserInput() always returns the ID of the widget that caused an event. You cannot tell the difference when many different types of event could have occured. This is why there are different levels of verbosity with `opt(`notify ) or `opt(`notify, `immediate ) and the new WaitForEvent() UI builtin function which returns more detailed information. A Table widget for example can generate both Activated and SelectionChanged WidgetEvents.
Exactly what makes UserInput() return for each widget class is described in full detail in the YaST2 event reference.
The YaST2 event handling model has been (and will probably always remain) a subject of neverending discussions. Each and every new team member and everybody who casually writes a YaST2 module (to configure the subsystem that is his real responsibility) feels compelled to restart this discussion.
The idea of having a function called UserInput() seems to conjure up ghastly memories of horrible times that we hoped to have overcome: The days of home-computer era BASIC programming or university Pascal lectures (remember Pascal's readln()?) or even low-tech primitive C programs (gets() or scanf() are not better, either).
But it's not quite like that. Even though the function name is similar, the concept is radically different: It is not just one single value that is being read, it is a whole dialog full of whatever widgets you see fit to put there. All the widgets take care of themselves; they all handle their values automatically. You just have to ask them (UI::QueryWidget()) for the values when you need them (leave them alone as long as you don't).
The similarity with computing stone age remains, however, in that you have to explicitly call UserInput() or related when you need user input. If you don't, you open your dialog, and a moment later when you continue in your code it closes again - with little chance for the user to enter anything.
Thus, the YaST2 approach has its intrinsic formalisms in that sequence:
OpenDialog(...);
UserInput();
QueryWidget(...);
QueryWidget(...);
QueryWidget(...);
...
CloseDialog();
This is the price to pay for this level of simplicity.
In the course of those discussions some design alternatives began to emerge:
Use the single-event-loop and callback model like most other toolkits.
Keep multiple event loops (like UserInput()), but add callbacks to individual widget events when needed so the YCP application can do some more fine-grained control of individual events.
Keep multiple event loops, but return more information than this simplistic UserInput() that can return no more than one single ID.
Having just a single event loop would not really solve any problem, but create a lot of new ones: A sequence of wizard style dialogs would be really hard to program. Switching back and forth between individual wizard dialogs would have to be moved into some callbacks, and a lot of status data for them all to share (which dialog, widget status etc.) would have to be made global.
What a mess. We certainly don't want that.
All the callback-driven models have one thing in common: Most of the application logic would have to be split up and moved into the callbacks. The sequence of operations would be pretty much invisible to the application developer, thus the logical workflow would be pretty much lost.
Most who discussed that agreed that we don't want that, too.
Add to that the formalisms that would be required for having callbacks: Either add a piece of callback code (at least a function name) to UI::OpenDialog() for each widget that should get callbacks or provide a new UI builtin function like, say, UI::SetCallback() or UI::AddCallback() that gets a YCP map that specifies at least the widget to add the callback to, the event to react to and the code (or at least a function name) to execute and some transparent client data where the application can pass arbitrary data to the callback to keep the amount of required global data down.
UI::RemoveCallback()
It might look about like this:
define void selectionChanged( any widgetID, map event, any clientData ) {
...
// Handle SelectionChanged event
...
};
define void activated( any widgetID, map event, any clientData ) {
...
// Handle Activated event
...
};
...
UI::OpenDialog(
...
`Table(`id(`devices ), ... ),
...
);
...
UI::AddCallback(`id(`devices ), `SelectionChanged, nil );
UI::AddCallback(`id(`devices ), `Activated, nil );
If you think "oh, that doesn't look all too bad", think twice. This example is trivial, yet there are already three separate places that address similar things:
The callback definitions. Agreed, you'll need some kind of code that actually does the application's business somewhere anyway. But chances are that the callbacks are no more than mere wrappers that call the functions that actually do the application's operations. You don't want to mix up all the back engine code with the UI related stuff.
Widget creation with UI::OpenDialog()
Adding callbacks with UI::AddCallback()
A lot of GUI toolkits do it very much this way - most Xt based toolkits for example (OSF/Motif, Athena widgets, ...). But this used to be a source of constant trouble: Change a few things here and be sure that revenge will come upon you shortly. It simply adds to the overall complexity of something that is already complex enough - way enough.
Bottom line: Having callbacks is not really an improvement.
What remains is to stick to the general model of YaST2 but return more information - of course while remaining compatible with existing YCP code. We don't want (neither can we economically afford to) break all existing YCP code. So the existing UI builtin functions like UserInput() or PollInput() have to remain exactly the same. But of course we can easily add a completely new UI builtin function that does return more information.
This is what we did. This is how WaitForEvent() came into existence. It behaves like UserInput(), but it returns more information about what really happened - in the form of an event map rather than just a single ID. That map contains that ID (of course) plus additional data depending on the event that occured.
One charming advantage of just adding another UI builtin is that existing code does not need to be touched at all. Only if you want to take advantage of the additional information returned by WaitForEvent() you need to do anything at all.
So let's all hope with this approach we found a compromise we all can live with. While that probably will not prevent those discussions by new team members, maybe it will calm down the current team members' discussion a bit. ;-)
Since the YaST2 UI doesn't have a single event loop where the program spends most of its time, an indefinite period of time may pass between causing an event (e.g., the user clicks on a widget) and event delivery - the time where the (YCP) application actually receives the event and begins processing it. That time gap depends on exactly when the YCP code executes the next UserInput() etc. statement.
This of course means that events that occured in the mean time need to be stored somewhere for the YCP code to pick them up with UserInput() etc.
The first approach that automatically comes to mind is "use a queue and deliver them first-in, first-out". But this brings along its own problems:
Events are only useful in the context of the dialog they belong to. When an event's dialog is closed or when a new dialog is opened on top of that event's dialog (a popup for example) it doesn't make any more sense to handle that event. Even worse, it will usually lead to utter confusion, maybe even damage.
Imagine this situation: The user opens a YaST2 partitioning module just to have a look at his current partitioning scheme.
Side note: This scenario is fictious. The real YaST2 partitioning module is not like that. Any similarities with present or past partitioning modules or present or past YaST2 hackers or users is pure coincidence and not intended. Ah yes, and no animals were harmed in the process of making that scenario. ;-)
The main dialog with an "OK" button (with, say, ID `ok) opens.
It takes some time to initialize data in the background.
The user clicks "OK".
The background initialization takes some more time.
The user becomes impatient and clicks "OK" again.
The background initialization still is not done.
The user clicks "OK" again.
The initialization is done. Usually, the YCP code would now reach UserInput() and ueued events would be delivered (remember, this is only a fictious scenario - the UI does not really do that). The first "OK" click from the queue is delivered - i.e. UserInput() returns `ok.
But this doesn't happen this time: The initialization code found
out that something might be wrong with the partitioning or file
systems. It might make sense to convert, say, the mounted
/usr file system from oldLameFs-3.0 to
newCoolFs-0.95Beta - which usually works out allright, but
of course you never know what disaster lies ahead when doing such
things with file systems (and, even worse, with an experimental
beta version).
The initialization code opens a popup dialog with some text to informs the user about that. The user can now click "OK" to do trigger the file system conversion or "Cancel" to keep everything as it is.
The handler for that popup dialog calls UserInput() - which happily takes the next event from the queue - the `ok button click that doesn't really belong to that dialog, but UserInput() cannot tell that. Neither can the caller. It simply gets `ok as if the user had clicked the "OK" button in the popup.
The program has to assume the user confirmed the request to convert the file system. The conversion starts.
The experimental beta code in newCoolFs-0.95Beta cannot handle the existing data in that partition as it should. It asks if it is allright to delete all data on that partition. Another popup dialog opens with that question.
The handler for that confirmation popup takes the next event from the queue which is the third `ok click that should have gone to the main window. But the handler doesn't know that and takes that `ok as the confirmation it asked for.
/usr is completely emptied. Half of the system is gone
(along with most of YaST2's files). The disaster is complete - the
system is wrecked beyond repair.
Argh. What a mess.
Yes, this example is contrived. But it shows the general problem: Events belong to one specific dialog. It never makes any sense to deliver events to other dialogs.
But this isn't all. Even if the internal UI engine (the libyui) could make sure that events are only delivered to the dialog they belong to (maybe with a separate queue for each dialog), events may never blindly be taken from any queue. If the user typed (or clicked) a lot ahead, disaster scenarios similar to the one described above might occur just as well.
Events are context specific. The dialog they belong to is not their only context; they also depend on the application logic (i.e. on YCP code). This is another byproduct of the YaST2 event handling approach.
It has been suggested to use (per-dialog) event queues, but to flush their contents when the dialog context changes:
When a new dialog is opened (OpenDialog())
When the current dialog is closed (CloseDialog())
When parts of the dialog are replaced (ReplaceWidget())
Upon the YCP application's specific request (new UI builtin FlushEvents())
Exactly when and how this should happen is unclear. Every imaginable way has its downsides or some pathologic scenarios. You just can't do this right. And YCP application developers would have to know when and how this happens - which is clearly nothing they should be troubled with.
This is why all current YaST2 UIs have onle one single pending event and not a queue of events. When a new event occurs, it usually overwrites any event that may still be pending - i.e. events get lost if there are too many of them (more than the YCP application can and wants to handle).
While it may sound critical to have only one single pending event, on this works out just as everybody expects:
When the YCP application is busy and the user clicks wildly around in the dialog, only the last of his clicks is acted upon. This is what all impatient users want anyway: "do this, no, do that, no, do that, no, cancel that all". The "Cancel" is what he will get, not everything in the sequence he clicked.
The YCP application does not get bogged down by a near-endless sequence of events from the event queues. If things are so sluggish that there are more events than the application can handle in the first place, getting even more to handle will not help any.
YaST2 dialogs are designed like fill-in forms with a few (not too many) buttons. The input field widgets etc. are self-sufficient; they do their own event handling (so no typed text will get lost). No more than one button click in each dialog makes sense anyway. After that the user has to wait for the next dialog to answer more questions. It does not make any sense to queue events here; the context in the next dialog is different anyway.
As described above, events can and do get lost if there are too many of them. This is not a problem for button clicks (the most common type of event), and it should not be a problem for any other events if the YCP application is written defensively.
Don't take anything for granted. Never rely on any specific event to always occur to make the application work allright.
In particular, never rely on individual SelectionChanged WidgetEvents to keep several widgets in sync with each other. If the user clicks faster than the application can handle, don't simply count those events to find out what to do. Always treat that as a hint to find out what exactly happened: Ask the widgets about their current status. They know best. They are what the user sees on the screen. Don't surprise the user with other values than what he can see on-screen.
In the past, some widgets that accepted initially selected items upon creation had sometimes triggered events for that initial selection, sometimes not. Even though it is a performance optimization goal of the UI to suppress such program-generated events, it cannot be taken for granted if they occur or not. But it's easy not to rely on that. Instead of writing code like this:
{
// Example how NOT to do things
UI::OpenDialog(
...
`SelectionBox(`id(`colors ),
[
`item(`id("FF0000"), "Red" ),
`item(`id("00FF00"), "Blue",true), // Initially selected
`item(`id("0000FF"), "Green" )
]
)
);
// Intentionally NOT setting the initial color:
//
// Selecting an item in the SelectionBox upon creation will trigger a
// SelectionChanged event right upon entering the event loop.
// The SelectionChanged handler code will take care of setting the initial color.
// THIS IS A STUPID IDEA!
map event = $[];
repeat
{
event = UI::WaitForEvent();
if ( event["ID"]:nil == `colors )
{
if ( event["EventReason"]:nil == "SelectionChanged" )
{
// Handle color change
setColor( UI::QueryWidget(`id(`colors ), `SelectedItem ) );
}
}
...
} until ( event["ID"]:nil == `close );
}
{
// Fixed the broken logic in the example above
UI::OpenDialog(
...
`SelectionBox(`id(`colors ),
[
`item(`id("FF0000"), "Red" ),
`item(`id("00FF00"), "Blue",true), // Initially selected
`item(`id("0000FF"), "Green" )
] ),
);
// Set initial color
setColor( UI::QueryWidget(`id(`colors ), `SelectedItem ) );
map event = $[];
repeat
{
event = UI::WaitForEvent();
if ( event["ID"]:nil == `colors )
{
if ( event["EventReason"]:nil == "SelectionChanged" )
{
// Handle color change
setColor( UI::QueryWidget(`id(`colors ), `SelectedItem ) );
}
}
...
} until ( event["ID"]:nil == `close );
}
It's that easy. This small change can make code reliable or subject to failure on minor outside changes - like a version of the Qt lib that handles things differently and sends another SelectionChanged Qt signal that might be mapped to a SelectionChanged WidgetEvents - or does not send that signal any more like previous versions might have done.
Being sceptical and not believing anything, much less taking anything for granted is an attitude that most programmers adopt as they gain more an more programming experience.
Keep it that way. It's a healthy attitude. It helps to avoid a lot of problems in the first place that might become hard-to-find bugs after a while.
This section describes only those builtin functions of the YaST2 user interface that are relevant for event handling. The YaST2 UI has many more builtin functions that are not mentioned here. Refer to the UI builtin reference for details.
The Event-related UI Builtin are available in the reference
Use WaitForEvent() to get full information about a YaST2 UI event. UserInput() only returns a small part of that information, the ID field of the event map.
The event map returned by WaitForEvent() always contains at least the following elements:
| Map Key | Value Type | Valid Values | Description |
|---|---|---|---|
| EventType | string |
| |
| ID | any | The ID (a widget ID for WidgetEvents) that caused the event. This is what UserInput() returns. | |
| EventSerialNo | integer | >= 0 | The serial number of this event. Intended for debugging. |
All WidgetEvents have these map fields in common:
| Map Key | Value Type | Valid Values | Description |
|---|---|---|---|
| EventType | string | WidgetEvent | (constant) |
| EventReason | string | The reason for this event. This is something like an event sub-type. Use this to find out what the user really did with the widget. | |
| ID | any | The ID of the widget that caused the event. This is what UserInput() returns. | |
| WidgetID | any | The ID of the widget that caused the event. This is nothing but an alias for "ID", but with this alias you can easily find out if this is a widget event at the same time as you retrieve the widget ID: No other events than WidgetEvent have this field. | |
| WidgetClass | string | PushButton SelectionBox Table CheckBox ... | The class (type) of the widget that caused the event. |
| WidgetDebugLabel | string |
The label (more general: the widget's shortcut property) of the widget that caused the event - in human readable form without any shortcut markers ("&"), maybe abbreviated to a reasonable length. This label is translated to the current locale (the current user's language). This is intended for debugging so you can easily dump something into the log file when you get an event. Wigets that don't have a label don't add this field to the event map, so make sure you use a reasonable default when using a map lookup for this field: Don't use nil, use "" (the emtpy string) instead. |
| Map Key | Value Type | Valid Values | Description |
|---|---|---|---|
| EventReason | string | Activated | (constant) |
An Activated WidgetEvent is sent when the user explicitly wishes to activate an action.
Traditionally, this means clicking on a PushButton or activating it with some other means like pressing its shortcut key combination, moving the keyboard focus to it and pressing space.
Some other widgets (Table, SelectionBox, Tree) can also trigger this kind of event if they have the notify option set.
User interface style hint: YCP applications should use this to do the "typical" operation of that item - like editing an entry if the dialog has an "Edit" button. Use this Activated WidgetEvent only as a redundant way (for "power users") of invoking an action. Always keep that "Edit" (or similar) button around for novice users; double-clicks are by no way obvious. The user shouldn't need to experiment how to get things done.
| Widget Type | Widget Options | Action to Trigger the Event |
|---|---|---|
| PushButton | (none) |
|
| Table | `opt(`notify) |
|
| SelectionBox | `opt(`notify) |
|
| Tree | `opt(`notify) |
|
Note that MenuButton and RichText don't ever send WidgetEvents. They send MenuEvents instead.
| Map Key | Value Type | Valid Values | Description |
|---|---|---|---|
| EventReason | string | ValueChanged | (constant) |
A ValueChanged WidgetEvent is sent by most interactive widgets that have a value that can be changed by the user. They all require the notify option to be set to send this event.
Widgets that have the concept of a "selected item" like SelectionBox, Table, or Tree don't send this event - they send a SelectionChanged WidgetEvent instead. One exception to this rule is the MultiSelectionBox which can send both events, depending on what the user did.
| Widget Type | Widget Options | Action to Trigger the Event |
|---|---|---|
| MultiSelectionBox | `opt(`notify) | Toggle an item's on/off state:
|
| CheckBox | `opt(`notify) | Toggle the on/off state:
|
| RadioButton | `opt(`notify) | Set this RadioButton to on:
|
| `opt(`notify) | Enter text. | |
| ComboBox | `opt(`notify) |
|
| IntField | `opt(`notify) | Change the numeric value:
|
| Slider | `opt(`notify) |
|
| PartitionSplitter | `opt(`notify) |
|
| Map Key | Value Type | Valid Values | Description |
|---|---|---|---|
| EventReason | string | SelectionChanged | (constant) |
A SelectionChanged WidgetEvent is sent by most widgets that have the concept of a "selected item" like SelectionBox, Table, or Tree when the selected item changes.
Note that the MultiSelectionBox widget can send a SelectionChanged event, but also a ValueChanged WidgetEvent depending on what the user did. This is one reason to keep SelectionChanged and ValueChanged two distinct events: Widgets can have both concepts which may be equally important, depending on the YCP application.
The ComboBox never sends a SelectionChanged event. It only sends ValueChanged WidgetEvents.
The rationale behind this is that merely opening the drop-down list without actually accepting one of its items is just a temporary operation in a separate pop-up window (the drop-down list) that should not affect the YCP application or other widgets in the same dialog until the user actually accepts a value - upon which event a ValueChanged WidgetEvent is sent.
| Widget Type | Widget Options | Action to Trigger the Event | ||||
|---|---|---|---|---|---|---|
| SelectionBox |
| Select another item:
| ||||
| Qt: | `opt(`notify) | |||||
| NCurses: | `opt(`notify,`immediate) | |||||
| Table | `opt(`notify,`immediate) | Select another item:
| ||||
| Tree | `opt(`notify) | Select another item:
| ||||
| MultiSelectionBox | `opt(`notify) | Select another item:
|
| Map Key | Value Type | Valid Values | Description |
|---|---|---|---|
| EventType | string | MenuEvent | (constant) |
| ID | any |
The ID of the menu item the user selected or the href target (as string) for hyperlinks in RichText widgets. Notice:This is not the widget ID, it is a menu item or hyperlink ID inside that MenuButton or RichText widget! |
A MenuEvent is sent when the user activates a menu entry in a MenuButton or a hyperlink in a RichText widget.
Since the ID of the MenuButton or RichText widget is irrelevant in either case, this is not another subclass of WidgetEvent; the ID field has different semantics - and remember, the ID field is the only thing what UserInput() returns so this is particularly important.
For most YCP applications this difference is purely academic. Simply use the ID and treat it like it were just another button's ID.
No notify option is necessary for getting this event. Both MenuButton and RichText deliver MenuEvents right away.
| Map Key | Value Type | Valid Values | Description |
|---|---|---|---|
| EventType | string | TimeoutEvent | (constant) |
| ID | symbol | `timeout | (constant) |
A TimeoutEvent is sent when the timeout specified at WaitForEvent() or TimeoutUserInput() is expired and there is no other event pending (i.e. there is no other user input).
PollInput() never returns a TimeoutEvent; it simply returns nil if there is no input.
| Map Key | Value Type | Valid Values | Description |
|---|---|---|---|
| EventType | string | CancelEvent | (constant) |
| ID | symbol | `cancel | (constant) |
A CancelEvent is an event that is sent when the user performs a general "cancel" action that is usually not part of the YCP application.
For the Qt UI, this means he used the window manager close button or a special key combination like Alt-F4 to close the active dialog's window. For the NCurses UI, this means he hit the ESC key.
User interface style hint: It is usually a good idea for each dialog to provide some kind of "safe exit" anyway. Most popup dialogs (at least those that have more than just a simple "OK" button) should provide a "Cancel" button. If you use the widget ID `cancel for that button, CancelEvents integrate seamlessly into your YCP application.
"Main window" type dialogs should have an "Abort" button or something similar. If you don't use the widget ID `cancel for that button, don't forget to handle `cancel or "CancelEvent" like that "Abort" button. The user should always have a safe way out of a dialog - preferably one that doesn't change anything. Don't forget to add a confirmation popup before you really exit if there are unsaved data that might get lost!
KeyEvents are specific to the NCurses UI. They are not intended for general usage. The idea is to use them where the default keyboard focus handling is insufficient - for example, when the logical layout of a dialog is known and the keyboard focus should be moved to the logically right widget upon pressing the cursor right key.
Widgets deliver KeyEvents if they have `opt( keyEvent )
set. This is independent of the notify option.
It is completely up to the UI what key presses are delivered as key events. Never rely on each and every key press to be delivered.
| Map Key | Value Type | Valid Values | Description |
|---|---|---|---|
| EventType | string | KeyEvent | (constant) |
| ID | string |
| The key symbol of this event in human readable form. This is what UserInput() returns. |
| KeySymbol | string |
| The key symbol of this event in human readable form. This is nothing but an alias for "ID", but with this alias you can easily find out if this is a key event at the same time as you retrieve the key symbol: No other events than KeyEvent have this field. |
| FocusWidgetID | any | The ID of the widget that currently has the keyboard focus. Unlike a WidgetEvent, this is not the same as "ID". | |
| FocusWidgetClass | string | TextEntry SelectionBox ... | The class (type) of the widget that has the keyboard focus. |
| FocusWidgetDebugLabel | string |
The label (more general: the widget's shortcut property) |