Hello world

A basic client as a first step

Usually, the first step when creating an application with a graphical interface is drawing some mockups for that interface. It can be done with any available tool (including good old paper&pencil), but when developing a YaST module you will always have to take into account the ncurses interface. With that in mind, maybe using libYUI (the widget abstraction library used by YaST) to write the mockup is not a bad idea.

As explained in "YaST architecture" at the official YaST documentation page, executing YaST means actually calling a YaST client. So let's write a client that will draw our mockup.

Checkout the initial mockup

First of all, let's travel in time to the origin and checkout the initial commit of the repository.

git checkout hello_world

Browsing the code

The repository now contains just a directory called "src" to mimic the structure described in "YaST code organization description" at the documentation. You will find that directory in every YaST module and you will soon find why.

Inside that directory there are only two files: "clients/mockup.rb" and "lib/journalctl/mockup_dialog.rb". The first one is mainly a Ruby script. Whatever code is there, it will be executed right away when calling the client. Several old YaST modules contain clients with the functionality implemented in-line. This is now considered harmful since it makes harder to use automatic test tools. The preferred approach is to encapsulate the functionality in classes and have minimal clients that simply invoke the methods on those classes. As you can see, our mockup client simply calls the method "run" of a MockupDialog object.

require "journalctl/mockup_dialog"
Journalctl::MockupDialog.new.run

All the interesting code for this first "hello world" is in mockup_dialog.rb, starting with the first two lines.

require "yast"
Yast.import "UI"

The first line give you access to all the goodies from the YaST Ruby bindings, which includes several cool features that will be used in this tutorial. To fully understand the second line, you need to know what a YaST module is. As stated in the architecture document from the official YaST documentation, a module is a YaST component encapsulating functionality related to a specific area and offering this functionality to other components through a YCP interface. The method Yast.import loads one of those components and creates a Ruby object for it in the Yast namespace. That object can then be used to access the methods and variables provided by the YCP interface of that module from your Ruby code, no matter in which language the module is written. One final note, if the module is implemented in Ruby then Yast.import is almost equivalent to a good old Ruby's "require", which means that you'll have full access to the Ruby class defined in the module, not only to methods published through its YCP interface. The code above imports the UI module, which is not implemented in Ruby and thus will be used through its published YCP interface.

In the following lines we simply define a Journalctl namespace (for hygienic reasons) and our class including several mixins provided by the YaST Ruby bindings.

module Journalctl
  class MockupDialog
  
    include Yast::UIShortcuts
    include Yast::I18n
    include Yast::Logger

The first mixin deserves a specific paragraph (see below), the other two should be obvious enough. Yast::I18n provides methods for internationalization following the gettext nomenclature and Yast::Logger provides a "log" method returning an object that can be used to write content in the YaST logs.

The first mixin provides several methods to increase the readability of the code used to define the user interface, but to fully understand how it works it's convenient to understand the YCP's concept of "term". A term is one of the basic data types in the YaST Components Protocol and the main mechanism to defer the execution of a piece of code. The definition of the UI is done using a tree-like structure of YCP terms where each term represents a function call. For example, the following ruby code defines a tree of terms that will be translated to the corresponding calls to libYUI functions when needed.

content = Yast::Term.new(:VBox,
               Yast::Term.new(:Label, "Say hello..."),
               Yast::Term.new(:PushButton,
                               Yast::Term.new(:Id, :hello),
                               "now!"))

The code above defines a vertical box which contains a label and a button, with the button being identified by the id :hello. Too much code (and too ugly) for such a small output. Including the UIShorcuts mixin the same code can be written as:

include Yast::UIShortcuts
content = VBox(
  Label("Say hello..."),
  PushButton(Id(:hello), "now!")
)

Much more readable and clean. The only drawback is that now is easier to forget that the "content" variable will not contain the result of calling the VBox function of libYUI, but a tree structure with the function calls (function names and corresponding arguments) that will be performed while actually drawing the interface.

And last but not least, the MockupDialog contains the "run" method which relies on several functions from the UI module and the included mixins to open a dialog, capture the user's input, write that input to the YaST log and close the dialog. In this example the OpenDialog function receives a term with the options and another term containing the tree of widgets to draw (which in this case is a tiny tree with just one label as the only node).

def run

  Yast::UI.OpenDialog(
    Opt(:decorated, :defaultsize),
    Label(_("Hello world!"))
  )

  input = Yast::UI.UserInput

  log.info "Received #{input}"

  Yast::UI.CloseDialog
end

Running the code

Sure you cannot wait to see the code in action and most likely you have already tried to run it before reading this text. Your first attempt would probably be to use the "rake run" task that we already used during the introduction. But that will not work because we have not created a proper Rakefile file yet. So let's forget about the magic for a while and let's run our mockup client manually by just running this from the root of our repository:

Y2DIR=src/ /usr/sbin/yast2 mockup

Pretty straightforward, just like invoking any YaST client installed in the system but using the Y2DIR environment variable to tell YaST to look first into our src directory when looking for YaST components.

Right now, the only possible interaction with our client is closing the dialog. While doing it, take a look to the file ~/.y2log (or /var/log/YaST/y2log if you are running the client as the root user) to see what is happening and to check that our printed message is there.

Let's also take a look to how it looks in the ncurses interface.

Y2DIR=src/ /usr/sbin/yast2 --ncurses mockup

Press "esc" to close the dialog and exit. You can see the debugging output again in ~/.y2log

Rejoice!

Celebrate your first YaST module. Once you are done with celebration, proceed to the second step.