openSUSE:YaST quick tutorial
Introduction
YaST modules are written in the YCP language. To test the examples call:
/usr/lib/YaST2/bin/y2base example.ycp qt
for graphical user interface, or for ncurses:
/usr/lib/YaST2/bin/y2base example.ycp ncurses
Hello World
// Hello, World in YCP { UI::OpenDialog( `VBox( `Label("Hello, World!"), `PushButton("OK") ) ); UI::UserInput(); UI::CloseDialog(); }
Imagine we have the following config file in /etc:
# Demo config file for writing YaST2 modules: # # Server configuration for a (phoney) network game "sonic" # port = 5160 public = yes max_players = 40 map_dir = /usr/share/sonic/maps map = forest.map respawn_time = 10 ban_list = /etc/sonic/banned admin_password = "k1s43zz8"
// UI for simple sonic-server.conf editor // using TextEntry for most fields { UI::OpenDialog(`VBox( `TextEntry( "Port" ), `CheckBox( "Public Access" ), `TextEntry( "Max. Players" ), `TextEntry( "Map Directory" ), `TextEntry( "Map" ), `TextEntry( "Respawn Time" ), `TextEntry( "Ban List" ), `TextEntry( "Admin Password" ), `PushButton( "OK" ) ) ); UI::UserInput(); UI::CloseDialog(); }
But let's use specific widgets for the kind of data we are supposed to allow there:
// UI for simple sonic-server.conf editor // using type-specific widgets { UI::OpenDialog(`VBox( `Heading( "Sonic Server Configuration" ), `IntField ("Port", 5155, 5164, 5160 ), `CheckBox ( "Public Access" ), `IntField ( "Max. Players", 2, 999, 10 ), `TextEntry( "Map Directory" ), `TextEntry( "Map" ), `IntField ( "Respawn Time", 0, 3600, 5 ), `TextEntry( "Ban List" ), `Password ( "Admin Password" ), `PushButton( "OK" ) ) ); UI::UserInput(); UI::CloseDialog(); }
In order to fetch the values from the widgets (user interface elements, such as checkboxes, text entry fields, etc.), we need to name them, so let's add IDs to each one:
// UI for simple sonic-server.conf editor // using IDs for all widgets so values can be fetched and stored { UI::OpenDialog(`VBox( `Heading( "Sonic Server Configuration" ), `IntField (`id(`port ), "Port", 5155, 5164, 5160 ), `CheckBox (`id(`public ), "Public Access" ), `IntField (`id(`max_pl ), "Max. Players", 2, 999, 10 ), `TextEntry(`id(`map_dir ), "Map Directory" ), `TextEntry(`id(`map ), "Map" ), `IntField (`id(`respawn ), "Respawn Time", 0, 3600, 5 ), `TextEntry(`id(`ban_list ), "Ban List" ), `Password (`id(`admin_pw ), "Admin Password" ), `PushButton(`id(`ok), "OK" ) ) ); UI::UserInput(); UI::CloseDialog(); }
Let's polish it a little more, adding margins and a cancel button:
// UI for simple sonic-server.conf editor // // look somewhat polished: margins, spacing, alignment // added a "Cancel" button { UI::OpenDialog( `MarginBox( 2, 0.3, `VBox( `Heading( "Sonic Server Configuration" ), `VSpacing( 1 ), `IntField (`id(`port ), "Port", 5155, 5164, 5160 ), `Left( `CheckBox (`id(`public ), "Public Access" ) ), `IntField (`id(`max_pl ), "Max. Players", 2, 999, 10 ), `TextEntry(`id(`map_dir ), "Map Directory" ), `TextEntry(`id(`map ), "Map" ), `IntField (`id(`respawn ), "Respawn Time", 0, 3600, 5 ), `TextEntry(`id(`ban_list ), "Ban List" ), `Password (`id(`admin_pw ), "Admin Password" ), `VSpacing( 0.5 ), `HBox( `PushButton(`id(`ok ), "OK" ), `PushButton(`id(`cancel ), "Cancel" ) ) ) ) ); UI::UserInput(); UI::CloseDialog(); }
We can modularize it. That means, we construct it part by part and save them all in a term. Then, when opening the dialog, we pass the term instead of all the dialog widgets. That can be useful to make the code easier to read, and it allows us to use the same component in more than one dialog.
// UI for simple sonic-server.conf editor // dialog modularized { term fields = `VBox( `IntField (`id(`port ), "Port", 5155, 5164, 5160 ), `Left( `CheckBox (`id(`public ), "Public Access" ) ), `IntField (`id(`max_pl ), "Max. Players", 2, 999, 10 ), `TextEntry(`id(`map_dir ), "Map Directory" ), `TextEntry(`id(`map ), "Map" ), `IntField (`id(`respawn ), "Respawn Time", 0, 3600, 5 ), `TextEntry(`id(`ban_list ), "Ban List" ), `Password (`id(`admin_pw ), "Admin Password" ) ); term button_box = `HBox( `PushButton(`id(`ok ), "OK" ), `PushButton(`id(`cancel ), "Cancel" ) ); UI::OpenDialog( `MarginBox( 2, 0.3, `VBox( `Heading( "Sonic Server Configuration" ), `VSpacing( 0.7 ), fields, `VSpacing( 0.5 ), button_box ) ) ); UI::UserInput(); UI::CloseDialog(); }
Now, we turn it into a wizard:
// UI for sonic-server.conf editor in "wizard" look & feel { import "Wizard"; term fields = `VBox( `IntField (`id(`port ), "Port", 5155, 5164, 5160 ), `Left( `CheckBox (`id(`public ), "Public Access" ) ), `IntField (`id(`max_pl ), "Max. Players", 2, 999, 10 ), `TextEntry(`id(`map_dir ), "Map Directory" ), `TextEntry(`id(`map ), "Map" ), `IntField (`id(`respawn ), "Respawn Time", 0, 3600, 5 ), `TextEntry(`id(`ban_list ), "Ban List" ), `Password (`id(`admin_pw ), "Admin Password" ) ); string help_text = "Help text (to do)"; Wizard::OpenAcceptDialog(); Wizard::SetContents( "Sonic Server Configuration", // Dialog title `MarginBox( 10, 0, fields ), // Dialog contents help_text, true, // "Cancel" button enabled? true ); // "Accept" button enabled? UI::UserInput(); UI::CloseDialog(); }
Now we want to read and write data to and from the config file. For that, we write an agent, based on the config file agent.
/** * File: etc_sonic_sonic_server_conf.scr * Summary: Agent for reading/writing (bogus) sonic server configuration * Author: Stefan Hundhammer <sh@suse.de> * Access: read / write * * Reads/writes the values defined in /etc/sonic/sonic-server.conf */ .etc.sonic.sonic_server_conf `ag_ini( `SysConfigFile("/etc/sonic/sonic-server.conf") )
Here's a working version of the wizard which uses the agent to read and write:
// sonic-server.conf editor that actually reads and writes the config file // // trivial version - lots of copy & paste { import "Wizard"; // Read values from file /etc/sonic/sonic_server.conf string port = (string) SCR::Read( .etc.sonic.sonic_server_conf.port ); string public = (string) SCR::Read( .etc.sonic.sonic_server_conf.public ); string max_players = (string) SCR::Read( .etc.sonic.sonic_server_conf.max_players ); string map_dir = (string) SCR::Read( .etc.sonic.sonic_server_conf.map_dir ); string current_map = (string) SCR::Read( .etc.sonic.sonic_server_conf.map ); string respawn = (string) SCR::Read( .etc.sonic.sonic_server_conf.respawn_time ); string ban_list = (string) SCR::Read( .etc.sonic.sonic_server_conf.ban_list ); string admin_pw = (string) SCR::Read( .etc.sonic.sonic_server_conf.admin_password ); // Build dialog term fields = `VBox( `IntField (`id(`port ), "Port", 5155, 5164, tointeger( port ) ), `Left( `CheckBox (`id(`public ), "Public Access", public == "yes" ) ), `IntField (`id(`max_pl ), "Max. Players", 2, 999, tointeger( max_players ) ), `TextEntry(`id(`map_dir ), "Map Directory" , map_dir ), `TextEntry(`id(`map ), "Map" , current_map ), `IntField (`id(`respawn ), "Respawn Time", 0, 3600, tointeger( respawn ) ), `TextEntry(`id(`ban_list ), "Ban List" , ban_list ), `Password (`id(`admin_pw ), "Admin Password" , admin_pw ) ); string help_text = "Help text (to do)"; Wizard::OpenAcceptDialog(); Wizard::SetContents( "Sonic Server Configuration", // Dialog title `MarginBox( 10, 0, fields ), // Dialog contents help_text, true, // "Cancel" button enabled? true ); // "Accept" button enabled? // Dialog input loop symbol button = nil; repeat { button = (symbol) UI::UserInput(); } until ( button == `accept || button == `cancel ); if ( button == `accept ) { // Read field contents port = sformat( "%1", UI::QueryWidget(`port, `Value ) ); public = (boolean) UI::QueryWidget(`public, `Value ) ? "yes" : "no"; max_players = sformat( "%1", UI::QueryWidget(`max_pl, `Value ) ); map_dir = (string) UI::QueryWidget(`map_dir, `Value ); current_map = (string) UI::QueryWidget(`map, `Value ); respawn = sformat( "%1", UI::QueryWidget(`respawn, `Value ) ); ban_list = (string) UI::QueryWidget(`ban_list, `Value ); admin_pw = (string) UI::QueryWidget(`admin_pw, `Value ); // Write values back to file /etc/sonic/sonic_server.conf SCR::Write( .etc.sonic.sonic_server_conf.port , port ); SCR::Write( .etc.sonic.sonic_server_conf.public , public ); SCR::Write( .etc.sonic.sonic_server_conf.max_players , max_players ); SCR::Write( .etc.sonic.sonic_server_conf.map_dir , map_dir ); SCR::Write( .etc.sonic.sonic_server_conf.map , current_map ); SCR::Write( .etc.sonic.sonic_server_conf.respawn_time , respawn ); SCR::Write( .etc.sonic.sonic_server_conf.ban_list , ban_list ); SCR::Write( .etc.sonic.sonic_server_conf.admin_password , admin_pw ); // Post a popup dialog UI::OpenDialog(`VBox( `Label( "Values written to\n/etc/sonic/sonic_server.conf" ), `PushButton(`opt(`default), "&OK" ) ) ); UI::TimeoutUserInput( 4000 ); // millisec UI::CloseDialog(); // Closes the popup } // The (main) dialog needs to remain open until here, // otherwise the widgets that are queried no longer exist! UI::CloseDialog(); }
But now we want to do some refactoring, splitting the code into functions:
// sonic-server.conf editor that actually reads and writes the config file // // more elegant version // // demo showing how to work with functions, lists, maps, terms, paths { import "Wizard"; typedef map<string, any> ConfigEntry; list<ConfigEntry> sonic_config = [ $[ "name": "port" , "type": "int" , "def": 5455, "min": 5155, "max": 5164 ], $[ "name": "public" , "type": "bool" , "def": true ], $[ "name": "max_players" , "type": "int" , "def": 50, "min": 2, "max": 999 ], $[ "name": "map_dir" , "type": "string" ], $[ "name": "map" , "type": "string" ], $[ "name": "respawn_time" , "type": "int" , "def": 7, "min": 0, "max": 3600 ], $[ "name": "ban_list" , "type": "string" ], $[ "name": "admin_password", "type": "password" ] ]; path sonic_config_path = .etc.sonic.sonic_server_conf; // Mapping from config entry names to widget captions // (in a real life example this would go to the ConfigEntry map, too) map<string, string> widget_caption = $[ "port" : "Port", "public" : "Public Access", "max_players" : "Max. Players", "map_dir" : "Map Directory", "map" : "Map", "respawn_time" : "Respawn Time", "ban_list" : "Ban List", "admin_password" : "Admin Password" ]; /** * Create a widget for a config entry * * Parameters: * entry map describing the config entry * scr_base_path SCR path to use (entry["name"] will be added) * * Return: * term describing the widget **/ term entryWidget( ConfigEntry entry, path scr_base_path ) { string name = (string) entry[ "name" ]:nil; string type = (string) entry[ "type" ]:"string"; string caption = widget_caption[ name ]:name; // Read current value from config file any value = SCR::Read( add( scr_base_path, name ) ); // Create corresponding widget term widget = nil; if ( type == "string" ) { widget = `TextEntry( `id( name ), caption, value != nil ? value : "" ); } else if ( type == "password" ) { widget = `Password( `id( name ), caption, value != nil ? value : "" ); } else if ( type == "bool" ) { widget = `Left( `CheckBox( `id( name ), caption, value == "yes" ) ); } else if ( type == "int" ) { integer min = tointeger( entry[ "min" ]:0 ); integer max = tointeger( entry[ "max" ]:65535 ); widget = `IntField( `id( name ), caption, min, max, value != nil ? tointeger( value ) : min ); } else { y2error( "Unknown type in config entry: %1", entry ); } return widget; } /** * Create widgets in a VBox for a list of config entries * * Parameters: * scr_base_path SCR path to use (entry["name"] will be added) * entries list of maps describing the config entries * * Return: * widget term **/ term configWidgets( list<ConfigEntry> entry_list, path scr_base_path ) { term vbox = `VBox(); foreach( ConfigEntry entry, entry_list, { term widget = entryWidget( entry, scr_base_path ); if ( widget != nil ) vbox = add( vbox, widget ); }); return vbox; } /** * Write a list of configuration entries to file. * Get the current value for each entry from a widget in the current dialog. **/ void writeConfig( list<ConfigEntry> entry_list, path scr_base_path ) { foreach( ConfigEntry entry, entry_list, { string name = (string) entry[ "name" ]:nil; if ( name == nil ) { y2error( "Entry without name in entry list: %1", entry ); } else { any value = UI::QueryWidget(`id( name ), `Value ); if ( is( value, boolean ) ) value = ( (boolean) value ) ? "yes" : "no"; SCR::Write( add( scr_base_path, name ), value ); } }); } /** * Display an information popup dialog with a timeout. * A timeout of 0 means "no timeout, wait for user to click". **/ void infoPopup( string message, integer timeout_sec ) { UI::OpenDialog(`VBox( `Label( message ), `PushButton(`opt(`default), "&OK" ) ) ); UI::TimeoutUserInput( timeout_sec * 1000 ); UI::CloseDialog(); } // // Main // string help_text = "Help text (to do)"; term fields = configWidgets( sonic_config, sonic_config_path ); Wizard::OpenAcceptDialog(); Wizard::SetContents( "Sonic Server Configuration", // Dialog title `MarginBox( 10, 0, fields ), // Dialog contents help_text, true, // "Cancel" button enabled? true ); // "Accept" button enabled? // Dialog input loop symbol button = nil; repeat { button = (symbol) UI::UserInput(); } until ( button == `accept || button == `cancel ); if ( button == `accept ) { writeConfig( sonic_config, sonic_config_path ); infoPopup( "Values written to\n/etc/sonic/sonic_server.conf", 4 ); } // The (main) dialog needs to remain open until here, // otherwise the widgets that are queried no longer exist! UI::CloseDialog(); }