Product SiteDocumentation Site

6.3. Text User Interface

The third supported interface, after Kickstart and GUI which have been discussed in previous sections, Anaconda also supports a text-based interface. This interface is more limited in its capabilities, but on some systems it may be the only choice for an interactive installation. For more information about differences between the text-based and graphical interface and about limitations of the TUI, see Section 1.1, “Introduction to Anaconda”.
To add support for the text interface into your add-on, create a new set of subpackages under the tui directory as described in Section 5, “Anaconda Add-on Structure”.
Text mode support in the installer is based on the 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.
Internally, there are three main classes in the 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.
For an add-on, the most important classes are 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.
Because the text interface is simpler than the GUI, there are only two such arguments: 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.
Each spoke is also expected to override several methods, namely __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”.
The example below shows the implementation of a simple TUI spoke in the Hello World sample add-on:

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: ")
It is not necessary to override the __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.
The 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.
The execute method has the same purpose as the equivalent method in the GUI; in this case, the method does nothing.
Methods 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.
The 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.
Return value of the 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).
In contrast to the graphical mode, the 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.
To show another screen (for example, if you need additional information which was entered in a different spoke), you can instantiate another TUIObject and call one of the self.app.switch_screen* methods of the App.
Due to restrictions of the text-based interface, TUI spokes tend to have a very similar structure: a list of checkboxes or entries which should be checked or unchecked and populated by the user. The previous paragraphs show a way to implement a TUI spoke where the its methods handle printing and processing of the available and provided data. However, there is a different way to accomplish this using the 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
The auxiliary class _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.
For more information about the EditTUISpoke class, see the comments in the above example.