tui directory as described in Section 5, “Anaconda Add-on Structure”.
simpleline utility, which only allows very simple user interaction. It does not support cursor movement (instead acting like a line printer) nor any visual enhancements like using different colors or fonts.
simpleline toolkit: App, UIScreen and Widget. Widgets, which are units containing information to be shown (printed) on the screen, are placed on UIScreens which are switched by a single instance of the App class. On top of the basic elements, there are hubs, spokes and dialogs, all containing various widgets in a way similar to the graphical interface.
NormalTUISpoke and various other classes defined in the pyanaconda.ui.tui.spokes package. All of those classes are based on the TUIObject class, which itself is an equivalent of the GUIObject class discussed in the previous chapter. Each TUI spoke is a Python class inheriting from the NormalTUISpoke class, overriding special arguments and methods defined by the API.
title, which determines the title of the spoke, and category, which determines its category (the category name is not displayed anywhere, it is only used for grouping). Both have the same meanings as the equivalent GUI arguments, described in the previous section.
__init__, initialize, refresh, refresh, apply, execute, input, and prompt, and properties (ready, completed, mandatory, and status). All of these have already been described in Section 6.2, “Graphical user interface”.
Example 13. Defining a Simple TUI Spoke
def __init__(self, app, data, storage, payload, instclass): """ :see: pyanaconda.ui.tui.base.UIScreen :see: pyanaconda.ui.tui.base.App :param app: reference to application which is a main class for TUI screen handling, it is responsible for mainloop control and keeping track of the stack where all TUI screens are scheduled :type app: instance of pyanaconda.ui.tui.base.App :param data: data object passed to every spoke to load/store data from/to it :type data: pykickstart.base.BaseHandler :param storage: object storing storage-related information (disks, partitioning, bootloader, etc.) :type storage: blivet.Blivet :param payload: object storing packaging-related information :type payload: pyanaconda.packaging.Payload :param instclass: distribution-specific information :type instclass: pyanaconda.installclass.BaseInstallClass """ NormalTUISpoke.__init__(self, app, data, storage, payload, instclass) self._entered_text = "" def initialize(self): """ The initialize method that is called after the instance is created. The difference between __init__ and this method is that this may take a long time and thus could be called in a separated thread. :see: pyanaconda.ui.common.UIObject.initialize """ NormalTUISpoke.initialize(self) def refresh(self, args=None): """ The refresh method that is called every time the spoke is displayed. It should update the UI elements according to the contents of self.data. :see: pyanaconda.ui.common.UIObject.refresh :see: pyanaconda.ui.tui.base.UIScreen.refresh :param args: optional argument that may be used when the screen is scheduled (passed to App.switch_screen* methods) :type args: anything :return: whether this screen requests input or not :rtype: bool """ self._entered_text = self.data.addons.org_fedora_hello_world.text return True def apply(self): """ The apply method that is called when the spoke is left. It should update the contents of self.data with values set in the spoke. """ self.data.addons.org_fedora_hello_world.text = self._entered_text def execute(self): """ The excecute method that is called when the spoke is left. It is supposed to do all changes to the runtime environment according to the values set in the spoke. """ # nothing to do here pass def input(self, args, key): """ The input method that is called by the main loop on user's input. :param args: optional argument that may be used when the screen is scheduled (passed to App.switch_screen* methods) :type args: anything :param key: user's input :type key: unicode :return: if the input should not be handled here, return it, otherwise return True or False if the input was processed succesfully or not respectively :rtype: bool|unicode """ if key: self._entered_text = key # no other actions scheduled, apply changes self.apply() # close the current screen (remove it from the stack) self.close() return True def prompt(self, args=None): """ The prompt method that is called by the main loop to get the prompt for this screen. :param args: optional argument that can be passed to App.switch_screen* methods :type args: anything :return: text that should be used in the prompt for the input :rtype: unicode|None """ return _("Enter a new text or leave empty to use the old one: ")
__init__ method if it only calls the ancestor's __init__, but the comments in the example describe the arguments passed to constructors of spoke classes in an understandable way.
initialize method sets up a default value for the internal attribute of the spoke, which is then updated by the refresh method and used by the apply method to update Kickstart data. The only differences in these two methods from their equivalents in the GUI is the return type of the refresh method (bool instead of None) and an additional args argument they take. The meaning of the returned value is explained in the comments - it tells the application (the App class instance) whether this spoke requires user input or not. The additional args argument is used for passing extra information to the spoke when scheduled.
execute method has the same purpose as the equivalent method in the GUI; in this case, the method does nothing.
input and prompt are specific to the text interface; there are no equivalents in Kickstart or GUI. These two methods are responsible for user interaction.
prompt method should return a prompt which will be displayed after the content of the spoke is printed. After a string is entered in reaction to the prompt, this string is passed to the input method for processing. The input method then processes the entered string and takes action depending on its type and value. The above example asks for any value and then stores it as an internal attribute (key). In more complicated add-ons, you typically need to perform some non-trivial actions, such as parse c as "continue" or r as "refresh", convert numbers into integers, show additional screens or toggle boolean values.
input class must be either the INPUT_PROCESSED or INPUT_DISCARDED constant (both of these are defined in the pyanaconda.constants_text module), or the input string itself (in case this input should be processed by a different screen).
apply method is not called automatically when leaving the spoke; it must be called explicitly from the input method. The same applies to closing (hiding) the spoke's screen, which is done by calling the close method.
TUIObject and call one of the self.app.switch_screen* methods of the App.
EditTUISpoke class from the pyanaconda.ui.tui.spokes package. By inheriting this class, you can implement a typical TUI spoke by only specifying fields and attributes which should be set in it. The example below demonstrates this:
Example 14. Using EditTUISpoke to Define a Text Interface Spoke
class _EditData(object): """Auxiliary class for storing data from the example EditSpoke""" def __init__(self): """Trivial constructor just defining the fields that will store data""" self.checked = False self.shown_input = "" self.hidden_input = "" class HelloWorldEditSpoke(EditTUISpoke): """Example class demonstrating usage of EditTUISpoke inheritance""" title = _("Hello World Edit") category = HelloWorldCategory # simple RE used to specify we only accept a single word as a valid input _valid_input = re.compile(r'\w+') # special class attribute defining spoke's entries as: # Entry(TITLE, ATTRIBUTE, CHECKING_RE or TYPE, SHOW_FUNC or SHOW) # where: # TITLE specifies descriptive title of the entry # ATTRIBUTE specifies attribute of self.args that should be set to the # value entered by the user (may contain dots, i.e. may specify # a deep attribute) # CHECKING_RE specifies compiled RE used for deciding about # accepting/rejecting user's input # TYPE may be one of EditTUISpoke.CHECK or EditTUISpoke.PASSWORD used # instead of CHECKING_RE for simple checkboxes or password entries, # respectively # SHOW_FUNC is a function taking self and self.args and returning True or # False indicating whether the entry should be shown or not # SHOW is a boolean value that may be used instead of the SHOW_FUNC # # :see: pyanaconda.ui.tui.spokes.EditTUISpoke edit_fields = [ Entry("Simple checkbox", "checked", EditTUISpoke.CHECK, True), Entry("Always shown input", "shown_input", _valid_input, True), Entry("Conditioned input", "hidden_input", _valid_input, lambda self, args: bool(args.shown_input)), ] def __init__(self, app, data, storage, payload, instclass): EditTUISpoke.__init__(self, app, data, storage, payload, instclass) # just populate the self.args attribute to have a store for data # typically self.data or a subtree of self.data is used as self.args self.args = _EditData() @property def completed(self): # completed if user entered something non-empty to the Conditioned input return bool(self.args.hidden_input) @property def status(self): return "Hidden input %s" % ("entered" if self.args.hidden_input else "not entered") def apply(self): # nothing needed here, values are set in the self.args tree pass
_EditData serves as a data container which is used to store values entered by the user. The HelloWorldEditSpoke class defines a simple spoke with one checkbox and two entries, all of which are instances of the EditTUISpokeEntry class imported as the Entry class). The first one is shown every time the spoke is displayed, the second instance is only shown if the first one contains a non-empty value.
EditTUISpoke class, see the comments in the above example.