Product SiteDocumentation Site

6.3. Textual user interface

The Section 1, “Introduction” mentions that apart from the GUI installation, the Anaconda installer also supports text mode installation that may be the only choice on some hardware configurations. The previous Section 6.2, “Graphical user interface” describes, how it is possible for an addon to define and implement graphical screens for the installer allowing user interaction. Now it's time to have a look at the text mode which is based on the Anaconda's simpleline toolkit that is suitable for purely textual output without any "advanced" features like colours, fonts, cursor movement etc.
Internally, there are three main classes in the simpleline toolkit — App, UIScreen and Widget. Widgets, which are elemental units containing the information to be shown (printed) to the user, are placed on the UIScreens that are switched by a single instance of the App class. On top of those basic elements there are hubs, spokes and dialogs all containing various widgets similarly as in the GUI. So from the addon's perspective, the most important classes are the NormalTUISpoke and various other classes defined in the pyanaconda.ui.tui.spokes package. All those classes are based on the TUIObject class which is an equivalent of the GUIObject class mentioned in the previous chapter.
Creating a text spoke for an addon again means creating subpackages of the main addon's package. This time it has to be named tui and the directory for spokes has to be, surprisingly, named spokes. A TUI spoke is again a Python class this time inheriting from the NormalTUISpoke class overriding special arguments and methods defined by the API. Because the TUI is simpler than the GUI there are fewer such arguments, namely two — title and category. These two have the same meaning as in the case of the GUI spoke. However, categories are used only for grouping and ordering spokes on hubs in the text mode (their titles are not shown anywere), so the easiest thing is to use some preexisting category.
Apart from the two arguments, the spoke class is expected to define (override) a few methods — __init__, initialize, refresh, refresh, apply, execute, input, prompt — and properties described in the Section 6.2, “Graphical user interface” for the case of a GUI spoke. Let's have a look on a trivial TUI spoke defined in the Hello world addon. We could start with the methods:
    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: ")
There is no need to override the __init__ method if it just calls the ancestor's __init__ method, but the comments in the example code describe the arguments passed to constructors of spoke classes in an understandable way. The initialize method just sets up a default value for the spoke's internal attribute which is then updated by the refresh method and used by the apply method to update the kickstart data. The only differences from the GUI equivalents of those two methods are the return type of the refresh method that is bool instead of None and an additional args argument it takes. The meaning of the returned value is explained in the comment — it tells the application (the single App class instance) whether that spoke requires input from user or not. The additional args argument is used for passing extra information to the spoke when it scheduled. Then there is also the execute method with the same purpose as in the GUI and with the same pass statement doing all that is needed in such a trivial case.
The input and prompt methods are TUI-specific and as thier names suggest they are responsible for interaction with the user. The prompt method should simply return the prompt that should be printed once the content of the spoke is printed. After a user enters some string in a reaction to the prompt, the entered string is passed to the input method for processing. The input method usually needs to parse the input and take some actions based on its type and value. Since our spoke just asks for some value, the value is simply stored in an internal attribute. But typically there are some non-trivial actions done like accepting the 'c' or 'r' inputs for continuing or refreshing, respectively, converting numbers into integers and showing additional screens or toggling bool values based on them etc. In contrast to the GUI code, the apply method is not called automatically when leaving the spoke, so we need to call it explicitly from the input method. The same applies to closing (hiding) the spoke's screen done by calling the close method of the spoke. If we want to show another screen (need some additional info from user entered in a different spoke, dialog, etc.) we can also instantiate another TUIObject here and call one of the self.app.switch_screen* methods of the App. The last interesting thing about the input method is its return value. It has to be either INPUT_PROCESSED or INPUT_DISCARDED constant (both defined in the pyanaconda.constants_text module) or the input string itself in case such an input should be processed by some other screen.
Since the restrictions of the text user interface are quite strong the TUI spokes typically have a very similar structure — a list of checkboxes or entries that should be (un)checked or populated by the user. The previous paragraphs show the imperative way of implementing such TUI spoke where the spoke's methods handle printing and processing of the available and provided data. However, there is a different, simpler way of doing that by using the declarative EditTUISpoke class from the pyanaconda.ui.tui.spokes package. By inheriting from this class, it is possible to implement a typical TUI spoke by just specifying fields and attributes that should be set on the spoke. The following code defines an example spoke implemented that way:
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
The auxiliary class _EditData just serves as a data container that is used for storing values entered by the user. The HelloWorldEditSpoke 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 and the second one that is shown only if there is some non-empty value set in the first one. The comments in the example code should be enough explanatory to guide reader through the declarative definition of a TUI spoke by using the EditTUISpoke class.